fix(mini-user): 微信小程序补 Intl 兜底并去掉 toLocaleString

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 14:53:54 +08:00
parent 04fd4d50d0
commit a7a0a54688
9 changed files with 93 additions and 26 deletions
+1
View File
@@ -1,3 +1,4 @@
import './lib/intl-polyfill';
import './lib/text-encoding-polyfill'; import './lib/text-encoding-polyfill';
import { PropsWithChildren, useRef } from 'react'; import { PropsWithChildren, useRef } from 'react';
import Taro, { useDidShow } from '@tarojs/taro'; import Taro, { useDidShow } from '@tarojs/taro';
+72
View File
@@ -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<string, unknown>) {}
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 {};
+9
View File
@@ -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}`;
}
+1 -4
View File
@@ -12,6 +12,7 @@ import {
DEFAULT_SHARE_TITLE, DEFAULT_SHARE_TITLE,
toWeappShareMessage, toWeappShareMessage,
} from '../../lib/wechat-share'; } from '../../lib/wechat-share';
import { formatMoney } from '../../lib/money';
import iconBenefit from '../../assets/tabbar/benefit-active.png'; import iconBenefit from '../../assets/tabbar/benefit-active.png';
type BenefitSummary = { type BenefitSummary = {
@@ -38,10 +39,6 @@ type RedeemHistoryItem = {
createdAt: string; createdAt: string;
}; };
function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function usagePercent(coupon: CouponItem) { function usagePercent(coupon: CouponItem) {
const total = Number(coupon.totalAmount); const total = Number(coupon.totalAmount);
if (total <= 0) return 0; if (total <= 0) return 0;
+1 -4
View File
@@ -37,6 +37,7 @@ import iconStores from '../../assets/icons/可用门店.png';
import iconCs from '../../assets/icons/联系客服.png'; import iconCs from '../../assets/icons/联系客服.png';
import iconQualification from '../../assets/icons/资质公示.png'; import iconQualification from '../../assets/icons/资质公示.png';
import iconAbout from '../../assets/icons/关于我们.png'; import iconAbout from '../../assets/icons/关于我们.png';
import { formatMoney } from '../../lib/money';
const ORDER_SHORTCUTS = [ const ORDER_SHORTCUTS = [
{ tab: 'pending_pay', icon: iconPendingPay, label: '待付款' }, { tab: 'pending_pay', icon: iconPendingPay, label: '待付款' },
@@ -54,10 +55,6 @@ const SERVICES = [
const isWeapp = process.env.TARO_ENV === 'weapp'; const isWeapp = process.env.TARO_ENV === 'weapp';
function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
export default function MinePage() { export default function MinePage() {
const [authed, setAuthed] = useState(() => isLoggedIn()); const [authed, setAuthed] = useState(() => isLoggedIn());
const [profile, setProfile] = useState<UserProfile | null>(null); const [profile, setProfile] = useState<UserProfile | null>(null);
@@ -6,6 +6,7 @@ import PageShell from '../../components/PageShell';
import RedeemQrCode from '../../components/RedeemQrCode'; import RedeemQrCode from '../../components/RedeemQrCode';
import SubPageHeader from '../../components/SubPageHeader'; import SubPageHeader from '../../components/SubPageHeader';
import { request, toast } from '../../lib/api'; import { request, toast } from '../../lib/api';
import { formatMoney } from '../../lib/money';
const POLL_INTERVAL_MS = 2500; const POLL_INTERVAL_MS = 2500;
const LAST_REDEEM_RECORD_KEY = 'lastRedeemRecordId'; const LAST_REDEEM_RECORD_KEY = 'lastRedeemRecordId';
@@ -26,10 +27,6 @@ type RedeemTokenStatus =
} }
| { status: 'EXPIRED' }; | { status: 'EXPIRED' };
function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatTimer(seconds: number) { function formatTimer(seconds: number) {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
const secs = seconds % 60; const secs = seconds % 60;
@@ -5,6 +5,7 @@ import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader'; import SubPageHeader from '../../components/SubPageHeader';
import { request, toast } from '../../lib/api'; import { request, toast } from '../../lib/api';
import { formatShanghaiDateTime } from '../../lib/datetime'; import { formatShanghaiDateTime } from '../../lib/datetime';
import { formatMoney } from '../../lib/money';
const LAST_REDEEM_RESULT_KEY = 'lastRedeemResult'; const LAST_REDEEM_RESULT_KEY = 'lastRedeemResult';
@@ -17,15 +18,6 @@ type RedeemRecord = {
createdAt: string; 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:03Asia/Shanghai */ /** 与权益「历史记录」一致:2026-08-03 13:53:03Asia/Shanghai */
function formatChinaDateTime(input?: string | null) { function formatChinaDateTime(input?: string | null) {
return formatShanghaiDateTime(input ?? new Date()); return formatShanghaiDateTime(input ?? new Date());
+1 -4
View File
@@ -5,6 +5,7 @@ import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader'; import SubPageHeader from '../../components/SubPageHeader';
import { goLogin } from '../../lib/auth-nav'; import { goLogin } from '../../lib/auth-nav';
import { isLoggedIn, request, toast } from '../../lib/api'; import { isLoggedIn, request, toast } from '../../lib/api';
import { formatMoney } from '../../lib/money';
type BenefitSummary = { type BenefitSummary = {
totalBalance: number; totalBalance: number;
@@ -13,10 +14,6 @@ type BenefitSummary = {
const MIN_REDEEM_AMOUNT = 0.01; const MIN_REDEEM_AMOUNT = 0.01;
function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
/** /**
* 核销金额输入清洗: * 核销金额输入清洗:
* - 只保留数字与一个小数点 * - 只保留数字与一个小数点
@@ -104,9 +104,14 @@ function formatRedeemAmountYuan(amount: number | string) {
function formatRecentRedeemLine(row: RecentRedeem) { function formatRecentRedeemLine(row: RecentRedeem) {
try { try {
// 优先用服务端拼好的 text,避免客户端时区 / Intl 差异
if (row.text?.trim()) return row.text.trim(); if (row.text?.trim()) return row.text.trim();
const label = String(row.userLabel || '用户***').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); const amount = formatRedeemAmountYuan(row.amount);
return `${label} ${time} 核销${amount}`; return `${label} ${time} 核销${amount}`;
} catch { } catch {