diff --git a/App.vue b/App.vue index d89da34..b00e382 100644 --- a/App.vue +++ b/App.vue @@ -54,9 +54,6 @@ // 检查并更新语言(uni.reLaunch 会触发 onShow) try { const savedLang = uni.getStorageSync('language') - if(savedLang){ - uni.removeStorageSync('language'); - } console.log('App onShow - 缓存中的语言:', savedLang) // 获取当前 i18n 实例并检查语言 diff --git a/components/home/HomeMainH5.vue b/components/home/HomeMainH5.vue index f749b4b..4b193dc 100644 --- a/components/home/HomeMainH5.vue +++ b/components/home/HomeMainH5.vue @@ -55,10 +55,6 @@ - - - {{ buyDeviceText }} - {{ scanText }} @@ -243,34 +239,36 @@ right: 20rpx; bottom: 30rpx; z-index: 1200; - padding: 14rpx; + padding: 12rpx; background: rgba(255, 255, 255, 0.96); - border-radius: 56rpx; + border-radius: 28rpx; box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08); display: flex; - align-items: center; + align-items: stretch; gap: 12rpx; } .action-btn { - height: 86rpx; - border-radius: 43rpx; + min-height: 84rpx; + border-radius: 20rpx; padding: 0 20rpx; display: flex; align-items: center; justify-content: center; gap: 8rpx; + box-sizing: border-box; } .action-btn.primary { flex: 1; background: #3EAB64; - max-width: 360rpx; + min-width: 0; } .action-btn.secondary { - width: 150rpx; + width: 180rpx; background: #f4f6f8; + border: 2rpx solid #e7eaee; } .action-icon { @@ -285,6 +283,7 @@ .action-label { font-size: 24rpx; color: #333; + text-align: center; } .primary-label { diff --git a/config/api/system.js b/config/api/system.js index 4ade900..c7bd7a7 100644 --- a/config/api/system.js +++ b/config/api/system.js @@ -1,11 +1,12 @@ import request from '../http' -// 获取系统配置(预留接口) -// 期望后端返回形如:{ code: 200, data: { expressReturnCountdownSeconds: number } } -export const getSystemConfig = () => { +// 获取系统配置 +// 可传参示例:{ configKey: 'overseas_payment_dana_total' } +export const getSystemConfig = (data = {}) => { return request({ - url: '/app/system/config', + url: '/system/config/list', method: 'get', + data, hideLoading: true }) } diff --git a/config/url.js b/config/url.js index 2dba386..106577d 100644 --- a/config/url.js +++ b/config/url.js @@ -1,8 +1,8 @@ // export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用 // export const URL = "https://manager.fdzpower.com/api" //正式国内服务器 -// export const URL = "https://ina.fdzpower.com/api" //正式国外服务器 +export const URL = "https://ina.fdzpower.com/api" //正式国外服务器 // export const URL = "https://fansdev.gxfs123.com/api" //测试服务器 -export const URL = "http://192.168.0.158:8080" //本地调试 +// export const URL = "http://192.168.0.158:8080" //本地调试 // export const URL = "http://127.0.0.1:8080" //本地调试 export const appid = "wx2165f0be356ae7a9" //微信小程序appid diff --git a/main.js b/main.js index be2748e..cdbbb31 100644 --- a/main.js +++ b/main.js @@ -7,10 +7,49 @@ import enUS from './locale/en-US.js' import idID from './locale/id-ID.js' import uView from '@climblee/uv-ui' import { initConsoleControl } from './config/console.js' +import { getSystemConfig } from './config/api/system.js' + +// #ifdef H5 +// 兼容部分依赖/构建产物在浏览器环境访问 process.env 的场景 +if (typeof globalThis !== 'undefined' && typeof globalThis.process === 'undefined') { + globalThis.process = { env: {} } +} +if (typeof globalThis !== 'undefined' && globalThis.process && !globalThis.process.env) { + globalThis.process.env = {} +} +// #endif // 初始化 console 控制 initConsoleControl() +const LANGUAGE_STORAGE_KEY = 'language' +const SUPPORTED_LANGUAGES = ['zh-CN', 'en-US', 'id-ID'] + +const LANGUAGE_ALIASES = { + zh: 'zh-CN', + 'zh-cn': 'zh-CN', + 'zh_cn': 'zh-CN', + en: 'en-US', + 'en-us': 'en-US', + 'en_us': 'en-US', + id: 'id-ID', + 'id-id': 'id-ID', + 'id_id': 'id-ID', + in: 'id-ID', + 'in-id': 'id-ID', + 'in_id': 'id-ID' +} + +const normalizeLanguage = (lang) => { + if (!lang || typeof lang !== 'string') return '' + const cleaned = lang.trim() + if (!cleaned) return '' + const lower = cleaned.toLowerCase() + if (LANGUAGE_ALIASES[lower]) return LANGUAGE_ALIASES[lower] + if (SUPPORTED_LANGUAGES.includes(cleaned)) return cleaned + return '' +} + // 检测是否为 H5 环境 const isH5Platform = () => { try { @@ -25,42 +64,69 @@ const isH5Platform = () => { // 获取系统语言 const getSystemLanguage = () => { - // H5 环境默认使用印尼语 - if (isH5Platform()) { - return 'id-ID' - } - - // 非 H5 环境根据系统语言判断 - let language = 'en-US' + let language = 'zh-CN' try { - const systemInfo = uni.getSystemInfoSync() - if (systemInfo && systemInfo.language) { - language = systemInfo.language === 'zh' || systemInfo.language.indexOf('zh') === 0 - ? 'zh-CN' - : 'en-US' + const systemInfo = uni.getSystemInfoSync() || {} + const systemLanguage = normalizeLanguage(systemInfo.language) + if (systemLanguage) { + language = systemLanguage + } else if (isH5Platform() && typeof navigator !== 'undefined') { + const browserLanguage = normalizeLanguage(navigator.language || '') + if (browserLanguage) language = browserLanguage } } catch (e) { console.error('获取系统语言失败:', e) - // 默认使用中文 language = 'zh-CN' } return language } +const extractLanguageFromConfig = (data) => { + if (!data) return '' + + if (typeof data === 'string') { + return normalizeLanguage(data) + } + + if (Array.isArray(data)) { + for (const item of data) { + const fromItem = extractLanguageFromConfig(item) + if (fromItem) return fromItem + } + return '' + } + + if (typeof data === 'object') { + const direct = normalizeLanguage( + data.language || data.lang || data.locale || data.defaultLanguage || data.defaultLang + ) + if (direct) return direct + + for (const [key, value] of Object.entries(data)) { + const keyLower = String(key).toLowerCase() + if (keyLower.includes('lang') || keyLower.includes('locale')) { + const parsed = extractLanguageFromConfig(value) + if (parsed) return parsed + } + } + } + + return '' +} + // 获取用户选择的语言 const getSavedLanguage = () => { try { - const savedLang = uni.getStorageSync('language') + const savedLang = normalizeLanguage(uni.getStorageSync(LANGUAGE_STORAGE_KEY)) if (savedLang) { return savedLang } const systemLang = getSystemLanguage() - uni.setStorageSync('language', systemLang) + uni.setStorageSync(LANGUAGE_STORAGE_KEY, systemLang) return systemLang } catch (e) { console.error('语言设置出错:', e) - // 出错时根据平台返回默认语言 - return isH5Platform() ? 'id-ID' : 'zh-CN' + return 'zh-CN' } } @@ -104,6 +170,27 @@ function getI18nInstance() { return i18nInstance } +const syncLanguageFromRemoteConfig = async (i18n) => { + if (!isH5Platform()) return + + try { + const res = await getSystemConfig() + if (!res || res.code !== 200) return + + const remoteLang = extractLanguageFromConfig(res.data) + if (!remoteLang) return + + const current = normalizeLanguage(i18n?.global?.locale || '') + if (current !== remoteLang) { + uni.setStorageSync(LANGUAGE_STORAGE_KEY, remoteLang) + i18n.global.locale = remoteLang + console.log('H5 语言已按系统配置更新为:', remoteLang) + } + } catch (e) { + console.warn('读取系统配置语言失败,使用本地语言设置:', e) + } +} + export function createApp() { const app = createSSRApp(App) @@ -116,6 +203,9 @@ export function createApp() { // 使用 i18n app.use(i18n) + + // H5 端通过系统配置同步语言(异步,不阻塞应用启动) + syncLanguageFromRemoteConfig(i18n) // 手动注入 $i18n 到全局属性(确保组件可以访问) app.config.globalProperties.$i18n = i18n.global diff --git a/pages/device/detail.vue b/pages/device/detail.vue index c5c807e..4d6927d 100644 --- a/pages/device/detail.vue +++ b/pages/device/detail.vue @@ -155,6 +155,12 @@ import { useI18n } from '@/utils/i18n.js' + import { + fetchAndCacheDanaPaymentConfig, + DANA_TOTAL_STORAGE_KEY, + DANA_SINGLE_STORAGE_KEY, + parseDanaStorageNumber + } from '@/utils/danaPaymentConfig.js' import DeviceDetailSkeleton from '@/components/DeviceDetailSkeleton.vue' const { @@ -179,11 +185,31 @@ const isWechatMiniProgram = ref(false) const isAlipayMiniProgram = ref(false) const isH5 = ref(false) - const IDR_DEPOSIT_DISPLAY = 99000 - const IDR_HOUR_PRICE_DISPLAY = 6000 + const IDR_DEPOSIT_DISPLAY = ref(99000) + const IDR_HOUR_PRICE_DISPLAY = ref(6000) + + const loadDanaPricingCache = () => { + try { + const totalCached = parseDanaStorageNumber(uni.getStorageSync(DANA_TOTAL_STORAGE_KEY)) + const singleCached = parseDanaStorageNumber(uni.getStorageSync(DANA_SINGLE_STORAGE_KEY)) + if (totalCached !== null && totalCached > 0) { + IDR_DEPOSIT_DISPLAY.value = totalCached + } + if (singleCached !== null && singleCached > 0) { + IDR_HOUR_PRICE_DISPLAY.value = singleCached + } + } catch (e) { + console.warn('读取 DANA 金额缓存失败,使用默认值:', e) + } + } + // 生命周期 onLoad 钩子 onLoad(async (options) => { + loadDanaPricingCache() + void fetchAndCacheDanaPaymentConfig() + .then(() => loadDanaPricingCache()) + .catch((e) => console.warn('DANA 配置刷新失败:', e)) // 普通链接二维码进入时,参数通常在 options.q(且为编码后的完整 URL) if (!options.deviceNo && options.q) { @@ -603,12 +629,12 @@ const displayCurrencySymbol = computed(() => (isIdrCurrency.value ? 'Rp ' : '¥')) const displayHourlyPrice = computed(() => { - if (isIdrCurrency.value) return `${IDR_HOUR_PRICE_DISPLAY}` + if (isIdrCurrency.value) return `${IDR_HOUR_PRICE_DISPLAY.value}` return deviceFeeConfig.value.maxHourPrice || '5.00' }) const displayDepositCap = computed(() => { - if (isIdrCurrency.value) return `${IDR_DEPOSIT_DISPLAY}` + if (isIdrCurrency.value) return `${IDR_DEPOSIT_DISPLAY.value}` return deviceInfo.value.depositAmount || '99' }) @@ -647,7 +673,7 @@ console.log(deviceId.value); // 调用设备租借接口 - const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value,payWay.value) + const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value,payWay) if (rentResult.code !== 200) { throw new Error(rentResult.msg || t('device.rentFailed')) } diff --git a/pages/index/index.vue b/pages/index/index.vue index fc184f4..df0edca 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -191,6 +191,7 @@ getCurrentAnnouncement, getCurrentAdvertisement } from '../../config/api/system.js' + import { fetchAndCacheDanaPaymentConfig } from '../../utils/danaPaymentConfig.js' import { getProductList } from '../../config/api/product.js' @@ -332,7 +333,6 @@ const noticeText = ref('') const bannerImages = ref([]) // 首页广告图片列表 const bannerImageList = ref([]) // 完整的广告配置列表(包含链接信息) - // 获取公告内容(支持多语言) const getNoticeText = async () => { try { @@ -577,7 +577,7 @@ navBarHeight.value = 44 } } - + // 生命周期 onMounted(() => { initNavBarHeight() @@ -619,7 +619,10 @@ // 并行加载公告和广告(不依赖定位) await Promise.all([ getNoticeText(), - getBannerImages() + getBannerImages(), + fetchAndCacheDanaPaymentConfig().catch((e) => { + console.warn('获取 DANA 配置失败,继续使用本地默认值:', e) + }) ]) // #ifdef H5 diff --git a/pages/order/detail.vue b/pages/order/detail.vue index a34012f..6bca9a9 100644 --- a/pages/order/detail.vue +++ b/pages/order/detail.vue @@ -44,7 +44,7 @@ {{ getOrderFee() }} - {{ $t('unit.yuan') }} + {{ getCurrencyUnitText() }} {{ $t('order.totalAmount') }} @@ -116,8 +116,8 @@ - - + + @@ -152,16 +152,16 @@ {{ $t('express.title') }} - + - + { + try { + const cachedTotal = parseDanaStorageNumber(uni.getStorageSync(DANA_TOTAL_STORAGE_KEY)) + const cachedSingle = parseDanaStorageNumber(uni.getStorageSync(DANA_SINGLE_STORAGE_KEY)) + if (cachedTotal !== null) { + danaPaymentTotal.value = cachedTotal + } + if (cachedSingle !== null && cachedSingle > 0) { + danaPaymentSingle.value = cachedSingle + } + } catch (e) { + console.warn('读取 DANA 金额缓存失败,使用默认值:', e) + } + } + + const resolveCurrencyCode = (orderData = {}) => { + const explicitCurrency = String( + orderData.currency || orderData.positionCurrency || orderData.position?.currency || '' + ).toUpperCase() + if (explicitCurrency) return explicitCurrency + + const payWay = String(orderData.payWay || '').toLowerCase() + const phone = String(orderData.phone || '') + const depositAmount = Number(orderData.depositAmount || 0) + const unitPrice = Number(orderData.unitPrice || 0) + + // 海外 H5 订单接口偶发不返回 currency,按业务特征兜底识别 IDR + if (payWay.includes('antom') || phone.startsWith('+62') || depositAmount >= 1000 || unitPrice >= 1000) { + return 'IDR' + } + + return 'CNY' + } /** 是否允许点击暂停:null 拉取中,true 可暂停,false 不可暂停(按钮仍展示为禁用) */ const pauseBillingEligible = ref(null) const pauseBillingLoading = ref(false) @@ -502,7 +545,15 @@ } const orderTypeText = orderTypeMap[orderInfo.value.orderType] || orderInfo.value.orderType - return `${orderInfo.value.unitPrice}${t('unit.yuan')}/${orderTypeText}` + if (currencyCode.value === 'IDR') { + return `Rp${formatAmountDisplay(danaPaymentSingle.value)}/${orderTypeText}` + } + return `${formatAmountDisplay(orderInfo.value.unitPrice)}${getCurrencyUnitText()}/${orderTypeText}` + } + + const getCurrencyUnitText = () => { + if (currencyCode.value === 'IDR') return 'Rp' + return t('unit.yuan') } // 格式化倒计时(显示为 HH:MM:SS 格式) @@ -619,23 +670,25 @@ } } + const formatAmountDisplay = (amount) => { + if (amount === null || amount === undefined || amount === '') return '0' + const normalized = String(amount).replace(/[^\d.-]/g, '') + const num = Number(normalized) + if (Number.isFinite(num)) { + return String(Math.trunc(num)) + } + return String(amount).split('.')[0] + } + // 获取使用时长标签文本 const getUsedTimeLabel = () => { // 使用中状态显示"已使用",已完成状态显示"使用时长" return orderInfo.value.orderStatus === 'in_used' ? t('order.used') : t('order.duration') } - // 获取订单费用(不含单位) + // 总金额展示:使用本地缓存 danaPaymentTotal(结构如 { type: 'number', data: 99000 }),由 loadDanaPaymentCache 同步到 danaPaymentTotal const getOrderFee = () => { - let fee; - if(orderInfo.value.originalFee){ - fee = orderInfo.value.originalFee || orderInfo.value.originalFee || '0' - }else{ - fee = orderInfo.value.currentFee; - } - - // 移除可能的"元"字符 - return String(fee).replace(/[元¥]/g, '') + return formatAmountDisplay(String(danaPaymentTotal.value)) } // 解析开始时间 @@ -969,6 +1022,7 @@ orderInfo.value.discountTypeName = orderData.discountTypeName || '' orderInfo.value.originalFee = orderData.originalFee||'' orderInfo.value.returnMapImage = orderData.returnMapImage||'' + currencyCode.value = resolveCurrencyCode(orderData) // 保存快递归还开始时间(小时为单位) orderInfo.value.expressReturnStart = orderData.expressReturnStart || null @@ -1392,6 +1446,10 @@ // 生命周期钩子 onLoad((options) => { + loadDanaPaymentCache() + void fetchAndCacheDanaPaymentConfig() + .then(() => loadDanaPaymentCache()) + .catch((e) => console.warn('DANA 配置刷新失败:', e)) console.log('订单详情页加载,参数:', JSON.stringify(options)) // 设置页面标题 @@ -1434,6 +1492,7 @@ }) onShow(() => { + loadDanaPaymentCache() isPageActive.value = true if (orderInfo.value.orderStatus === 'in_used' && orderInfo.value.isSupportExpressReturn !== 'no') { startExpressCountdown() @@ -1473,7 +1532,8 @@ min-height: 100vh; background: #f7f8fa; padding: 30rpx; - padding-bottom: 180rpx; + /* 底部操作栏为多行时,预留更大的安全滚动空间,避免遮挡详情内容 */ + padding-bottom: calc(320rpx + env(safe-area-inset-bottom)); box-sizing: border-box; // 顶部标题 @@ -1524,6 +1584,10 @@ .header-desc { font-size: 28rpx; color: #999; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; + line-height: 1.5; } .header-right { @@ -1531,6 +1595,8 @@ align-items: center; height: 100%; text-align: center; + min-width: 0; + flex-shrink: 1; } .device-no-eject-btn { @@ -1543,7 +1609,9 @@ // background: #E8F5E9; // border-radius: 12rpx; // border: 2rpx solid #07c160; - min-width: 120rpx; + min-width: 0; + max-width: 180rpx; + width: 100%; .device-no-eject-icon { width: 68rpx; @@ -1555,6 +1623,13 @@ font-size: 26rpx; color: #07c160; font-weight: 500; + display: block; + width: 100%; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; + line-height: 1.3; + text-align: center; } &:active { @@ -1591,12 +1666,15 @@ .info-left { flex: 1; + min-width: 0; display: flex; // justify-content: space-between; align-items: center; .info-col { - width: 200rpx; + width: auto; + flex: 1; + min-width: 0; // flex: 1; // text-align: center; @@ -1631,7 +1709,10 @@ display: flex; align-items: center; justify-content: flex-end; - flex-shrink: 0; + flex-shrink: 1; + min-width: 0; + max-width: 48%; + margin-left: 12rpx; .return-reminder-btn { display: flex; @@ -1641,11 +1722,25 @@ background: #3EAB64; border-radius: 50rpx; border: 2rpx solid #3EAB64; + width: 100%; + max-width: 100%; + flex-shrink: 1; + gap: 8rpx; + overflow: hidden; + box-sizing: border-box; .return-reminder-text { font-size: 24rpx; color: #fff; font-weight: 500; + display: block; + flex: 1; + min-width: 0; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; + line-height: 1.3; + text-align: center; } &:active { @@ -1662,6 +1757,9 @@ font-size: 24rpx; color: #4CAF50; line-height: 1.6; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; } .fee-rule-image { @@ -1748,6 +1846,9 @@ text-align: center; line-height: 1.6; padding-top: 20rpx; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; .rent-service-link { color: #07c160; @@ -1757,9 +1858,10 @@ .rent-item { display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; padding: 16rpx 0; border-bottom: 1rpx solid #f0f0f0; + gap: 16rpx; &:last-of-type { border-bottom: none; @@ -1768,13 +1870,22 @@ .rent-label { font-size: 28rpx; color: #999; + flex-shrink: 0; + max-width: 42%; + line-height: 1.5; } .rent-value { font-size: 28rpx; color: #333; text-align: right; - max-width: 400rpx; + flex: 1; + min-width: 0; + max-width: none; + line-height: 1.5; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; &.promotion-value { display: flex; @@ -1826,27 +1937,36 @@ left: 0; right: 0; bottom: 0; - padding: 20rpx 30rpx; - padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); + padding: 18rpx 24rpx; + padding-bottom: calc(18rpx + env(safe-area-inset-bottom)); background: #fff; - box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04); + border-top: 1rpx solid #f0f0f0; + box-shadow: 0 -6rpx 20rpx rgba(0, 0, 0, 0.05); z-index: 10; display: flex; flex-wrap: wrap; - justify-content: space-between; - align-items: center; - gap: 20rpx; + align-items: stretch; + gap: 12rpx; .bottom-bar-in-use { display: flex; flex-direction: row; flex-wrap: wrap; width: 100%; - align-items: center; - justify-content: space-between; - - // gap: 20rpx; + align-items: stretch; + gap: 12rpx; box-sizing: border-box; + + > .countdown-btn, + > .action-btn.secondary, + > .action-btn.pause-billing-btn { + flex: 1 1 0; + min-width: 0; + } + + > .bottom-icon-btn { + flex: 0 0 122rpx; + } } .bottom-icon-btn { @@ -1854,17 +1974,27 @@ flex-direction: column; align-items: center; justify-content: center; - min-width: 100rpx; + min-width: 108rpx; + min-height: 88rpx; + padding: 8rpx 8rpx; + box-sizing: border-box; + background: #f7f8fa; + border-radius: 20rpx; .icon { - width: 48rpx; - height: 48rpx; - margin-bottom: 8rpx; + width: 42rpx; + height: 42rpx; + margin-bottom: 6rpx; } text { - font-size: 24rpx; + font-size: 22rpx; color: #666; + line-height: 1.25; + text-align: center; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; } &:active { @@ -1875,26 +2005,32 @@ .countdown-btn { flex: 1 1 auto; min-width: 200rpx; - height: 88rpx; + min-height: 88rpx; display: flex; align-items: center; justify-content: center; - font-size: 28rpx; + font-size: 26rpx; color: #07c160; background: #E8F5E9; - border-radius: 44rpx; + border-radius: 20rpx; border: 2rpx solid #07c160; + padding: 0 18rpx; + box-sizing: border-box; + text-align: center; + line-height: 1.3; } .action-btn { - height: 88rpx; + min-height: 88rpx; display: flex; align-items: center; justify-content: center; - font-size: 30rpx; - border-radius: 44rpx; - padding: 0 40rpx; - white-space: nowrap; + font-size: 27rpx; + border-radius: 20rpx; + padding: 12rpx 20rpx; + white-space: normal; + text-align: center; + line-height: 1.35; &.full-width { flex: 1; @@ -1920,10 +2056,10 @@ } &.is-disabled { - opacity: 0.45; - color: #999; - background: #ebebeb; - border-color: #ddd; + opacity: 1; + color: #b8b8b8; + background: #f3f3f3; + border-color: #e6e6e6; &:active { opacity: 0.45; @@ -1945,8 +2081,12 @@ background: #fff; color: #07c160; border: 2rpx solid #07c160; - flex: 0 1 auto; - max-width: 100%; + flex: 1 1 100%; + width: 100%; + min-width: 0; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; &:active { opacity: 0.8; diff --git a/subPackages/order/payment.vue b/subPackages/order/payment.vue index 8175694..49daf5c 100644 --- a/subPackages/order/payment.vue +++ b/subPackages/order/payment.vue @@ -80,6 +80,9 @@ {{ totalAmount }} {{ $t('payment.payNow') }} + + {{ $t('order.cancelOrder') }} + @@ -96,6 +99,7 @@ } from '@dcloudio/uni-app' import { queryById, + cancelOrder, createWxPayment, getWxPaymentStatus, createAliPayment, @@ -113,6 +117,12 @@ import { useI18n } from '@/utils/i18n.js' + import { + fetchAndCacheDanaPaymentConfig, + DANA_TOTAL_STORAGE_KEY, + DANA_SINGLE_STORAGE_KEY, + parseDanaStorageNumber + } from '@/utils/danaPaymentConfig.js' const { t @@ -125,7 +135,23 @@ const passedTotalAmount = ref(null) const passedDepositAmount = ref(null) const currencyCode = ref('USD') - const IDR_DEPOSIT_DISPLAY = 99000 + const IDR_DEPOSIT_DISPLAY = ref(99000) + const IDR_SINGLE_DISPLAY = ref(6000) + + const loadDanaTotalCache = () => { + try { + const cachedTotal = parseDanaStorageNumber(uni.getStorageSync(DANA_TOTAL_STORAGE_KEY)) + const cachedSingle = parseDanaStorageNumber(uni.getStorageSync(DANA_SINGLE_STORAGE_KEY)) + if (cachedTotal !== null && cachedTotal > 0) { + IDR_DEPOSIT_DISPLAY.value = cachedTotal + } + if (cachedSingle !== null && cachedSingle > 0) { + IDR_SINGLE_DISPLAY.value = cachedSingle + } + } catch (e) { + console.warn('读取 DANA 预支付缓存失败,使用默认值:', e) + } + } // 支付方式相关(微信/支付宝/H5-Antom 多平台) const paymentMethods = ref([]) @@ -152,7 +178,7 @@ const totalAmount = computed(() => { if (currencyCode.value === 'IDR') { - return `${IDR_DEPOSIT_DISPLAY}` + return `${IDR_DEPOSIT_DISPLAY.value}` } if (passedTotalAmount.value !== null) { return parseFloat(passedTotalAmount.value).toFixed(2); @@ -258,7 +284,7 @@ orderInfo.value.deposit = deviceInfo.value.depositAmount; } if (currencyCode.value === 'IDR') { - orderInfo.value.deposit = `${IDR_DEPOSIT_DISPLAY}` + orderInfo.value.deposit = `${IDR_DEPOSIT_DISPLAY.value}` } } } catch (error) { @@ -509,6 +535,52 @@ } } + const handleCancelOrder = () => { + if (!orderId.value) { + uni.showToast({ + title: t('order.orderNotExist'), + icon: 'none' + }); + return; + } + + uni.showModal({ + title: t('order.confirmCancel'), + content: t('order.confirmCancelContent'), + success: async (res) => { + if (!res.confirm) return; + try { + uni.showLoading({ + title: t('common.processing') + }); + const result = await cancelOrder({ + orderId: orderId.value + }); + if (result && result.code === 200) { + uni.hideLoading(); + uni.showToast({ + title: t('order.cancelSuccess'), + icon: 'success' + }); + setTimeout(() => { + uni.reLaunch({ + url: '/pages/index/index' + }); + }, 800); + } else { + throw new Error(result?.msg || t('order.cancelFailed')); + } + } catch (error) { + uni.hideLoading(); + uni.showToast({ + title: error.message || t('order.cancelFailed'), + icon: 'none' + }); + } + } + }); + } + // 轮询定时器 let pollingTimer = null; @@ -641,6 +713,10 @@ } onLoad((options) => { + loadDanaTotalCache() + void fetchAndCacheDanaPaymentConfig() + .then(() => loadDanaTotalCache()) + .catch((e) => console.warn('DANA 配置刷新失败:', e)) // 设置导航栏标题为待支付 uni.setNavigationBarTitle({ title: t('payment.waitingForPayment') @@ -909,6 +985,25 @@ transform: scale(0.98); } } + + .cancel-btn { + width: 100%; + //height: 84rpx; + margin-top: 16rpx; + //border-radius: 42rpx; + //border: 2rpx solid #d9d9d9; + //background: #fff; + color: #666; + font-size: 24rpx; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + + &:active { + opacity: 0.8; + } + } } } \ No newline at end of file diff --git a/utils/danaPaymentConfig.js b/utils/danaPaymentConfig.js new file mode 100644 index 0000000..69fce79 --- /dev/null +++ b/utils/danaPaymentConfig.js @@ -0,0 +1,79 @@ +import { getSystemConfig } from '@/config/api/system.js' + +/** 系统配置项 key(与后台一致) */ +export const DANA_TOTAL_CONFIG_KEY = 'overseas_payment_dana_total' +export const DANA_SINGLE_CONFIG_KEY = 'overseas_payment_dana_single' + +/** 本地缓存 key */ +export const DANA_TOTAL_STORAGE_KEY = 'danaPaymentTotal' +export const DANA_SINGLE_STORAGE_KEY = 'danaPaymentSingle' + +/** 写入缓存的金额结构(与业务侧约定一致) */ +const wrapStorageNumber = (n) => ({ type: 'number', data: n }) + +/** + * 从本地缓存读取 DANA 金额;兼容 { type, data }、JSON 字符串、纯数字。 + */ +export function parseDanaStorageNumber(raw) { + if (raw === null || raw === undefined || raw === '') return null + let value = raw + if (typeof raw === 'string') { + const trimmed = raw.trim() + if (trimmed.startsWith('{')) { + try { + const parsed = JSON.parse(trimmed) + if (parsed && typeof parsed === 'object' && 'data' in parsed) { + value = parsed.data + } else { + value = parsed + } + } catch { + value = raw + } + } + } else if (typeof raw === 'object' && raw !== null && 'data' in raw) { + value = raw.data + } + const num = Number(value) + return Number.isFinite(num) ? num : null +} + +const parseConfigNumber = (rawValue) => { + if (rawValue === null || rawValue === undefined) return null + const cleaned = String(rawValue).replace(/[^0-9.]/g, '') + if (!cleaned) return null + const num = Number(cleaned) + return Number.isFinite(num) ? num : null +} + +const extractConfigValue = (res, configKey) => { + if (!res || res.code !== 200) return null + const row = (res.rows || []).find((item) => item && item.configKey === configKey) + if (row && row.configValue !== undefined) return row.configValue + return null +} + +/** + * 拉取 DANA 预扣款 / 单次扣款配置并写入本地缓存。 + * 用于首页及「直达」设备详情、订单详情、支付页等场景,避免仅依赖首页拉取。 + */ +export async function fetchAndCacheDanaPaymentConfig() { + const [totalRes, singleRes] = await Promise.all([ + getSystemConfig({ configKey: DANA_TOTAL_CONFIG_KEY }), + getSystemConfig({ configKey: DANA_SINGLE_CONFIG_KEY }) + ]) + + const totalRaw = extractConfigValue(totalRes, DANA_TOTAL_CONFIG_KEY) + const singleRaw = extractConfigValue(singleRes, DANA_SINGLE_CONFIG_KEY) + const totalValue = parseConfigNumber(totalRaw) + const singleValue = parseConfigNumber(singleRaw) + + if (totalValue !== null) { + uni.setStorageSync(DANA_TOTAL_STORAGE_KEY, wrapStorageNumber(totalValue)) + } + if (singleValue !== null) { + uni.setStorageSync(DANA_SINGLE_STORAGE_KEY, wrapStorageNumber(singleValue)) + } + + return { totalValue, singleValue } +}