mirror of
https://gitee.com/bootx/dax-pay-h5
synced 2026-08-09 06:26:00 +08:00
feat(h5): 接入国际化(i18n)与统一日期格式
- 引入 vue-i18n + dayjs, 新增 zh-CN/en-US 语言包 - 双端 app 注入 i18n, van-config-provider 联动 vant 语言包 - 自动探测浏览器语言, 持久化仅记显式选择 - 持久化键名加 -v2 后缀作废历史陈旧值 - 日期统一 ISO yyyy-MM-dd, 消除 MM/DD vs DD/MM 歧义 - 后端 timestamptz 转浏览器本地时区 - HTTP 提示/网络异常/操作成功 等全量改 t() - statement 状态与通道枚举改 code + i18n - 清理 axios/helper 中 moment 死代码
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.18.1",
|
||||
"dayjs": "^1.11.21",
|
||||
"lodash-es": "^4.18.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
@@ -35,6 +36,7 @@
|
||||
"qs": "^6.15.2",
|
||||
"vant": "^4.9.24",
|
||||
"vue": "^3.5.38",
|
||||
"vue-i18n": "^11.4.6",
|
||||
"vue-router": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
64
pnpm-lock.yaml
generated
64
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.18.1
|
||||
version: 1.18.1
|
||||
dayjs:
|
||||
specifier: ^1.11.21
|
||||
version: 1.11.21
|
||||
lodash-es:
|
||||
specifier: ^4.18.1
|
||||
version: 4.18.1
|
||||
@@ -32,6 +35,9 @@ importers:
|
||||
vue:
|
||||
specifier: ^3.5.38
|
||||
version: 3.5.38(typescript@5.9.3)
|
||||
vue-i18n:
|
||||
specifier: ^11.4.6
|
||||
version: 11.4.6(vue@3.5.38(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(@vue/compiler-sfc@3.5.38)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3)))(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(less@4.6.7)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3))
|
||||
@@ -669,6 +675,22 @@ packages:
|
||||
'@iconify/utils@3.1.3':
|
||||
resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==}
|
||||
|
||||
'@intlify/core-base@11.4.6':
|
||||
resolution: {integrity: sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/devtools-types@11.4.6':
|
||||
resolution: {integrity: sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/message-compiler@11.4.6':
|
||||
resolution: {integrity: sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@intlify/shared@11.4.6':
|
||||
resolution: {integrity: sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==}
|
||||
engines: {node: '>= 22'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
@@ -1509,6 +1531,9 @@ packages:
|
||||
'@vue/compiler-ssr@3.5.38':
|
||||
resolution: {integrity: sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==}
|
||||
|
||||
'@vue/devtools-api@6.6.4':
|
||||
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
|
||||
|
||||
'@vue/devtools-api@7.7.9':
|
||||
resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==}
|
||||
|
||||
@@ -1917,6 +1942,9 @@ packages:
|
||||
resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dayjs@1.11.21:
|
||||
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
|
||||
|
||||
debug@4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -3880,6 +3908,12 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
|
||||
vue-i18n@11.4.6:
|
||||
resolution: {integrity: sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==}
|
||||
engines: {node: '>= 22'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
vue-router@5.1.0:
|
||||
resolution: {integrity: sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==}
|
||||
peerDependencies:
|
||||
@@ -4476,6 +4510,24 @@ snapshots:
|
||||
'@iconify/types': 2.0.0
|
||||
import-meta-resolve: 4.2.0
|
||||
|
||||
'@intlify/core-base@11.4.6':
|
||||
dependencies:
|
||||
'@intlify/devtools-types': 11.4.6
|
||||
'@intlify/message-compiler': 11.4.6
|
||||
'@intlify/shared': 11.4.6
|
||||
|
||||
'@intlify/devtools-types@11.4.6':
|
||||
dependencies:
|
||||
'@intlify/core-base': 11.4.6
|
||||
'@intlify/shared': 11.4.6
|
||||
|
||||
'@intlify/message-compiler@11.4.6':
|
||||
dependencies:
|
||||
'@intlify/shared': 11.4.6
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@11.4.6': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -5231,6 +5283,8 @@ snapshots:
|
||||
'@vue/compiler-dom': 3.5.38
|
||||
'@vue/shared': 3.5.38
|
||||
|
||||
'@vue/devtools-api@6.6.4': {}
|
||||
|
||||
'@vue/devtools-api@7.7.9':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.9
|
||||
@@ -5646,6 +5700,8 @@ snapshots:
|
||||
|
||||
dargs@8.1.0: {}
|
||||
|
||||
dayjs@1.11.21: {}
|
||||
|
||||
debug@4.3.4:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
@@ -7855,6 +7911,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vue-i18n@11.4.6(vue@3.5.38(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 11.4.6
|
||||
'@intlify/devtools-types': 11.4.6
|
||||
'@intlify/shared': 11.4.6
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.38(typescript@5.9.3)
|
||||
|
||||
vue-router@5.1.0(@vue/compiler-sfc@3.5.38)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.38(typescript@5.9.3)))(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.6.1)(less@4.6.7)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@babel/generator': 8.0.0
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useDesignSetting } from '@/shared/hooks/setting/useDesignSetting'
|
||||
import { useLocale } from '@/shared/locales'
|
||||
import { useRouteStore } from '@/shared/store/modules/route'
|
||||
import { darken, lighten } from '@/shared/utils'
|
||||
|
||||
const routeStore = useRouteStore()
|
||||
const { getDarkMode, getAppTheme, getIsPageAnimate, getPageAnimateType } = useDesignSetting()
|
||||
// vant 语言包(跟随当前语言,驱动 vant 内置组件文案:Dialog 按钮、Picker 等)
|
||||
const { vantLocale } = useLocale()
|
||||
|
||||
// 需要缓存的路由组件
|
||||
const keepAliveComponents = computed(() => routeStore.keepAliveComponents)
|
||||
@@ -59,7 +62,7 @@ const getTransitionName = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vanConfigProvider :theme="getDarkMode" :theme-vars="getThemeVars()">
|
||||
<vanConfigProvider :theme="getDarkMode" :theme-vars="getThemeVars()" :locale="vantLocale">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<div class="absolute bottom-0 top-0 w-full overflow-hidden">
|
||||
<transition :name="getTransitionName" mode="out-in" appear>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import MobileApp from '@/mobile/App.vue'
|
||||
import router, { setupRouter } from '@/mobile/router'
|
||||
import { setupI18n } from '@/shared/locales'
|
||||
import { setupStore } from '@/shared/store'
|
||||
import { useDesignSettingWithOut } from '@/shared/store/modules/designSetting'
|
||||
import 'vant/es/toast/style'
|
||||
@@ -25,6 +26,8 @@ export async function createMobileApp() {
|
||||
const app = createApp(MobileApp)
|
||||
// 挂载状态管理
|
||||
setupStore(app)
|
||||
// 挂载国际化(含 dayjs / vant / <html lang> 初始化副作用)
|
||||
setupI18n(app)
|
||||
// 初始化全局主题:跟随系统 prefers-color-scheme(不支持手动修改)
|
||||
useDesignSettingWithOut().initSystemListener()
|
||||
// 挂载路由
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { showToast } from 'vant'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
defineOptions({ name: 'CashierPage' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
// 演示订单数据(mock,不连后端)
|
||||
const order = {
|
||||
title: 'DaxPay 演示订单',
|
||||
const order = computed(() => ({
|
||||
title: t('cashier.demoOrderTitle'),
|
||||
orderNo: route.params.orderNo as string,
|
||||
amount: 0.01,
|
||||
// 15 分钟后过期
|
||||
expiredTime: Date.now() + 15 * 60 * 1000,
|
||||
}
|
||||
}))
|
||||
|
||||
// 演示支付方式(mock)
|
||||
// 演示支付方式(mock)— name 由 i18n 按 icon 渲染
|
||||
interface PayMethod {
|
||||
id: string
|
||||
name: string
|
||||
icon: 'wechat' | 'alipay' | 'union'
|
||||
recommend?: boolean
|
||||
}
|
||||
const payMethods: PayMethod[] = [
|
||||
{ id: '1', name: '微信支付', icon: 'wechat', recommend: true },
|
||||
{ id: '2', name: '支付宝', icon: 'alipay' },
|
||||
{ id: '3', name: '银联支付', icon: 'union' },
|
||||
{ id: '1', icon: 'wechat', recommend: true },
|
||||
{ id: '2', icon: 'alipay' },
|
||||
{ id: '3', icon: 'union' },
|
||||
]
|
||||
|
||||
// 支付方式名称(跟随语言)
|
||||
function methodName(item: PayMethod) {
|
||||
return t(`cashier.method.${item.icon}`)
|
||||
}
|
||||
|
||||
// 选中的支付方式(默认推荐项)
|
||||
const selectId = ref<string>(payMethods[0]!.id)
|
||||
|
||||
@@ -44,7 +50,7 @@ const countdown = computed(() => {
|
||||
})
|
||||
|
||||
function startCountdown() {
|
||||
remainSeconds.value = Math.max(0, Math.floor((order.expiredTime - Date.now()) / 1000))
|
||||
remainSeconds.value = Math.max(0, Math.floor((order.value.expiredTime - Date.now()) / 1000))
|
||||
timer = setInterval(() => {
|
||||
if (remainSeconds.value > 0) {
|
||||
remainSeconds.value--
|
||||
@@ -60,7 +66,7 @@ function pay() {
|
||||
const selected = payMethods.find(it => it.id === selectId.value)
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: `演示:已发起 ${selected?.name ?? ''} 支付`,
|
||||
message: t('cashier.demoPayStarted', { name: selected ? methodName(selected) : '' }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,18 +87,18 @@ onUnmounted(() => {
|
||||
<span class="cashier__amount">{{ order.amount }}</span>
|
||||
</div>
|
||||
<div class="cashier__countdown">
|
||||
<span class="cashier__countdown-label">支付剩余时间</span>
|
||||
<span class="cashier__countdown-label">{{ t('cashier.remainTime') }}</span>
|
||||
<span class="cashier__countdown-time">
|
||||
{{ countdown.h }}:{{ countdown.m }}:{{ countdown.s }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cashier__detail">
|
||||
<div class="cashier__detail-row">
|
||||
<span>订单标题</span>
|
||||
<span>{{ t('cashier.orderTitle') }}</span>
|
||||
<span>{{ order.title }}</span>
|
||||
</div>
|
||||
<div class="cashier__detail-row">
|
||||
<span>订单编号</span>
|
||||
<span>{{ t('cashier.orderNo') }}</span>
|
||||
<span>{{ order.orderNo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,7 +107,7 @@ onUnmounted(() => {
|
||||
<!-- 支付方式选择 -->
|
||||
<div class="cashier__body enter-y">
|
||||
<div class="cashier__section-title">
|
||||
请选择支付方式
|
||||
{{ t('cashier.selectMethod') }}
|
||||
</div>
|
||||
<div class="cashier__list">
|
||||
<div
|
||||
@@ -125,8 +131,8 @@ onUnmounted(() => {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="cashier__pay-name">
|
||||
{{ item.name }}
|
||||
<span v-if="item.recommend" class="cashier__recommend">推荐</span>
|
||||
{{ methodName(item) }}
|
||||
<span v-if="item.recommend" class="cashier__recommend">{{ t('cashier.recommend') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cashier__radio" :class="{ 'cashier__radio--checked': item.id === selectId }" />
|
||||
@@ -137,7 +143,7 @@ onUnmounted(() => {
|
||||
<!-- 底部支付按钮 -->
|
||||
<div class="cashier__footer enter-y">
|
||||
<button class="cashier__pay-btn" @click="pay">
|
||||
立即支付 ¥{{ order.amount }}
|
||||
{{ t('cashier.payNow') }} ¥{{ order.amount }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { showNotify } from 'vant'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
defineOptions({ name: 'CodePayPage' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const { code } = route.params
|
||||
|
||||
// 演示码牌配置(mock,金额类型 random 展示自定义金额输入)
|
||||
const cashierInfo = ref({
|
||||
name: '演示收款商户',
|
||||
name: t('codePay.demoMerchant'),
|
||||
amountType: 'random' as 'random' | 'fixed',
|
||||
amount: '0',
|
||||
})
|
||||
@@ -41,12 +43,12 @@ function onDelete() {
|
||||
function pay() {
|
||||
const value = Number(amount.value)
|
||||
if (!value) {
|
||||
showNotify({ type: 'warning', message: '金额不可为0' })
|
||||
showNotify({ type: 'warning', message: t('codePay.amountZero') })
|
||||
return
|
||||
}
|
||||
showNotify({
|
||||
type: 'success',
|
||||
message: `演示:码牌 ${code} 支付 ¥${amount.value}`,
|
||||
message: t('codePay.demoPay', { code, amount: amount.value }),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -68,7 +70,7 @@ function pay() {
|
||||
{{ cashierInfo.name }}
|
||||
</div>
|
||||
<div class="code-pay__amount-label">
|
||||
付款金额
|
||||
{{ t('codePay.amountLabel') }}
|
||||
</div>
|
||||
<div class="code-pay__amount">
|
||||
<span class="code-pay__currency">¥</span>
|
||||
@@ -80,9 +82,9 @@ function pay() {
|
||||
|
||||
<!-- 备注单元格 -->
|
||||
<div class="code-pay__remark enter-y" @click="showRemark = true">
|
||||
<span class="code-pay__remark-label">备注</span>
|
||||
<span class="code-pay__remark-label">{{ t('codePay.remark') }}</span>
|
||||
<div class="code-pay__remark-value">
|
||||
<span v-if="!description" class="code-pay__remark-placeholder">添加备注</span>
|
||||
<span v-if="!description" class="code-pay__remark-placeholder">{{ t('codePay.addRemark') }}</span>
|
||||
<span v-else>{{ description }}</span>
|
||||
</div>
|
||||
<!-- 箭头图标用 SVG 替代文字 › -->
|
||||
@@ -94,10 +96,10 @@ function pay() {
|
||||
<!-- 备注弹窗 -->
|
||||
<van-dialog
|
||||
v-model:show="showRemark"
|
||||
title="添加备注"
|
||||
:title="t('codePay.addRemark')"
|
||||
show-cancel-button
|
||||
confirm-button-text="保存"
|
||||
cancel-button-text="取消"
|
||||
:confirm-button-text="t('common.save')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
confirm-button-color="#5d9dfe"
|
||||
cancel-button-color="#999"
|
||||
>
|
||||
@@ -107,7 +109,7 @@ function pay() {
|
||||
autosize
|
||||
type="textarea"
|
||||
:maxlength="50"
|
||||
placeholder="请输入支付备注内容"
|
||||
:placeholder="t('codePay.remarkPlaceholder')"
|
||||
show-word-limit
|
||||
class="code-pay__remark-field"
|
||||
/>
|
||||
@@ -118,7 +120,7 @@ function pay() {
|
||||
v-if="cashierInfo.amountType === 'random'"
|
||||
theme="custom"
|
||||
extra-key="."
|
||||
close-button-text="确认支付"
|
||||
:close-button-text="t('codePay.confirmPay')"
|
||||
:show="true"
|
||||
@close="pay"
|
||||
@input="onInput"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'MobileDeviceOnly' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
function goHome() {
|
||||
@@ -11,13 +14,13 @@ function goHome() {
|
||||
<template>
|
||||
<div class="device-only">
|
||||
<p class="device-only__title">
|
||||
此页面仅限电脑端访问
|
||||
{{ t('error.deviceOnlyPc.title') }}
|
||||
</p>
|
||||
<p class="device-only__desc">
|
||||
请在电脑浏览器中打开本页面
|
||||
{{ t('error.deviceOnlyPc.desc') }}
|
||||
</p>
|
||||
<van-button type="primary" @click="goHome">
|
||||
返回首页
|
||||
{{ t('common.backHome') }}
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'ErrorPageSon' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
function goHome() {
|
||||
router.push('/')
|
||||
@@ -14,10 +17,10 @@ function goHome() {
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h1 class="m-4 text-base">
|
||||
抱歉,你访问的页面不存在
|
||||
{{ t('error.notFound') }}
|
||||
</h1>
|
||||
<van-button type="primary" @click="goHome">
|
||||
回到首页
|
||||
{{ t('common.backHome') }}
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'HomePage' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 由 vite define 注入的项目信息
|
||||
const { pkg, lastBuildTime } = __APP_INFO__
|
||||
const version = pkg.version
|
||||
@@ -14,14 +18,14 @@ const version = pkg.version
|
||||
<img class="home__logo" src="/logo.svg" alt="DaxPay">
|
||||
<!-- 欢迎语 -->
|
||||
<div class="home__title">
|
||||
欢迎使用 DaxPay
|
||||
{{ t('home.welcome') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部项目信息 -->
|
||||
<div class="home__footer">
|
||||
<p>版本号: v{{ version }}</p>
|
||||
<p>构建时间: {{ lastBuildTime }}</p>
|
||||
<p>{{ t('home.version') }}: v{{ version }}</p>
|
||||
<p>{{ t('home.buildTime') }}: {{ lastBuildTime }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocale } from '@/shared/locales'
|
||||
|
||||
defineOptions({ name: 'PcApp' })
|
||||
|
||||
// PC 端 vant 组件(如收银台、协议页的 Dialog/NavBar)语言包,跟随当前语言
|
||||
const { vantLocale } = useLocale()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<vanConfigProvider :locale="vantLocale">
|
||||
<RouterView />
|
||||
</vanConfigProvider>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import { createApp } from 'vue'
|
||||
import PcApp from '@/pc/App.vue'
|
||||
import { pcRouter, setupPCRouter } from '@/pc/router'
|
||||
import { setupI18n } from '@/shared/locales'
|
||||
|
||||
// PC 端独立的 pinia 实例(与移动端状态隔离,同一时刻仅一套应用运行)
|
||||
const pcStore = createPinia()
|
||||
@@ -26,6 +27,8 @@ function setupPCStore(app: App) {
|
||||
export async function createPCApp() {
|
||||
const app = createApp(PcApp)
|
||||
setupPCStore(app)
|
||||
// 挂载国际化(PC 端独立实例,与移动端共用同一 i18n 模块)
|
||||
setupI18n(app)
|
||||
setupPCRouter(app)
|
||||
await pcRouter.isReady()
|
||||
return app
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'PcHome' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 由 vite define 注入的项目信息(与移动端首页一致)
|
||||
const { pkg, lastBuildTime } = __APP_INFO__
|
||||
const version = pkg.version
|
||||
@@ -14,14 +18,14 @@ const version = pkg.version
|
||||
<img class="pc-home__logo" src="/logo.svg" alt="DaxPay">
|
||||
<!-- 欢迎语 -->
|
||||
<div class="pc-home__title">
|
||||
欢迎使用 DaxPay
|
||||
{{ t('home.welcome') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部项目信息 -->
|
||||
<div class="pc-home__footer">
|
||||
<p>版本号: v{{ version }}</p>
|
||||
<p>构建时间: {{ lastBuildTime }}</p>
|
||||
<p>{{ t('home.version') }}: v{{ version }}</p>
|
||||
<p>{{ t('home.buildTime') }}: {{ lastBuildTime }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import notFoundImg from '@/shared/assets/icons/exception/404.svg'
|
||||
|
||||
defineOptions({ name: 'PcNotFound' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
function goHome() {
|
||||
@@ -14,10 +16,10 @@ function goHome() {
|
||||
<div class="pc-notfound">
|
||||
<img class="pc-notfound__img" :src="notFoundImg" alt="404">
|
||||
<h1 class="pc-notfound__text">
|
||||
抱歉,你访问的页面不存在
|
||||
{{ t('error.notFound') }}
|
||||
</h1>
|
||||
<button class="pc-notfound__btn" type="button" @click="goHome">
|
||||
返回首页
|
||||
{{ t('common.backHome') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
defineOptions({ name: 'PcCashier' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
// 演示订单数据(mock,不连后端)
|
||||
const order = {
|
||||
title: 'DaxPay 演示订单',
|
||||
const order = computed(() => ({
|
||||
title: t('cashier.demoOrderTitle'),
|
||||
orderNo: route.params.orderNo as string,
|
||||
amount: 0.01,
|
||||
expiredTime: Date.now() + 15 * 60 * 1000,
|
||||
}
|
||||
}))
|
||||
|
||||
// 演示支付方式(mock)
|
||||
// 演示支付方式(mock)— name 由 i18n 按 icon 渲染
|
||||
interface PayMethod {
|
||||
id: string
|
||||
name: string
|
||||
icon: 'wechat' | 'alipay' | 'union'
|
||||
recommend?: boolean
|
||||
}
|
||||
const payMethods: PayMethod[] = [
|
||||
{ id: '1', name: '微信支付', icon: 'wechat', recommend: true },
|
||||
{ id: '2', name: '支付宝', icon: 'alipay' },
|
||||
{ id: '3', name: '银联支付', icon: 'union' },
|
||||
{ id: '1', icon: 'wechat', recommend: true },
|
||||
{ id: '2', icon: 'alipay' },
|
||||
{ id: '3', icon: 'union' },
|
||||
]
|
||||
|
||||
// 支付方式名称(跟随语言)
|
||||
function methodName(item: PayMethod) {
|
||||
return t(`cashier.method.${item.icon}`)
|
||||
}
|
||||
|
||||
const selectId = ref<string>(payMethods[0]!.id)
|
||||
|
||||
// 倒计时(原生 setInterval,项目无 @vueuse/core)
|
||||
@@ -41,7 +47,7 @@ const countdown = computed(() => {
|
||||
})
|
||||
|
||||
function startCountdown() {
|
||||
remainSeconds.value = Math.max(0, Math.floor((order.expiredTime - Date.now()) / 1000))
|
||||
remainSeconds.value = Math.max(0, Math.floor((order.value.expiredTime - Date.now()) / 1000))
|
||||
timer = setInterval(() => {
|
||||
if (remainSeconds.value > 0) {
|
||||
remainSeconds.value--
|
||||
@@ -73,7 +79,7 @@ onUnmounted(() => {
|
||||
<!-- 订单头部 -->
|
||||
<div class="pc-cashier__header">
|
||||
<div class="pc-cashier__countdown">
|
||||
<span class="pc-cashier__countdown-label">剩余支付时间</span>
|
||||
<span class="pc-cashier__countdown-label">{{ t('cashier.remainTimeShort') }}</span>
|
||||
<span class="pc-cashier__countdown-time">
|
||||
{{ countdown.h }}:{{ countdown.m }}:{{ countdown.s }}
|
||||
</span>
|
||||
@@ -82,13 +88,13 @@ onUnmounted(() => {
|
||||
{{ order.title }}
|
||||
</div>
|
||||
<div class="pc-cashier__price">
|
||||
<span class="pc-cashier__price-label">应付金额</span>
|
||||
<span class="pc-cashier__price-label">{{ t('cashier.payableAmount') }}</span>
|
||||
<p>
|
||||
<span>¥</span>{{ order.amount }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="pc-cashier__order-no">
|
||||
订单编号: {{ order.orderNo }}
|
||||
{{ t('cashier.orderNoLabel') }}{{ order.orderNo }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,8 +121,8 @@ onUnmounted(() => {
|
||||
<path fill="#fff" d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm130.4 590.4c-39.1 27.8-86.8 43.6-138.4 43.6-130.4 0-236-105.6-236-236s105.6-236 236-236c45.6 0 88.2 13 124 35.4l-58.5 58.5c-19.4-9-40.9-14-63.5-14-85.2 0-154.4 69.2-154.4 154.4S384.8 544.3 470 544.3c52.6 0 98.9-26.4 126.4-66.6l70.6 70.6c-7.8 2.1-15.8 4-24.6 6.7z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="pc-cashier__method-name">{{ item.name }}</span>
|
||||
<span v-if="item.recommend" class="pc-cashier__recommend">推荐</span>
|
||||
<span class="pc-cashier__method-name">{{ methodName(item) }}</span>
|
||||
<span v-if="item.recommend" class="pc-cashier__recommend">{{ t('cashier.recommend') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,10 +162,10 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="pc-cashier__qrcode-tip">
|
||||
<p class="pc-cashier__qrcode-title">
|
||||
扫码支付(演示)
|
||||
{{ t('cashier.qrcodePayDemo') }}
|
||||
</p>
|
||||
<p class="pc-cashier__qrcode-sub">
|
||||
请使用 {{ payMethods.find(i => i.id === selectId)?.name }} 扫码完成支付
|
||||
{{ t('cashier.qrcodeTip', { name: methodName(payMethods.find(i => i.id === selectId)!) }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,7 +173,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 立即支付按钮(支付前) -->
|
||||
<button v-if="!showQrcode" class="pc-cashier__pay-btn" @click="pay">
|
||||
立即支付
|
||||
{{ t('cashier.payNow') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'PcDeviceOnly' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
function goHome() {
|
||||
@@ -11,13 +14,13 @@ function goHome() {
|
||||
<template>
|
||||
<div class="pc-device-only">
|
||||
<p class="pc-device-only__title">
|
||||
此页面仅限手机端访问
|
||||
{{ t('error.deviceOnlyMobile.title') }}
|
||||
</p>
|
||||
<p class="pc-device-only__desc">
|
||||
请用手机扫描二维码,或在手机浏览器中打开本页面
|
||||
{{ t('error.deviceOnlyMobile.desc') }}
|
||||
</p>
|
||||
<button class="pc-device-only__btn" type="button" @click="goHome">
|
||||
返回首页
|
||||
{{ t('common.backHome') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineOptions({ name: 'MerchantStatement' })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 支付通道与状态用 code 表示,文案由 i18n 渲染
|
||||
type ChannelCode = 'wechat' | 'alipay' | 'union'
|
||||
type StatusCode = 'success' | 'fail' | 'pending'
|
||||
|
||||
interface Trade {
|
||||
time: string
|
||||
orderNo: string
|
||||
title: string
|
||||
amount: number
|
||||
channel: '微信' | '支付宝' | '银联'
|
||||
status: '成功' | '失败' | '处理中'
|
||||
channel: ChannelCode
|
||||
status: StatusCode
|
||||
}
|
||||
|
||||
// 演示交易流水(mock)
|
||||
const trades: Trade[] = [
|
||||
{ time: '2026-06-24 10:23:11', orderNo: 'DEMO20260624102311001', title: '会员订阅', amount: 99.00, channel: '微信', status: '成功' },
|
||||
{ time: '2026-06-24 09:48:32', orderNo: 'DEMO20260624094832002', title: '商品订单', amount: 158.50, channel: '支付宝', status: '成功' },
|
||||
{ time: '2026-06-24 09:12:05', orderNo: 'DEMO20260624091205003', title: '虚拟商品', amount: 12.00, channel: '微信', status: '处理中' },
|
||||
{ time: '2026-06-23 22:01:47', orderNo: 'DEMO20260623220147004', title: '服务费', amount: 300.00, channel: '银联', status: '成功' },
|
||||
{ time: '2026-06-23 18:34:20', orderNo: 'DEMO20260623183420005', title: '商品订单', amount: 46.80, channel: '支付宝', status: '失败' },
|
||||
{ time: '2026-06-23 15:09:56', orderNo: 'DEMO20260623150956006', title: '会员订阅', amount: 99.00, channel: '微信', status: '成功' },
|
||||
{ time: '2026-06-23 11:42:13', orderNo: 'DEMO20260623114213007', title: '虚拟商品', amount: 6.00, channel: '微信', status: '成功' },
|
||||
{ time: '2026-06-23 08:55:30', orderNo: 'DEMO20260623085530008', title: '商品订单', amount: 218.00, channel: '支付宝', status: '成功' },
|
||||
{ time: '2026-06-24 10:23:11', orderNo: 'DEMO20260624102311001', title: '会员订阅', amount: 99.00, channel: 'wechat', status: 'success' },
|
||||
{ time: '2026-06-24 09:48:32', orderNo: 'DEMO20260624094832002', title: '商品订单', amount: 158.50, channel: 'alipay', status: 'success' },
|
||||
{ time: '2026-06-24 09:12:05', orderNo: 'DEMO20260624091205003', title: '虚拟商品', amount: 12.00, channel: 'wechat', status: 'pending' },
|
||||
{ time: '2026-06-23 22:01:47', orderNo: 'DEMO20260623220147004', title: '服务费', amount: 300.00, channel: 'union', status: 'success' },
|
||||
{ time: '2026-06-23 18:34:20', orderNo: 'DEMO20260623183420005', title: '商品订单', amount: 46.80, channel: 'alipay', status: 'fail' },
|
||||
{ time: '2026-06-23 15:09:56', orderNo: 'DEMO20260623150956006', title: '会员订阅', amount: 99.00, channel: 'wechat', status: 'success' },
|
||||
{ time: '2026-06-23 11:42:13', orderNo: 'DEMO20260623114213007', title: '虚拟商品', amount: 6.00, channel: 'wechat', status: 'success' },
|
||||
{ time: '2026-06-23 08:55:30', orderNo: 'DEMO20260623085530008', title: '商品订单', amount: 218.00, channel: 'alipay', status: 'success' },
|
||||
]
|
||||
|
||||
// 汇总
|
||||
const summary = computed(() => {
|
||||
const success = trades.filter(t => t.status === '成功')
|
||||
const totalAmount = success.reduce((sum, t) => sum + t.amount, 0)
|
||||
const success = trades.filter(trade => trade.status === 'success')
|
||||
const totalAmount = success.reduce((sum, trade) => sum + trade.amount, 0)
|
||||
const successRate = trades.length ? (success.length / trades.length) * 100 : 0
|
||||
return {
|
||||
totalAmount: totalAmount.toFixed(2),
|
||||
@@ -38,9 +45,19 @@ const summary = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 状态文案
|
||||
function statusText(status: StatusCode) {
|
||||
return t(`statement.status.${status}`)
|
||||
}
|
||||
|
||||
// 通道文案
|
||||
function channelText(channel: ChannelCode) {
|
||||
return t(`statement.channel.${channel}`)
|
||||
}
|
||||
|
||||
// 状态对应样式类
|
||||
function statusClass(status: Trade['status']) {
|
||||
return { 成功: 'is-success', 失败: 'is-fail', 处理中: 'is-pending' }[status]
|
||||
function statusClass(status: StatusCode) {
|
||||
return { success: 'is-success', fail: 'is-fail', pending: 'is-pending' }[status]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -48,7 +65,7 @@ function statusClass(status: Trade['status']) {
|
||||
<div class="statement">
|
||||
<h1 class="statement__heading">
|
||||
<span class="statement__heading-bar" />
|
||||
商户对账单
|
||||
{{ t('statement.heading') }}
|
||||
</h1>
|
||||
|
||||
<!-- 汇总卡片:分色 + 图标 + 左侧色条 -->
|
||||
@@ -63,7 +80,7 @@ function statusClass(status: Trade['status']) {
|
||||
<path fill="currentColor" d="M832 64 192 64C121.3 64 64 121.3 64 192l0 640c0 70.7 57.3 128 128 128l640 0c70.7 0 128-57.3 128-128L960 192C960 121.3 902.7 64 832 64zM192 128l640 0c35.4 0 64 28.6 64 64L192 192C156.6 192 128 163.4 128 128 128 92.6 156.6 128 192 128zM896 832c0 35.4-28.6 64-64 64L192 896c-35.4 0-64-28.6-64-64L128 244.2C148.4 252.6 170.6 256 192 256l704 0c35.4 0 64 28.6 64 64L960 832zM704 544c-35.4 0-64-28.6-64-64s28.6-64 64-64 64 28.6 64 64S739.4 544 704 544z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="statement__summary-label">交易总额(成功)</span>
|
||||
<span class="statement__summary-label">{{ t('statement.totalAmountSuccess') }}</span>
|
||||
</div>
|
||||
<span class="statement__summary-value statement__summary-value--primary">¥{{ summary.totalAmount }}</span>
|
||||
</div>
|
||||
@@ -78,7 +95,7 @@ function statusClass(status: Trade['status']) {
|
||||
<path fill="currentColor" d="M832 128 192 128c-35.2 0-64 28.8-64 64l0 640c0 35.2 28.8 64 64 64l640 0c35.2 0 64-28.8 64-64L896 192C896 156.8 867.2 128 832 128zM832 832 192 832 192 192l640 0L832 832zM320 416l384 0c17.6 0 32-14.4 32-32s-14.4-32-32-32L320 352c-17.6 0-32 14.4-32 32S302.4 416 320 416zM320 544l384 0c17.6 0 32-14.4 32-32s-14.4-32-32-32L320 480c-17.6 0-32 14.4-32 32S302.4 544 320 544zM320 672l256 0c17.6 0 32-14.4 32-32s-14.4-32-32-32L320 608c-17.6 0-32 14.4-32 32S302.4 672 320 672z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="statement__summary-label">交易笔数</span>
|
||||
<span class="statement__summary-label">{{ t('statement.tradeCount') }}</span>
|
||||
</div>
|
||||
<span class="statement__summary-value">{{ summary.count }}</span>
|
||||
</div>
|
||||
@@ -93,7 +110,7 @@ function statusClass(status: Trade['status']) {
|
||||
<path fill="currentColor" d="M832 192 640 192c-17.6 0-32 14.4-32 32s14.4 32 32 32l115.2 0L512 499.2 379.2 366.4c-12.8-12.8-32-12.8-44.8 0L160 540.8c-12.8 12.8-12.8 32 0 44.8 6.4 6.4 14.4 9.6 22.4 9.6s16-3.2 22.4-9.6l152-152 132.8 132.8c12.8 12.8 32 12.8 44.8 0L832 268.8 832 384c0 17.6 14.4 32 32 32s32-14.4 32-32L896 224C896 206.4 881.6 192 864 192L832 192zM864 768 160 768c-17.6 0-32 14.4-32 32s14.4 32 32 32l704 0c17.6 0 32-14.4 32-32S881.6 768 864 768z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="statement__summary-label">支付成功率</span>
|
||||
<span class="statement__summary-label">{{ t('statement.successRate') }}</span>
|
||||
</div>
|
||||
<span class="statement__summary-value" :class="summary.rateHealthy ? 'statement__summary-value--success' : 'statement__summary-value--warning'">{{ summary.successRate }}%</span>
|
||||
</div>
|
||||
@@ -104,31 +121,31 @@ function statusClass(status: Trade['status']) {
|
||||
<table class="statement__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>交易时间</th>
|
||||
<th>订单号</th>
|
||||
<th>商品标题</th>
|
||||
<th>{{ t('statement.column.time') }}</th>
|
||||
<th>{{ t('statement.column.orderNo') }}</th>
|
||||
<th>{{ t('statement.column.title') }}</th>
|
||||
<th class="statement__th-amount">
|
||||
金额
|
||||
{{ t('statement.column.amount') }}
|
||||
</th>
|
||||
<th>支付通道</th>
|
||||
<th>状态</th>
|
||||
<th>{{ t('statement.column.channel') }}</th>
|
||||
<th>{{ t('statement.column.status') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="t in trades" :key="t.orderNo">
|
||||
<td>{{ t.time }}</td>
|
||||
<tr v-for="trade in trades" :key="trade.orderNo">
|
||||
<td>{{ trade.time }}</td>
|
||||
<td class="statement__mono">
|
||||
{{ t.orderNo }}
|
||||
{{ trade.orderNo }}
|
||||
</td>
|
||||
<td>{{ t.title }}</td>
|
||||
<td>{{ trade.title }}</td>
|
||||
<td class="statement__amount">
|
||||
¥{{ t.amount.toFixed(2) }}
|
||||
¥{{ trade.amount.toFixed(2) }}
|
||||
</td>
|
||||
<td>{{ t.channel }}</td>
|
||||
<td>{{ channelText(trade.channel) }}</td>
|
||||
<td>
|
||||
<span class="statement__status" :class="statusClass(t.status)">
|
||||
<span class="statement__status" :class="statusClass(trade.status)">
|
||||
<i class="statement__status-dot" />
|
||||
{{ t.status }}
|
||||
{{ statusText(trade.status) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -138,7 +155,7 @@ function statusClass(status: Trade['status']) {
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="statement__empty">
|
||||
<span class="statement__empty-text">暂无交易记录</span>
|
||||
<span class="statement__empty-text">{{ t('statement.empty') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
20
src/shared/locales/en-US/cashier.json
Normal file
20
src/shared/locales/en-US/cashier.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"demoOrderTitle": "DaxPay Demo Order",
|
||||
"remainTime": "Payment time left",
|
||||
"remainTimeShort": "Time left",
|
||||
"orderTitle": "Order title",
|
||||
"orderNo": "Order No.",
|
||||
"orderNoLabel": "Order No.: ",
|
||||
"payableAmount": "Amount payable",
|
||||
"selectMethod": "Select a payment method",
|
||||
"recommend": "Recommended",
|
||||
"payNow": "Pay Now",
|
||||
"demoPayStarted": "Demo: {name} payment initiated",
|
||||
"qrcodePayDemo": "Scan to pay (demo)",
|
||||
"qrcodeTip": "Scan the QR code with {name} to complete the payment",
|
||||
"method": {
|
||||
"wechat": "WeChat Pay",
|
||||
"alipay": "Alipay",
|
||||
"union": "UnionPay"
|
||||
}
|
||||
}
|
||||
10
src/shared/locales/en-US/codePay.json
Normal file
10
src/shared/locales/en-US/codePay.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"demoMerchant": "Demo Merchant",
|
||||
"amountLabel": "Amount",
|
||||
"remark": "Remark",
|
||||
"addRemark": "Add remark",
|
||||
"remarkPlaceholder": "Enter payment remark",
|
||||
"amountZero": "Amount cannot be 0",
|
||||
"confirmPay": "Confirm Payment",
|
||||
"demoPay": "Demo: code {code} payment ¥{amount}"
|
||||
}
|
||||
11
src/shared/locales/en-US/common.json
Normal file
11
src/shared/locales/en-US/common.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"back": "Back",
|
||||
"backHome": "Back to Home",
|
||||
"loading": "Loading...",
|
||||
"tip": "Notice",
|
||||
"operationSuccess": "Operation succeeded!",
|
||||
"operationFailed": "Operation failed!"
|
||||
}
|
||||
11
src/shared/locales/en-US/error.json
Normal file
11
src/shared/locales/en-US/error.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"notFound": "Sorry, the page you visited does not exist",
|
||||
"deviceOnlyPc": {
|
||||
"title": "This page is only available on desktop",
|
||||
"desc": "Please open this page in a desktop browser"
|
||||
},
|
||||
"deviceOnlyMobile": {
|
||||
"title": "This page is only available on mobile",
|
||||
"desc": "Please scan the QR code or open this page in a mobile browser"
|
||||
}
|
||||
}
|
||||
5
src/shared/locales/en-US/home.json
Normal file
5
src/shared/locales/en-US/home.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"welcome": "Welcome to DaxPay",
|
||||
"version": "Version",
|
||||
"buildTime": "Build time"
|
||||
}
|
||||
19
src/shared/locales/en-US/http.json
Normal file
19
src/shared/locales/en-US/http.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"requestError": "Request failed, please try again later",
|
||||
"timeout": "Request timed out, please refresh the page and retry!",
|
||||
"networkError": "Network error",
|
||||
"networkErrorDesc": "Please check your network connection",
|
||||
"status": {
|
||||
"401": "Unauthorized (invalid token, username or password)!",
|
||||
"403": "Authorized, but access is forbidden!",
|
||||
"404": "Network request error, resource not found!",
|
||||
"405": "Network request error, method not allowed!",
|
||||
"408": "Network request timed out",
|
||||
"500": "Server error, please contact the administrator!",
|
||||
"501": "Network not implemented",
|
||||
"502": "Network error",
|
||||
"503": "Service unavailable, server temporarily overloaded or under maintenance!",
|
||||
"504": "Network timeout",
|
||||
"505": "HTTP version not supported!"
|
||||
}
|
||||
}
|
||||
19
src/shared/locales/en-US/index.ts
Normal file
19
src/shared/locales/en-US/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import cashier from './cashier.json'
|
||||
import codePay from './codePay.json'
|
||||
import common from './common.json'
|
||||
import error from './error.json'
|
||||
import home from './home.json'
|
||||
import http from './http.json'
|
||||
import protocol from './protocol.json'
|
||||
import statement from './statement.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
http,
|
||||
cashier,
|
||||
codePay,
|
||||
protocol,
|
||||
statement,
|
||||
home,
|
||||
error,
|
||||
}
|
||||
8
src/shared/locales/en-US/protocol.json
Normal file
8
src/shared/locales/en-US/protocol.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"title": "Agreement",
|
||||
"typeNotExist": "Agreement type does not exist",
|
||||
"loadFailed": "Failed to load the agreement",
|
||||
"noContent": "No agreement content",
|
||||
"version": "Version {version}",
|
||||
"effectiveTime": "Effective from {time}"
|
||||
}
|
||||
25
src/shared/locales/en-US/statement.json
Normal file
25
src/shared/locales/en-US/statement.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"heading": "Merchant Statement",
|
||||
"totalAmountSuccess": "Total Amount (Success)",
|
||||
"tradeCount": "Transactions",
|
||||
"successRate": "Success Rate",
|
||||
"empty": "No transactions",
|
||||
"column": {
|
||||
"time": "Time",
|
||||
"orderNo": "Order No.",
|
||||
"title": "Item",
|
||||
"amount": "Amount",
|
||||
"channel": "Channel",
|
||||
"status": "Status"
|
||||
},
|
||||
"status": {
|
||||
"success": "Success",
|
||||
"fail": "Failed",
|
||||
"pending": "Processing"
|
||||
},
|
||||
"channel": {
|
||||
"wechat": "WeChat",
|
||||
"alipay": "Alipay",
|
||||
"union": "UnionPay"
|
||||
}
|
||||
}
|
||||
124
src/shared/locales/index.ts
Normal file
124
src/shared/locales/index.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 国际化核心装配
|
||||
*
|
||||
* - locale 状态以 vue-i18n 实例为单一事实源(composition 模式)
|
||||
* - 语言切换副作用:dayjs locale / vant 语言包 / `<html lang>` / localStorage 持久化
|
||||
* - mobile 与 pc 是两个独立 Vue 实例(各自独立 pinia),i18n 实例由两端 app.ts 分别 app.use(i18n)
|
||||
*/
|
||||
import type { App } from 'vue'
|
||||
import type { AppLocaleCode, AppLocaleOption } from './lang'
|
||||
import dayjs from 'dayjs'
|
||||
import { ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import enUS from './en-US'
|
||||
import {
|
||||
APP_LOCALES,
|
||||
DEFAULT_LOCALE,
|
||||
getLocaleOption,
|
||||
LOCALE_STORAGE_KEY,
|
||||
resolveInitialLocale,
|
||||
} from './lang'
|
||||
import zhCN from './zh-CN'
|
||||
|
||||
export type { AppLocaleCode, AppLocaleOption }
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: resolveInitialLocale(),
|
||||
fallbackLocale: DEFAULT_LOCALE,
|
||||
messages: {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
},
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
})
|
||||
|
||||
/** vant 语言包响应式引用,供 `van-config-provider` 的 `:locale` 绑定 */
|
||||
const vantLocalePkg = ref<Record<string, any> | null>(null)
|
||||
|
||||
/** 动态加载 vant 语言包 */
|
||||
async function loadVantLocale(code: AppLocaleCode) {
|
||||
const opt = getLocaleOption(code)
|
||||
if (!opt) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const mod = await opt.vantLoader()
|
||||
vantLocalePkg.value = mod.default ?? mod
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('[i18n] failed to load vant locale:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用语言变更的运行时副作用:dayjs / vant / `<html lang>`
|
||||
*
|
||||
* 注意:此处**不**写 localStorage。自动探测/初始化若落盘,
|
||||
* 会让陈旧值锁定语言、使后续浏览器语言变化失效。
|
||||
* 仅显式切换(`setLocale`)才持久化。
|
||||
*/
|
||||
function applySideEffects(code: AppLocaleCode) {
|
||||
const opt: AppLocaleOption = getLocaleOption(code) ?? APP_LOCALES[0]!
|
||||
// dayjs 全局 locale(影响 fromNow / localizedFormat 等本地化输出)
|
||||
dayjs.locale(opt.dayjsLocale)
|
||||
// 同步 <html lang> 属性
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.lang = opt.htmlLang
|
||||
}
|
||||
// 异步加载 vant 语言包
|
||||
void loadVantLocale(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换当前语言(显式选择,持久化到 localStorage)
|
||||
*/
|
||||
export function setLocale(code: AppLocaleCode) {
|
||||
if (!getLocaleOption(code)) {
|
||||
return
|
||||
}
|
||||
i18n.global.locale.value = code
|
||||
applySideEffects(code)
|
||||
// 仅显式切换才落盘,覆盖浏览器自动探测
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(LOCALE_STORAGE_KEY, code)
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前语言代码(非响应式读取,供非组件模块使用) */
|
||||
export function getCurrentLocale(): AppLocaleCode {
|
||||
return i18n.global.locale.value as AppLocaleCode
|
||||
}
|
||||
|
||||
/**
|
||||
* 供非组件模块使用的翻译函数(如 axios 拦截器、http checkStatus)
|
||||
*/
|
||||
export function t(key: string, named?: Record<string, unknown>): string {
|
||||
return i18n.global.t(key, named ?? ({} as Record<string, unknown>))
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件内语言组合式 API
|
||||
* @returns locale 当前语言(响应式 ref)、setLocale 切换函数、t 翻译函数、vantLocale vant 语言包 ref
|
||||
*/
|
||||
export function useLocale() {
|
||||
return {
|
||||
locale: i18n.global.locale,
|
||||
setLocale,
|
||||
t: i18n.global.t,
|
||||
vantLocale: vantLocalePkg,
|
||||
locales: APP_LOCALES,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装 i18n 到应用,并执行初始语言的副作用
|
||||
*/
|
||||
export function setupI18n(app: App) {
|
||||
app.use(i18n)
|
||||
applySideEffects(i18n.global.locale.value as AppLocaleCode)
|
||||
return i18n
|
||||
}
|
||||
|
||||
export default i18n
|
||||
94
src/shared/locales/lang.ts
Normal file
94
src/shared/locales/lang.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 语言选项与浏览器探测
|
||||
*
|
||||
* 语言代码与后端 common-i18n 对齐:仅支持 zh-CN / en-US。
|
||||
*/
|
||||
|
||||
/** 应用支持的语言代码(与后端 common-i18n 一致) */
|
||||
export type AppLocaleCode = 'zh-CN' | 'en-US'
|
||||
|
||||
export interface AppLocaleOption {
|
||||
/** 语言代码 */
|
||||
code: AppLocaleCode
|
||||
/** `<html lang>` 属性值 */
|
||||
htmlLang: string
|
||||
/** dayjs locale 名称 */
|
||||
dayjsLocale: string
|
||||
/** vant 语言包动态导入函数 */
|
||||
vantLoader: () => Promise<{ default: Record<string, any> }>
|
||||
}
|
||||
|
||||
/** 全部支持的语言选项 */
|
||||
export const APP_LOCALES: AppLocaleOption[] = [
|
||||
{
|
||||
code: 'zh-CN',
|
||||
htmlLang: 'zh-cmn-Hans',
|
||||
dayjsLocale: 'zh-cn',
|
||||
vantLoader: () => import('vant/es/locale/lang/zh-CN.mjs'),
|
||||
},
|
||||
{
|
||||
code: 'en-US',
|
||||
htmlLang: 'en',
|
||||
dayjsLocale: 'en',
|
||||
vantLoader: () => import('vant/es/locale/lang/en-US.mjs'),
|
||||
},
|
||||
]
|
||||
|
||||
/** 默认语言 */
|
||||
export const DEFAULT_LOCALE: AppLocaleCode = 'zh-CN'
|
||||
|
||||
/**
|
||||
* localStorage 持久化键
|
||||
*
|
||||
* 带 `-v2` 后缀:旧版本会在自动探测时落盘,导致陈旧值锁定语言、
|
||||
* 切换浏览器语言不再生效。新版仅由显式 `setLocale` 写入,
|
||||
* 加版本后缀可让历史陈旧的 `APP-LOCALE` 值自动作废,无需手动清理。
|
||||
*/
|
||||
export const LOCALE_STORAGE_KEY = 'APP-LOCALE-v2'
|
||||
|
||||
const LOCALE_MAP = new Map<string, AppLocaleOption>(APP_LOCALES.map(l => [l.code, l]))
|
||||
|
||||
/** 根据语言代码获取选项 */
|
||||
export function getLocaleOption(code: string): AppLocaleOption | undefined {
|
||||
return LOCALE_MAP.get(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从浏览器 navigator.language 匹配已支持的语言
|
||||
* - 任意中文变体(zh / zh-CN / zh-TW / zh-HK)→ zh-CN
|
||||
* - 任意英文变体(en / en-US / en-GB)→ en-US
|
||||
* - 其余回退到默认语言
|
||||
*/
|
||||
export function matchBrowserLocale(): AppLocaleCode {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
const langs = navigator.languages?.length ? navigator.languages : [navigator.language]
|
||||
for (const lang of langs) {
|
||||
if (!lang) {
|
||||
continue
|
||||
}
|
||||
const lower = lang.toLowerCase()
|
||||
if (lower.startsWith('zh')) {
|
||||
return 'zh-CN'
|
||||
}
|
||||
if (lower.startsWith('en')) {
|
||||
return 'en-US'
|
||||
}
|
||||
}
|
||||
return DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析初始语言,优先级:localStorage > 浏览器语言 > 默认
|
||||
*/
|
||||
export function resolveInitialLocale(): AppLocaleCode {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return matchBrowserLocale()
|
||||
}
|
||||
const saved = localStorage.getItem(LOCALE_STORAGE_KEY)
|
||||
if (saved && getLocaleOption(saved)) {
|
||||
return saved as AppLocaleCode
|
||||
}
|
||||
return matchBrowserLocale()
|
||||
}
|
||||
20
src/shared/locales/zh-CN/cashier.json
Normal file
20
src/shared/locales/zh-CN/cashier.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"demoOrderTitle": "DaxPay 演示订单",
|
||||
"remainTime": "支付剩余时间",
|
||||
"remainTimeShort": "剩余支付时间",
|
||||
"orderTitle": "订单标题",
|
||||
"orderNo": "订单编号",
|
||||
"orderNoLabel": "订单编号:",
|
||||
"payableAmount": "应付金额",
|
||||
"selectMethod": "请选择支付方式",
|
||||
"recommend": "推荐",
|
||||
"payNow": "立即支付",
|
||||
"demoPayStarted": "演示:已发起 {name} 支付",
|
||||
"qrcodePayDemo": "扫码支付(演示)",
|
||||
"qrcodeTip": "请使用 {name} 扫码完成支付",
|
||||
"method": {
|
||||
"wechat": "微信支付",
|
||||
"alipay": "支付宝",
|
||||
"union": "银联支付"
|
||||
}
|
||||
}
|
||||
10
src/shared/locales/zh-CN/codePay.json
Normal file
10
src/shared/locales/zh-CN/codePay.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"demoMerchant": "演示收款商户",
|
||||
"amountLabel": "付款金额",
|
||||
"remark": "备注",
|
||||
"addRemark": "添加备注",
|
||||
"remarkPlaceholder": "请输入支付备注内容",
|
||||
"amountZero": "金额不可为0",
|
||||
"confirmPay": "确认支付",
|
||||
"demoPay": "演示:码牌 {code} 支付 ¥{amount}"
|
||||
}
|
||||
11
src/shared/locales/zh-CN/common.json
Normal file
11
src/shared/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"back": "返回",
|
||||
"backHome": "返回首页",
|
||||
"loading": "加载中...",
|
||||
"tip": "提示",
|
||||
"operationSuccess": "操作成功!",
|
||||
"operationFailed": "操作失败!"
|
||||
}
|
||||
11
src/shared/locales/zh-CN/error.json
Normal file
11
src/shared/locales/zh-CN/error.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"notFound": "抱歉,你访问的页面不存在",
|
||||
"deviceOnlyPc": {
|
||||
"title": "此页面仅限电脑端访问",
|
||||
"desc": "请在电脑浏览器中打开本页面"
|
||||
},
|
||||
"deviceOnlyMobile": {
|
||||
"title": "此页面仅限手机端访问",
|
||||
"desc": "请用手机扫描二维码,或在手机浏览器中打开本页面"
|
||||
}
|
||||
}
|
||||
5
src/shared/locales/zh-CN/home.json
Normal file
5
src/shared/locales/zh-CN/home.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"welcome": "欢迎使用 DaxPay",
|
||||
"version": "版本号",
|
||||
"buildTime": "构建时间"
|
||||
}
|
||||
19
src/shared/locales/zh-CN/http.json
Normal file
19
src/shared/locales/zh-CN/http.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"requestError": "请求出错,请稍候重试",
|
||||
"timeout": "接口请求超时,请刷新页面重试!",
|
||||
"networkError": "网络异常",
|
||||
"networkErrorDesc": "请检查您的网络连接是否正常",
|
||||
"status": {
|
||||
"401": "用户没有权限(令牌、用户名、密码错误)!",
|
||||
"403": "用户得到授权,但是访问是被禁止的!",
|
||||
"404": "网络请求错误,未找到该资源!",
|
||||
"405": "网络请求错误,请求方法未允许!",
|
||||
"408": "网络请求超时",
|
||||
"500": "服务器错误,请联系管理员!",
|
||||
"501": "网络未实现",
|
||||
"502": "网络错误",
|
||||
"503": "服务不可用,服务器暂时过载或维护!",
|
||||
"504": "网络超时",
|
||||
"505": "http版本不支持该请求!"
|
||||
}
|
||||
}
|
||||
19
src/shared/locales/zh-CN/index.ts
Normal file
19
src/shared/locales/zh-CN/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import cashier from './cashier.json'
|
||||
import codePay from './codePay.json'
|
||||
import common from './common.json'
|
||||
import error from './error.json'
|
||||
import home from './home.json'
|
||||
import http from './http.json'
|
||||
import protocol from './protocol.json'
|
||||
import statement from './statement.json'
|
||||
|
||||
export default {
|
||||
common,
|
||||
http,
|
||||
cashier,
|
||||
codePay,
|
||||
protocol,
|
||||
statement,
|
||||
home,
|
||||
error,
|
||||
}
|
||||
8
src/shared/locales/zh-CN/protocol.json
Normal file
8
src/shared/locales/zh-CN/protocol.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"title": "协议",
|
||||
"typeNotExist": "协议类型不存在",
|
||||
"loadFailed": "协议加载失败",
|
||||
"noContent": "暂无协议内容",
|
||||
"version": "版本 {version}",
|
||||
"effectiveTime": "生效时间 {time}"
|
||||
}
|
||||
25
src/shared/locales/zh-CN/statement.json
Normal file
25
src/shared/locales/zh-CN/statement.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"heading": "商户对账单",
|
||||
"totalAmountSuccess": "交易总额(成功)",
|
||||
"tradeCount": "交易笔数",
|
||||
"successRate": "支付成功率",
|
||||
"empty": "暂无交易记录",
|
||||
"column": {
|
||||
"time": "交易时间",
|
||||
"orderNo": "订单号",
|
||||
"title": "商品标题",
|
||||
"amount": "金额",
|
||||
"channel": "支付通道",
|
||||
"status": "状态"
|
||||
},
|
||||
"status": {
|
||||
"success": "成功",
|
||||
"fail": "失败",
|
||||
"pending": "处理中"
|
||||
},
|
||||
"channel": {
|
||||
"wechat": "微信",
|
||||
"alipay": "支付宝",
|
||||
"union": "银联"
|
||||
}
|
||||
}
|
||||
68
src/shared/utils/datetime.ts
Normal file
68
src/shared/utils/datetime.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 日期时间格式化工具(基于 dayjs)
|
||||
*
|
||||
* - 后端时间字段统一为 `timestamptz(6)`,返回带偏移的 ISO 8601,此处 `.local()` 转为浏览器本地时区显示
|
||||
* - 日期/日期时间统一使用 ISO 8601 年-月-日顺序(`YYYY-MM-DD`),消除各国 MM/DD vs DD/MM 歧义
|
||||
* - dayjs 全局 locale 由 `setupI18n` 的副作用函数随语言切换同步(仅影响相对时间 fromNow 的本地化文本)
|
||||
*/
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import 'dayjs/locale/en'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(localizedFormat)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
/**
|
||||
* 统一日期格式
|
||||
*
|
||||
* 采用 ISO 8601 年-月-日(`YYYY-MM-DD`),不随语言变化,
|
||||
* 以消除 US 的 `MM/DD/YYYY` 与欧洲的 `DD/MM/YYYY` 之间的歧义。
|
||||
*/
|
||||
const DATE_FORMAT = 'YYYY-MM-DD'
|
||||
|
||||
/** 统一日期时间格式 */
|
||||
const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm'
|
||||
|
||||
/**
|
||||
* 格式化为日期(`YYYY-MM-DD`,统一格式不随语言变化)
|
||||
* @param iso ISO 8601 字符串(可带时区偏移),转浏览器本地时区显示
|
||||
*/
|
||||
export function formatDate(iso?: string | null): string {
|
||||
if (!iso) {
|
||||
return ''
|
||||
}
|
||||
const d = dayjs(iso).local()
|
||||
return d.isValid() ? d.format(DATE_FORMAT) : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为日期时间(`YYYY-MM-DD HH:mm`,统一格式不随语言变化)
|
||||
* @param iso ISO 8601 字符串(可带时区偏移),转浏览器本地时区显示
|
||||
*/
|
||||
export function formatDateTime(iso?: string | null): string {
|
||||
if (!iso) {
|
||||
return ''
|
||||
}
|
||||
const d = dayjs(iso).local()
|
||||
return d.isValid() ? d.format(DATETIME_FORMAT) : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对时间(如"3分钟前"/"3 minutes ago"),跟随 dayjs 全局 locale 本地化
|
||||
* @param iso ISO 8601 字符串(可带时区偏移),转浏览器本地时区显示
|
||||
*/
|
||||
export function formatRelativeTime(iso?: string | null): string {
|
||||
if (!iso) {
|
||||
return ''
|
||||
}
|
||||
const d = dayjs(iso).local()
|
||||
return d.isValid() ? d.fromNow() : ''
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { showFailToast } from 'vant'
|
||||
import { t } from '@/shared/locales'
|
||||
|
||||
/**
|
||||
* HTTP 状态码错误提示
|
||||
*
|
||||
* - 400 及未知状态:提示后端返回的 `msg`(动态内容,不翻译)
|
||||
* - 401/403/404/405/408/500/501/502/503/504/505:固定文案,走 i18n(http.status.{code})
|
||||
*/
|
||||
export function checkStatus(status: number, msg: string): void {
|
||||
switch (status) {
|
||||
case 400:
|
||||
@@ -9,38 +16,38 @@ export function checkStatus(status: number, msg: string): void {
|
||||
// 未登录则跳转登录页面,并携带当前页面的路径
|
||||
// 在登录成功后返回当前页面,这一步需要在登录页操作。
|
||||
case 401:
|
||||
showFailToast('用户没有权限(令牌、用户名、密码错误)!')
|
||||
showFailToast(t(`http.status.401`))
|
||||
break
|
||||
case 403:
|
||||
showFailToast('用户得到授权,但是访问是被禁止的。!')
|
||||
showFailToast(t('http.status.403'))
|
||||
break
|
||||
// 404请求不存在
|
||||
case 404:
|
||||
showFailToast('网络请求错误,未找到该资源!')
|
||||
showFailToast(t('http.status.404'))
|
||||
break
|
||||
case 405:
|
||||
showFailToast('网络请求错误,请求方法未允许!')
|
||||
showFailToast(t('http.status.405'))
|
||||
break
|
||||
case 408:
|
||||
showFailToast('网络请求超时')
|
||||
showFailToast(t('http.status.408'))
|
||||
break
|
||||
case 500:
|
||||
showFailToast('服务器错误,请联系管理员!')
|
||||
showFailToast(t('http.status.500'))
|
||||
break
|
||||
case 501:
|
||||
showFailToast('网络未实现')
|
||||
showFailToast(t('http.status.501'))
|
||||
break
|
||||
case 502:
|
||||
showFailToast('网络错误')
|
||||
showFailToast(t('http.status.502'))
|
||||
break
|
||||
case 503:
|
||||
showFailToast('服务不可用,服务器暂时过载或维护!')
|
||||
showFailToast(t('http.status.503'))
|
||||
break
|
||||
case 504:
|
||||
showFailToast('网络超时')
|
||||
showFailToast(t('http.status.504'))
|
||||
break
|
||||
case 505:
|
||||
showFailToast('http版本不支持该请求!')
|
||||
showFailToast(t('http.status.505'))
|
||||
break
|
||||
default:
|
||||
showFailToast(msg)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { isObject, isString } from '@/shared/utils/is'
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'
|
||||
|
||||
export function joinTimestamp<T extends boolean>(
|
||||
join: boolean,
|
||||
restful: T,
|
||||
@@ -19,7 +17,7 @@ export function joinTimestamp(join: boolean, restful = false): string | object {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Format request parameter time
|
||||
* @description: 规整请求参数(去除字符串首尾空白)
|
||||
*/
|
||||
export function formatRequestDate(params: Recordable) {
|
||||
if (Object.prototype.toString.call(params) !== '[object Object]') {
|
||||
@@ -27,9 +25,6 @@ export function formatRequestDate(params: Recordable) {
|
||||
}
|
||||
|
||||
for (const key in params) {
|
||||
if (params[key] && params[key]._isAMomentObject) {
|
||||
params[key] = params[key].format(DATE_TIME_FORMAT)
|
||||
}
|
||||
if (isString(key)) {
|
||||
const value = params[key]
|
||||
if (value) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import axios from 'axios'
|
||||
import { showDialog, showFailToast } from 'vant'
|
||||
import { ContentTypeEnum, RequestEnum, ResultEnum } from '@/shared/enums/httpEnum'
|
||||
import { useGlobSetting } from '@/shared/hooks/setting'
|
||||
import { t } from '@/shared/locales'
|
||||
import { deepMerge, isUrl } from '@/shared/utils'
|
||||
import { isString } from '@/shared/utils/is/'
|
||||
import { setObjToUrlParams } from '@/shared/utils/urlUtils'
|
||||
@@ -50,29 +51,29 @@ const transform: AxiosTransform = {
|
||||
|
||||
if (!data) {
|
||||
// return '[HTTP] Request has no return value';
|
||||
throw new Error('请求出错,请稍候重试')
|
||||
throw new Error(t('http.requestError'))
|
||||
}
|
||||
// 这里 code,result,message为 后台统一的字段,需要修改为项目自己的接口返回格式
|
||||
const { code, result, message } = data
|
||||
// 后台统一返回格式:{ code, message, data }
|
||||
const { code, data: resultData, message } = data
|
||||
// 请求成功
|
||||
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS
|
||||
// 是否显示提示信息
|
||||
if (isShowMessage) {
|
||||
if (hasSuccess && (successMessageText || isShowSuccessMessage)) {
|
||||
showDialog({
|
||||
message: successMessageText || message || '操作成功!',
|
||||
message: successMessageText || message || t('common.operationSuccess'),
|
||||
}).then(() => {
|
||||
// on close
|
||||
})
|
||||
}
|
||||
else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) {
|
||||
// 是否显示自定义信息提示
|
||||
showFailToast(message || errorMessageText || '操作失败!')
|
||||
showFailToast(message || errorMessageText || t('common.operationFailed'))
|
||||
}
|
||||
else if (!hasSuccess && options.errorMessageMode === 'modal') {
|
||||
// errorMessageMode=‘custom-modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
||||
showDialog({
|
||||
title: '提示',
|
||||
title: t('common.tip'),
|
||||
message,
|
||||
}).then(() => {
|
||||
// on close
|
||||
@@ -82,7 +83,7 @@ const transform: AxiosTransform = {
|
||||
|
||||
// 接口请求成功,直接返回结果
|
||||
if (code === ResultEnum.SUCCESS) {
|
||||
return result
|
||||
return resultData
|
||||
}
|
||||
// 接口请求错误,统一提示错误信息 这里逻辑可以根据项目进行修改
|
||||
const errorMsg = message
|
||||
@@ -173,13 +174,13 @@ const transform: AxiosTransform = {
|
||||
const err: string = error.toString()
|
||||
try {
|
||||
if (code === 'ECONNABORTED' && message.includes('timeout')) {
|
||||
showFailToast('接口请求超时,请刷新页面重试!')
|
||||
showFailToast(t('http.timeout'))
|
||||
return
|
||||
}
|
||||
if (err && err.includes('Network Error')) {
|
||||
showDialog({
|
||||
title: '网络异常',
|
||||
message: '请检查您的网络连接是否正常',
|
||||
title: t('http.networkError'),
|
||||
message: t('http.networkErrorDesc'),
|
||||
})
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
|
||||
Reference in New Issue
Block a user