fix(mini-user): 微信小程序补 Intl 兜底并去掉 toLocaleString
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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 {};
|
||||||
@@ -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}`;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:03(Asia/Shanghai) */
|
/** 与权益「历史记录」一致:2026-08-03 13:53:03(Asia/Shanghai) */
|
||||||
function formatChinaDateTime(input?: string | null) {
|
function formatChinaDateTime(input?: string | null) {
|
||||||
return formatShanghaiDateTime(input ?? new Date());
|
return formatShanghaiDateTime(input ?? new Date());
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user