diff --git a/apps/mini-user/src/app.tsx b/apps/mini-user/src/app.tsx index a65b356..bd8cca0 100644 --- a/apps/mini-user/src/app.tsx +++ b/apps/mini-user/src/app.tsx @@ -1,3 +1,4 @@ +import './lib/intl-polyfill'; import './lib/text-encoding-polyfill'; import { PropsWithChildren, useRef } from 'react'; import Taro, { useDidShow } from '@tarojs/taro'; diff --git a/apps/mini-user/src/lib/intl-polyfill.ts b/apps/mini-user/src/lib/intl-polyfill.ts new file mode 100644 index 0000000..0858baa --- /dev/null +++ b/apps/mini-user/src/lib/intl-polyfill.ts @@ -0,0 +1,72 @@ +/** + * 微信小程序基础库无 Intl。业务代码已避免依赖,此处仅作兜底, + * 防止旧包 / 依赖偶发 `new Intl.DateTimeFormat` 直接白屏。 + */ +function pad(n: number) { + return String(n).padStart(2, '0'); +} + +function shanghaiParts(date: Date) { + const sh = new Date(date.getTime() + 8 * 60 * 60 * 1000); + return { + year: String(sh.getUTCFullYear()), + month: pad(sh.getUTCMonth() + 1), + day: pad(sh.getUTCDate()), + hour: pad(sh.getUTCHours()), + minute: pad(sh.getUTCMinutes()), + second: pad(sh.getUTCSeconds()), + }; +} + +function installIntlStub() { + const root = (typeof globalThis !== 'undefined' + ? globalThis + : typeof global !== 'undefined' + ? global + : typeof wx !== 'undefined' + ? wx + : {}) as typeof globalThis & { Intl?: typeof Intl }; + + if (typeof root.Intl !== 'undefined' && typeof root.Intl.DateTimeFormat === 'function') { + return; + } + + class MiniDateTimeFormat { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(_locales?: string | string[], _options?: Record) {} + + format(date?: Date | number) { + const d = date instanceof Date ? date : new Date(date ?? Date.now()); + if (Number.isNaN(d.getTime())) return ''; + const p = shanghaiParts(d); + return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}:${p.second}`; + } + + formatToParts(date?: Date | number) { + const d = date instanceof Date ? date : new Date(date ?? Date.now()); + if (Number.isNaN(d.getTime())) return []; + const p = shanghaiParts(d); + return [ + { type: 'year', value: p.year }, + { type: 'literal', value: '-' }, + { type: 'month', value: p.month }, + { type: 'literal', value: '-' }, + { type: 'day', value: p.day }, + { type: 'literal', value: ' ' }, + { type: 'hour', value: p.hour }, + { type: 'literal', value: ':' }, + { type: 'minute', value: p.minute }, + { type: 'literal', value: ':' }, + { type: 'second', value: p.second }, + ]; + } + } + + root.Intl = { + DateTimeFormat: MiniDateTimeFormat, + } as unknown as typeof Intl; +} + +installIntlStub(); + +export {}; diff --git a/apps/mini-user/src/lib/money.ts b/apps/mini-user/src/lib/money.ts new file mode 100644 index 0000000..1b500dd --- /dev/null +++ b/apps/mini-user/src/lib/money.ts @@ -0,0 +1,9 @@ +/** 金额展示(不依赖 Intl / toLocaleString,兼容微信小程序) */ +export function formatMoney(amount: number | string): string { + const n = typeof amount === 'number' ? amount : Number(amount); + if (!Number.isFinite(n)) return '0.00'; + const fixed = n.toFixed(2); + const [intPart, dec] = fixed.split('.'); + const withComma = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return `${withComma}.${dec}`; +} diff --git a/apps/mini-user/src/pages/benefit/index.tsx b/apps/mini-user/src/pages/benefit/index.tsx index 85e034c..83c4108 100644 --- a/apps/mini-user/src/pages/benefit/index.tsx +++ b/apps/mini-user/src/pages/benefit/index.tsx @@ -12,6 +12,7 @@ import { DEFAULT_SHARE_TITLE, toWeappShareMessage, } from '../../lib/wechat-share'; +import { formatMoney } from '../../lib/money'; import iconBenefit from '../../assets/tabbar/benefit-active.png'; type BenefitSummary = { @@ -38,10 +39,6 @@ type RedeemHistoryItem = { createdAt: string; }; -function formatMoney(amount: number) { - return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} - function usagePercent(coupon: CouponItem) { const total = Number(coupon.totalAmount); if (total <= 0) return 0; diff --git a/apps/mini-user/src/pages/mine/index.tsx b/apps/mini-user/src/pages/mine/index.tsx index 397e29d..6257404 100644 --- a/apps/mini-user/src/pages/mine/index.tsx +++ b/apps/mini-user/src/pages/mine/index.tsx @@ -37,6 +37,7 @@ import iconStores from '../../assets/icons/可用门店.png'; import iconCs from '../../assets/icons/联系客服.png'; import iconQualification from '../../assets/icons/资质公示.png'; import iconAbout from '../../assets/icons/关于我们.png'; +import { formatMoney } from '../../lib/money'; const ORDER_SHORTCUTS = [ { tab: 'pending_pay', icon: iconPendingPay, label: '待付款' }, @@ -54,10 +55,6 @@ const SERVICES = [ const isWeapp = process.env.TARO_ENV === 'weapp'; -function formatMoney(amount: number) { - return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} - export default function MinePage() { const [authed, setAuthed] = useState(() => isLoggedIn()); const [profile, setProfile] = useState(null); diff --git a/apps/mini-user/src/pages/redeem-code/index.tsx b/apps/mini-user/src/pages/redeem-code/index.tsx index e7cde41..0e7e23b 100644 --- a/apps/mini-user/src/pages/redeem-code/index.tsx +++ b/apps/mini-user/src/pages/redeem-code/index.tsx @@ -6,6 +6,7 @@ import PageShell from '../../components/PageShell'; import RedeemQrCode from '../../components/RedeemQrCode'; import SubPageHeader from '../../components/SubPageHeader'; import { request, toast } from '../../lib/api'; +import { formatMoney } from '../../lib/money'; const POLL_INTERVAL_MS = 2500; const LAST_REDEEM_RECORD_KEY = 'lastRedeemRecordId'; @@ -26,10 +27,6 @@ type RedeemTokenStatus = } | { status: 'EXPIRED' }; -function formatMoney(amount: number) { - return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} - function formatTimer(seconds: number) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; diff --git a/apps/mini-user/src/pages/redeem-success/index.tsx b/apps/mini-user/src/pages/redeem-success/index.tsx index bcf392f..890640b 100644 --- a/apps/mini-user/src/pages/redeem-success/index.tsx +++ b/apps/mini-user/src/pages/redeem-success/index.tsx @@ -5,6 +5,7 @@ import PageShell from '../../components/PageShell'; import SubPageHeader from '../../components/SubPageHeader'; import { request, toast } from '../../lib/api'; import { formatShanghaiDateTime } from '../../lib/datetime'; +import { formatMoney } from '../../lib/money'; const LAST_REDEEM_RESULT_KEY = 'lastRedeemResult'; @@ -17,15 +18,6 @@ type RedeemRecord = { createdAt: string; }; -function formatMoney(amount: number) { - const n = Number(amount); - if (!Number.isFinite(n)) return '0.00'; - const fixed = n.toFixed(2); - const [intPart, dec] = fixed.split('.'); - const withComma = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); - return `${withComma}.${dec}`; -} - /** 与权益「历史记录」一致:2026-08-03 13:53:03(Asia/Shanghai) */ function formatChinaDateTime(input?: string | null) { return formatShanghaiDateTime(input ?? new Date()); diff --git a/apps/mini-user/src/pages/redeem/index.tsx b/apps/mini-user/src/pages/redeem/index.tsx index 86206dc..c7af0c0 100644 --- a/apps/mini-user/src/pages/redeem/index.tsx +++ b/apps/mini-user/src/pages/redeem/index.tsx @@ -5,6 +5,7 @@ import PageShell from '../../components/PageShell'; import SubPageHeader from '../../components/SubPageHeader'; import { goLogin } from '../../lib/auth-nav'; import { isLoggedIn, request, toast } from '../../lib/api'; +import { formatMoney } from '../../lib/money'; type BenefitSummary = { totalBalance: number; @@ -13,10 +14,6 @@ type BenefitSummary = { const MIN_REDEEM_AMOUNT = 0.01; -function formatMoney(amount: number) { - return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); -} - /** * 核销金额输入清洗: * - 只保留数字与一个小数点 diff --git a/apps/mini-user/src/pages/store-detail/index.tsx b/apps/mini-user/src/pages/store-detail/index.tsx index 0b03c8f..a482e62 100644 --- a/apps/mini-user/src/pages/store-detail/index.tsx +++ b/apps/mini-user/src/pages/store-detail/index.tsx @@ -104,9 +104,14 @@ function formatRedeemAmountYuan(amount: number | string) { function formatRecentRedeemLine(row: RecentRedeem) { try { + // 优先用服务端拼好的 text,避免客户端时区 / Intl 差异 if (row.text?.trim()) return row.text.trim(); const label = String(row.userLabel || '用户***').trim() || '用户***'; - const time = formatRedeemTime(row.createdAt); + const rawTime = String(row.createdAt || '').trim(); + const time = + /^\d{4}-\d{2}-\d{2}/.test(rawTime) + ? rawTime.slice(0, 19).replace('T', ' ') + : formatRedeemTime(row.createdAt); const amount = formatRedeemAmountYuan(row.amount); return `${label} ${time} 核销${amount}元`; } catch {