mockpay功能调试
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-08-20 00:51:18 +08:00
parent 2f633b307c
commit 4db21cf1a1
11 changed files with 320 additions and 531 deletions
+127 -59
View File
@@ -1,18 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { request } from '../lib/api';
import {
BIG_SCREEN_DEMO_CHANNEL,
clearPendingBigScreenDemo,
readPendingBigScreenDemo,
type BigScreenDemoPayload,
} from '../lib/admin-events';
export type BigScreenOrder = {
id: string;
orderNo: string;
payAmount: number;
items: string;
/** 展示用时间(付款成功时间) */
createdAt: string;
paidAt?: string;
userPhoneMasked: string | null;
};
@@ -23,6 +19,101 @@ const ROW_MS = 2500;
const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const TIER_DURATION: Record<AmountTier, number> = { 1: 3000, 2: 4500, 3: 6000 };
const SEED_IDS = new Set(
['seed-1', 'seed-2', 'seed-3', 'seed-4', 'seed-5', 'seed-6', 'seed-7', 'seed-8'],
);
function seedPaidAt(h: number, m: number, s: number): string {
const d = new Date();
d.setHours(h, m, s, 0);
return d.toISOString();
}
/** 发布会滚动列表垫底演示数据(不参与成交动效) */
const BIG_SCREEN_SEED_ORDERS: BigScreenOrder[] = [
{
id: 'seed-1',
orderNo: 'DK-DEMO-001',
payAmount: 1288,
items: '杜康君酿 · 浓香型 × 6瓶',
createdAt: seedPaidAt(14, 28, 36),
paidAt: seedPaidAt(14, 28, 36),
userPhoneMasked: '138****8888',
},
{
id: 'seed-2',
orderNo: 'DK-DEMO-002',
payAmount: 688,
items: '杜康君酿 · 浓香型 × 2瓶',
createdAt: seedPaidAt(14, 22, 18),
paidAt: seedPaidAt(14, 22, 18),
userPhoneMasked: '139****6666',
},
{
id: 'seed-3',
orderNo: 'DK-DEMO-003',
payAmount: 1588,
items: '杜康典藏 · 礼盒装 × 2套',
createdAt: seedPaidAt(14, 15, 42),
paidAt: seedPaidAt(14, 15, 42),
userPhoneMasked: '136****9527',
},
{
id: 'seed-4',
orderNo: 'DK-DEMO-004',
payAmount: 520,
items: '杜康特曲 · 500ml × 4瓶',
createdAt: seedPaidAt(14, 8, 55),
paidAt: seedPaidAt(14, 8, 55),
userPhoneMasked: '135****3210',
},
{
id: 'seed-5',
orderNo: 'DK-DEMO-005',
payAmount: 268,
items: '杜康君酿 · 浓香型 × 1瓶',
createdAt: seedPaidAt(13, 56, 7),
paidAt: seedPaidAt(13, 56, 7),
userPhoneMasked: '137****1888',
},
{
id: 'seed-6',
orderNo: 'DK-DEMO-006',
payAmount: 999,
items: '杜康窖藏 · 珍藏版 × 3瓶',
createdAt: seedPaidAt(13, 48, 33),
paidAt: seedPaidAt(13, 48, 33),
userPhoneMasked: '133****7788',
},
{
id: 'seed-7',
orderNo: 'DK-DEMO-007',
payAmount: 188,
items: '杜康小酒 · 品鉴装 × 2瓶',
createdAt: seedPaidAt(13, 35, 21),
paidAt: seedPaidAt(13, 35, 21),
userPhoneMasked: '158****0099',
},
{
id: 'seed-8',
orderNo: 'DK-DEMO-008',
payAmount: 1200,
items: '杜康君酿 · 浓香型 × 5瓶',
createdAt: seedPaidAt(13, 22, 49),
paidAt: seedPaidAt(13, 22, 49),
userPhoneMasked: '186****5566',
},
];
function mergeDisplayItems(live: BigScreenOrder[]): BigScreenOrder[] {
if (!live.length) return [...BIG_SCREEN_SEED_ORDERS];
return [...live, ...BIG_SCREEN_SEED_ORDERS];
}
function isSeedOrder(order: BigScreenOrder): boolean {
return SEED_IDS.has(order.id);
}
function pad(n: number) {
return String(n).padStart(2, '0');
}
@@ -35,7 +126,7 @@ function formatDateLine(d: Date) {
return `${d.getFullYear()} / ${pad(d.getMonth() + 1)} / ${pad(d.getDate())} ${WEEKDAYS[d.getDay()]}`;
}
function formatHm(iso: string): string {
function formatOrderTime(iso: string): string {
try {
return formatClock(new Date(iso));
} catch {
@@ -50,6 +141,11 @@ function formatAmount(n: number): string {
});
}
function amountCssClass(payAmount: number, base: string): string {
const wide = Math.floor(Math.abs(Number(payAmount || 0))) >= 100_000;
return wide ? `${base} ${base}--6d` : base;
}
export function amountTier(payAmount: number): AmountTier {
if (payAmount >= 1000) return 3;
if (payAmount >= 500) return 2;
@@ -83,9 +179,11 @@ function OrderRow({
className={`big-screen-row big-screen-row--t${tier}${latest ? ' is-latest' : ''}`}
>
{latest ? <span className="big-screen-row-mark" /> : null}
<span className="big-screen-amount">¥ {formatAmount(order.payAmount)}</span>
<span className={amountCssClass(order.payAmount, 'big-screen-amount')}>
¥ {formatAmount(order.payAmount)}
</span>
<span className="big-screen-items">{order.items || '—'}</span>
<span className="big-screen-time">{formatHm(order.createdAt)}</span>
<span className="big-screen-time">{formatOrderTime(order.paidAt ?? order.createdAt)}</span>
<span className="big-screen-phone">{order.userPhoneMasked || '—'}</span>
</div>
);
@@ -253,11 +351,13 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
{tier >= 2 ? <div className="big-screen-fx-sweep big-screen-fx-sweep--alt" /> : null}
<div className={`big-screen-fx-card big-screen-fx-card--t${tier}`}>
<div className="big-screen-fx-kicker">{tier === 3 ? '高额成交' : tier === 2 ? '大额成交' : '新成交'}</div>
<div className="big-screen-fx-amount">¥ {formatAmount(displayAmount)}</div>
<div className={amountCssClass(displayAmount || order.payAmount, 'big-screen-fx-amount')}>
¥ {formatAmount(displayAmount)}
</div>
<div className="big-screen-fx-items">{order.items || '—'}</div>
<div className="big-screen-fx-meta">
<span>{order.userPhoneMasked || '—'}</span>
<span>{formatHm(order.createdAt)}</span>
<span>{formatOrderTime(order.paidAt ?? order.createdAt)}</span>
</div>
</div>
</div>
@@ -270,12 +370,16 @@ export default function BigScreenPage() {
const [paused, setPaused] = useState(false);
const [celebrate, setCelebrate] = useState<BigScreenOrder | null>(null);
const seenIdsRef = useRef<Set<string> | null>(null);
const celebratedIdsRef = useRef<Set<string>>(new Set());
const queueRef = useRef<BigScreenOrder[]>([]);
const celebratingRef = useRef(false);
const viewportRef = useRef<HTMLDivElement | null>(null);
const demoSeenRef = useRef<Set<string>>(new Set());
const [viewportH, setViewportH] = useState(0);
useEffect(() => {
for (const o of BIG_SCREEN_SEED_ORDERS) celebratedIdsRef.current.add(o.id);
}, []);
const playNext = useCallback(() => {
const next = queueRef.current.shift() ?? null;
celebratingRef.current = !!next;
@@ -285,11 +389,13 @@ export default function BigScreenPage() {
const enqueueNew = useCallback(
(fresh: BigScreenOrder[]) => {
if (!fresh.length) return;
const ranked = [...fresh].sort((a, b) => {
const toCelebrate = fresh.filter((o) => !celebratedIdsRef.current.has(o.id));
if (!toCelebrate.length) return;
for (const o of toCelebrate) celebratedIdsRef.current.add(o.id);
const ranked = [...toCelebrate].sort((a, b) => {
const td = amountTier(b.payAmount) - amountTier(a.payAmount);
if (td !== 0) return td;
return +new Date(b.createdAt) - +new Date(a.createdAt);
return +new Date(b.paidAt ?? b.createdAt) - +new Date(a.paidAt ?? a.createdAt);
});
queueRef.current.push(...ranked);
if (!celebratingRef.current) playNext();
@@ -297,38 +403,16 @@ export default function BigScreenPage() {
[playNext],
);
const playDemo = useCallback(
(payload: BigScreenDemoPayload) => {
if (!payload?.orders?.length || demoSeenRef.current.has(payload.id)) return;
demoSeenRef.current.add(payload.id);
const now = new Date().toISOString();
const fake: BigScreenOrder[] = payload.orders.map((o, i) => ({
id: `${payload.id}-${i}`,
orderNo: `${payload.id}-${i}`,
payAmount: o.payAmount,
items: o.items,
createdAt: now,
userPhoneMasked: o.userPhoneMasked,
}));
setItems((prev) => [...fake, ...prev.filter((row) => !row.id.startsWith('demo-'))]);
enqueueNew(fake);
clearPendingBigScreenDemo();
},
[enqueueNew],
);
const fetchData = useCallback(() => {
return request<{ items: BigScreenOrder[] }>('/admin/orders/big-screen?limit=2000')
.then((d) => {
const list = d.items ?? [];
setItems((prev) => {
const demos = prev.filter((o) => o.id.startsWith('demo-'));
return demos.length ? [...demos, ...list] : list;
});
setItems(mergeDisplayItems(list));
setLoadError('');
const seen = seenIdsRef.current;
if (!seen) {
seenIdsRef.current = new Set(list.map((o) => o.id));
for (const o of list) celebratedIdsRef.current.add(o.id);
return;
}
const fresh = list.filter((o) => !seen.has(o.id));
@@ -337,6 +421,7 @@ export default function BigScreenPage() {
})
.catch((e) => {
setLoadError(e instanceof Error ? e.message : '加载失败');
setItems(mergeDisplayItems([]));
});
}, [enqueueNew]);
@@ -346,24 +431,6 @@ export default function BigScreenPage() {
return () => clearInterval(id);
}, [fetchData]);
useEffect(() => {
const pending = readPendingBigScreenDemo();
if (pending) playDemo(pending);
let ch: BroadcastChannel | null = null;
try {
ch = new BroadcastChannel(BIG_SCREEN_DEMO_CHANNEL);
ch.onmessage = (ev: MessageEvent<BigScreenDemoPayload>) => {
if (ev.data?.type === 'demo' && ev.data.orders?.length) playDemo(ev.data);
};
} catch {
ch = null;
}
return () => {
ch?.close();
};
}, [playDemo]);
useEffect(() => {
const html = document.documentElement;
const prevHtml = html.style.overflow;
@@ -399,7 +466,8 @@ export default function BigScreenPage() {
const trackItems = useMemo(() => unitItems.concat(unitItems), [unitItems]);
const marqueeMs = Math.max(unitItems.length, 1) * ROW_MS;
const rolling = items.length > 0 && unitItems.length > 0;
const latestId = items[0]?.id;
const latestLive = items.find((o) => !isSeedOrder(o));
const latestId = latestLive?.id;
return (
<div className="big-screen-page">
@@ -431,7 +499,7 @@ export default function BigScreenPage() {
<div className="big-screen-list-head">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div className="big-screen-list-body" ref={viewportRef}>