diff --git a/src/mobile/app.ts b/src/mobile/app.ts
index 6dbce78..b1a6c0b 100644
--- a/src/mobile/app.ts
+++ b/src/mobile/app.ts
@@ -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 / 初始化副作用)
+ setupI18n(app)
// 初始化全局主题:跟随系统 prefers-color-scheme(不支持手动修改)
useDesignSettingWithOut().initSystemListener()
// 挂载路由
diff --git a/src/mobile/views/cashier/index.vue b/src/mobile/views/cashier/index.vue
index 7d515b0..9b7582c 100644
--- a/src/mobile/views/cashier/index.vue
+++ b/src/mobile/views/cashier/index.vue
@@ -1,34 +1,40 @@
@@ -68,7 +70,7 @@ function pay() {
{{ cashierInfo.name }}
- 付款金额
+ {{ t('codePay.amountLabel') }}
¥
@@ -80,9 +82,9 @@ function pay() {
@@ -78,7 +95,7 @@ function statusClass(status: Trade['status']) {
-
交易笔数
+
{{ t('statement.tradeCount') }}
{{ summary.count }}
@@ -93,7 +110,7 @@ function statusClass(status: Trade['status']) {
- 支付成功率
+ {{ t('statement.successRate') }}
{{ summary.successRate }}%
@@ -104,31 +121,31 @@ function statusClass(status: Trade['status']) {
- | 交易时间 |
- 订单号 |
- 商品标题 |
+ {{ t('statement.column.time') }} |
+ {{ t('statement.column.orderNo') }} |
+ {{ t('statement.column.title') }} |
- 金额
+ {{ t('statement.column.amount') }}
|
- 支付通道 |
- 状态 |
+ {{ t('statement.column.channel') }} |
+ {{ t('statement.column.status') }} |
-
- | {{ t.time }} |
+
+ | {{ trade.time }} |
- {{ t.orderNo }}
+ {{ trade.orderNo }}
|
- {{ t.title }} |
+ {{ trade.title }} |
- ¥{{ t.amount.toFixed(2) }}
+ ¥{{ trade.amount.toFixed(2) }}
|
- {{ t.channel }} |
+ {{ channelText(trade.channel) }} |
-
+
- {{ t.status }}
+ {{ statusText(trade.status) }}
|
@@ -138,7 +155,7 @@ function statusClass(status: Trade['status']) {
- 暂无交易记录
+ {{ t('statement.empty') }}
diff --git a/src/shared/locales/en-US/cashier.json b/src/shared/locales/en-US/cashier.json
new file mode 100644
index 0000000..e4f77dd
--- /dev/null
+++ b/src/shared/locales/en-US/cashier.json
@@ -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"
+ }
+}
diff --git a/src/shared/locales/en-US/codePay.json b/src/shared/locales/en-US/codePay.json
new file mode 100644
index 0000000..1925883
--- /dev/null
+++ b/src/shared/locales/en-US/codePay.json
@@ -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}"
+}
diff --git a/src/shared/locales/en-US/common.json b/src/shared/locales/en-US/common.json
new file mode 100644
index 0000000..8355ceb
--- /dev/null
+++ b/src/shared/locales/en-US/common.json
@@ -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!"
+}
diff --git a/src/shared/locales/en-US/error.json b/src/shared/locales/en-US/error.json
new file mode 100644
index 0000000..cab8eae
--- /dev/null
+++ b/src/shared/locales/en-US/error.json
@@ -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"
+ }
+}
diff --git a/src/shared/locales/en-US/home.json b/src/shared/locales/en-US/home.json
new file mode 100644
index 0000000..7224373
--- /dev/null
+++ b/src/shared/locales/en-US/home.json
@@ -0,0 +1,5 @@
+{
+ "welcome": "Welcome to DaxPay",
+ "version": "Version",
+ "buildTime": "Build time"
+}
diff --git a/src/shared/locales/en-US/http.json b/src/shared/locales/en-US/http.json
new file mode 100644
index 0000000..ad6fb18
--- /dev/null
+++ b/src/shared/locales/en-US/http.json
@@ -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!"
+ }
+}
diff --git a/src/shared/locales/en-US/index.ts b/src/shared/locales/en-US/index.ts
new file mode 100644
index 0000000..ad6b3c9
--- /dev/null
+++ b/src/shared/locales/en-US/index.ts
@@ -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,
+}
diff --git a/src/shared/locales/en-US/protocol.json b/src/shared/locales/en-US/protocol.json
new file mode 100644
index 0000000..76e6c4c
--- /dev/null
+++ b/src/shared/locales/en-US/protocol.json
@@ -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}"
+}
diff --git a/src/shared/locales/en-US/statement.json b/src/shared/locales/en-US/statement.json
new file mode 100644
index 0000000..85f7cf4
--- /dev/null
+++ b/src/shared/locales/en-US/statement.json
@@ -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"
+ }
+}
diff --git a/src/shared/locales/index.ts b/src/shared/locales/index.ts
new file mode 100644
index 0000000..a6dc099
--- /dev/null
+++ b/src/shared/locales/index.ts
@@ -0,0 +1,124 @@
+/**
+ * 国际化核心装配
+ *
+ * - locale 状态以 vue-i18n 实例为单一事实源(composition 模式)
+ * - 语言切换副作用:dayjs locale / vant 语言包 / `` / 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 | 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 / ``
+ *
+ * 注意:此处**不**写 localStorage。自动探测/初始化若落盘,
+ * 会让陈旧值锁定语言、使后续浏览器语言变化失效。
+ * 仅显式切换(`setLocale`)才持久化。
+ */
+function applySideEffects(code: AppLocaleCode) {
+ const opt: AppLocaleOption = getLocaleOption(code) ?? APP_LOCALES[0]!
+ // dayjs 全局 locale(影响 fromNow / localizedFormat 等本地化输出)
+ dayjs.locale(opt.dayjsLocale)
+ // 同步 属性
+ 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 {
+ return i18n.global.t(key, named ?? ({} as Record))
+}
+
+/**
+ * 组件内语言组合式 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
diff --git a/src/shared/locales/lang.ts b/src/shared/locales/lang.ts
new file mode 100644
index 0000000..37ac391
--- /dev/null
+++ b/src/shared/locales/lang.ts
@@ -0,0 +1,94 @@
+/**
+ * 语言选项与浏览器探测
+ *
+ * 语言代码与后端 common-i18n 对齐:仅支持 zh-CN / en-US。
+ */
+
+/** 应用支持的语言代码(与后端 common-i18n 一致) */
+export type AppLocaleCode = 'zh-CN' | 'en-US'
+
+export interface AppLocaleOption {
+ /** 语言代码 */
+ code: AppLocaleCode
+ /** `` 属性值 */
+ htmlLang: string
+ /** dayjs locale 名称 */
+ dayjsLocale: string
+ /** vant 语言包动态导入函数 */
+ vantLoader: () => Promise<{ default: Record }>
+}
+
+/** 全部支持的语言选项 */
+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(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()
+}
diff --git a/src/shared/locales/zh-CN/cashier.json b/src/shared/locales/zh-CN/cashier.json
new file mode 100644
index 0000000..3e5ed81
--- /dev/null
+++ b/src/shared/locales/zh-CN/cashier.json
@@ -0,0 +1,20 @@
+{
+ "demoOrderTitle": "DaxPay 演示订单",
+ "remainTime": "支付剩余时间",
+ "remainTimeShort": "剩余支付时间",
+ "orderTitle": "订单标题",
+ "orderNo": "订单编号",
+ "orderNoLabel": "订单编号:",
+ "payableAmount": "应付金额",
+ "selectMethod": "请选择支付方式",
+ "recommend": "推荐",
+ "payNow": "立即支付",
+ "demoPayStarted": "演示:已发起 {name} 支付",
+ "qrcodePayDemo": "扫码支付(演示)",
+ "qrcodeTip": "请使用 {name} 扫码完成支付",
+ "method": {
+ "wechat": "微信支付",
+ "alipay": "支付宝",
+ "union": "银联支付"
+ }
+}
diff --git a/src/shared/locales/zh-CN/codePay.json b/src/shared/locales/zh-CN/codePay.json
new file mode 100644
index 0000000..8b9fdcd
--- /dev/null
+++ b/src/shared/locales/zh-CN/codePay.json
@@ -0,0 +1,10 @@
+{
+ "demoMerchant": "演示收款商户",
+ "amountLabel": "付款金额",
+ "remark": "备注",
+ "addRemark": "添加备注",
+ "remarkPlaceholder": "请输入支付备注内容",
+ "amountZero": "金额不可为0",
+ "confirmPay": "确认支付",
+ "demoPay": "演示:码牌 {code} 支付 ¥{amount}"
+}
diff --git a/src/shared/locales/zh-CN/common.json b/src/shared/locales/zh-CN/common.json
new file mode 100644
index 0000000..b619011
--- /dev/null
+++ b/src/shared/locales/zh-CN/common.json
@@ -0,0 +1,11 @@
+{
+ "confirm": "确认",
+ "cancel": "取消",
+ "save": "保存",
+ "back": "返回",
+ "backHome": "返回首页",
+ "loading": "加载中...",
+ "tip": "提示",
+ "operationSuccess": "操作成功!",
+ "operationFailed": "操作失败!"
+}
diff --git a/src/shared/locales/zh-CN/error.json b/src/shared/locales/zh-CN/error.json
new file mode 100644
index 0000000..6392601
--- /dev/null
+++ b/src/shared/locales/zh-CN/error.json
@@ -0,0 +1,11 @@
+{
+ "notFound": "抱歉,你访问的页面不存在",
+ "deviceOnlyPc": {
+ "title": "此页面仅限电脑端访问",
+ "desc": "请在电脑浏览器中打开本页面"
+ },
+ "deviceOnlyMobile": {
+ "title": "此页面仅限手机端访问",
+ "desc": "请用手机扫描二维码,或在手机浏览器中打开本页面"
+ }
+}
diff --git a/src/shared/locales/zh-CN/home.json b/src/shared/locales/zh-CN/home.json
new file mode 100644
index 0000000..ffe9380
--- /dev/null
+++ b/src/shared/locales/zh-CN/home.json
@@ -0,0 +1,5 @@
+{
+ "welcome": "欢迎使用 DaxPay",
+ "version": "版本号",
+ "buildTime": "构建时间"
+}
diff --git a/src/shared/locales/zh-CN/http.json b/src/shared/locales/zh-CN/http.json
new file mode 100644
index 0000000..362abe7
--- /dev/null
+++ b/src/shared/locales/zh-CN/http.json
@@ -0,0 +1,19 @@
+{
+ "requestError": "请求出错,请稍候重试",
+ "timeout": "接口请求超时,请刷新页面重试!",
+ "networkError": "网络异常",
+ "networkErrorDesc": "请检查您的网络连接是否正常",
+ "status": {
+ "401": "用户没有权限(令牌、用户名、密码错误)!",
+ "403": "用户得到授权,但是访问是被禁止的!",
+ "404": "网络请求错误,未找到该资源!",
+ "405": "网络请求错误,请求方法未允许!",
+ "408": "网络请求超时",
+ "500": "服务器错误,请联系管理员!",
+ "501": "网络未实现",
+ "502": "网络错误",
+ "503": "服务不可用,服务器暂时过载或维护!",
+ "504": "网络超时",
+ "505": "http版本不支持该请求!"
+ }
+}
diff --git a/src/shared/locales/zh-CN/index.ts b/src/shared/locales/zh-CN/index.ts
new file mode 100644
index 0000000..ad6b3c9
--- /dev/null
+++ b/src/shared/locales/zh-CN/index.ts
@@ -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,
+}
diff --git a/src/shared/locales/zh-CN/protocol.json b/src/shared/locales/zh-CN/protocol.json
new file mode 100644
index 0000000..371c01f
--- /dev/null
+++ b/src/shared/locales/zh-CN/protocol.json
@@ -0,0 +1,8 @@
+{
+ "title": "协议",
+ "typeNotExist": "协议类型不存在",
+ "loadFailed": "协议加载失败",
+ "noContent": "暂无协议内容",
+ "version": "版本 {version}",
+ "effectiveTime": "生效时间 {time}"
+}
diff --git a/src/shared/locales/zh-CN/statement.json b/src/shared/locales/zh-CN/statement.json
new file mode 100644
index 0000000..3bbfc5e
--- /dev/null
+++ b/src/shared/locales/zh-CN/statement.json
@@ -0,0 +1,25 @@
+{
+ "heading": "商户对账单",
+ "totalAmountSuccess": "交易总额(成功)",
+ "tradeCount": "交易笔数",
+ "successRate": "支付成功率",
+ "empty": "暂无交易记录",
+ "column": {
+ "time": "交易时间",
+ "orderNo": "订单号",
+ "title": "商品标题",
+ "amount": "金额",
+ "channel": "支付通道",
+ "status": "状态"
+ },
+ "status": {
+ "success": "成功",
+ "fail": "失败",
+ "pending": "处理中"
+ },
+ "channel": {
+ "wechat": "微信",
+ "alipay": "支付宝",
+ "union": "银联"
+ }
+}
diff --git a/src/shared/utils/datetime.ts b/src/shared/utils/datetime.ts
new file mode 100644
index 0000000..f0d4aaa
--- /dev/null
+++ b/src/shared/utils/datetime.ts
@@ -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() : ''
+}
diff --git a/src/shared/utils/http/axios/checkStatus.ts b/src/shared/utils/http/axios/checkStatus.ts
index aeb2acc..c122519 100644
--- a/src/shared/utils/http/axios/checkStatus.ts
+++ b/src/shared/utils/http/axios/checkStatus.ts
@@ -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)
diff --git a/src/shared/utils/http/axios/helper.ts b/src/shared/utils/http/axios/helper.ts
index a7cd913..c5915a0 100644
--- a/src/shared/utils/http/axios/helper.ts
+++ b/src/shared/utils/http/axios/helper.ts
@@ -1,7 +1,5 @@
import { isObject, isString } from '@/shared/utils/is'
-const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'
-
export function joinTimestamp(
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) {
diff --git a/src/shared/utils/http/axios/index.ts b/src/shared/utils/http/axios/index.ts
index 377acbf..d56d766 100644
--- a/src/shared/utils/http/axios/index.ts
+++ b/src/shared/utils/http/axios/index.ts
@@ -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(() => {})