diff --git a/apps/admin-web/src/admin.css b/apps/admin-web/src/admin.css index 2143e93..e2089d8 100644 --- a/apps/admin-web/src/admin.css +++ b/apps/admin-web/src/admin.css @@ -300,7 +300,7 @@ body:has(.big-screen-page), .big-screen-list-head, .big-screen-row { display: grid; - grid-template-columns: 180px 1fr 160px 200px; + grid-template-columns: 208px 1fr 160px 200px; align-items: center; column-gap: 16px; } @@ -434,6 +434,13 @@ body:has(.big-screen-page), font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; + letter-spacing: 0.5px; + min-width: 0; +} + +.big-screen-amount--6d { + font-size: 0.86em; + letter-spacing: 0; } .big-screen-items { @@ -540,13 +547,14 @@ body:has(.big-screen-page), .big-screen-fx-card { position: relative; z-index: 2; - min-width: 420px; - max-width: 72vw; + min-width: min(420px, 92vw); + max-width: min(920px, 92vw); padding: 28px 40px 32px; border-radius: 12px; text-align: center; background: rgba(8, 22, 48, 0.82); backdrop-filter: blur(8px); + box-sizing: border-box; } .big-screen-fx-card--t1 { @@ -562,7 +570,7 @@ body:has(.big-screen-page), } .big-screen-fx-card--t3 { - min-width: 520px; + min-width: min(560px, 94vw); padding: 36px 48px 40px; border: 3px solid #ffd666; box-shadow: @@ -623,12 +631,19 @@ body:has(.big-screen-page), } .big-screen-fx-amount { - font-size: 64px; + font-size: clamp(44px, 8.5vw, 64px); font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: 2px; line-height: 1.1; color: #e6f7ff; + max-width: 100%; + margin-inline: auto; +} + +.big-screen-fx-amount--6d { + font-size: clamp(36px, 7vw, 52px) !important; + letter-spacing: 1px; } .big-screen-fx-card--t1 .big-screen-fx-amount { @@ -643,7 +658,7 @@ body:has(.big-screen-page), } .big-screen-fx-card--t3 .big-screen-fx-amount { - font-size: 84px; + font-size: clamp(52px, 10vw, 84px); color: #ffd666; text-shadow: 0 0 12px #ffd666, @@ -651,6 +666,11 @@ body:has(.big-screen-page), animation: big-screen-amount-flash 0.9s ease-in-out infinite; } +.big-screen-fx-card--t3 .big-screen-fx-amount--6d { + font-size: clamp(44px, 8.5vw, 68px) !important; + letter-spacing: 1px; +} + @keyframes big-screen-amount-pop { 0% { transform: scale(0.7); @@ -695,14 +715,23 @@ body:has(.big-screen-page), } .big-screen-list-head, .big-screen-row { - grid-template-columns: 140px 1fr 120px 160px; + grid-template-columns: 172px 1fr 120px 160px; font-size: 16px; } + .big-screen-amount--6d { + font-size: 0.82em; + } .big-screen-fx-amount { - font-size: 44px; + font-size: clamp(36px, 8vw, 44px); } .big-screen-fx-card--t3 .big-screen-fx-amount { - font-size: 56px; + font-size: clamp(40px, 9vw, 56px); + } + .big-screen-fx-amount--6d { + font-size: clamp(32px, 6.5vw, 40px) !important; + } + .big-screen-fx-card--t3 .big-screen-fx-amount--6d { + font-size: clamp(34px, 7vw, 48px) !important; } } diff --git a/apps/admin-web/src/components/ProxyOrderModal.tsx b/apps/admin-web/src/components/ProxyOrderModal.tsx index 19d53a6..9c387f0 100644 --- a/apps/admin-web/src/components/ProxyOrderModal.tsx +++ b/apps/admin-web/src/components/ProxyOrderModal.tsx @@ -217,6 +217,12 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder method: 'POST', body: JSON.stringify({ payMethod: 'NATIVE' }), }); + if (pay.mode === 'mock') { + message.success(`支付成功:${order.orderNo}`); + resetForm(); + onSuccess({ id: order.id, orderNo: order.orderNo }); + return; + } setCodeUrl(pay.codeUrl ?? null); startPoll(order.id); } catch (e) { diff --git a/apps/admin-web/src/lib/admin-events.ts b/apps/admin-web/src/lib/admin-events.ts index 818a9ce..abf8da7 100644 --- a/apps/admin-web/src/lib/admin-events.ts +++ b/apps/admin-web/src/lib/admin-events.ts @@ -1,71 +1,5 @@ export const PACKAGE_AUDIT_CHANGED_EVENT = 'admin:package-audit-changed'; export const AUDIT_NOTICE_CHANGED_EVENT = 'dukang:audit-notice-changed'; -export const BIG_SCREEN_DEMO_CHANNEL = 'dukang-big-screen'; -export const BIG_SCREEN_DEMO_STORAGE_KEY = 'dukang:big-screen-demo'; - -export type BigScreenDemoOrder = { - payAmount: number; - items: string; - userPhoneMasked: string; -}; - -export type BigScreenDemoPayload = { - type: 'demo'; - id: string; - at: number; - orders: BigScreenDemoOrder[]; -}; - -export function buildBigScreenDemoPayload(): BigScreenDemoPayload { - return { - type: 'demo', - id: `demo-${Date.now()}`, - at: Date.now(), - orders: [ - { payAmount: 1288, items: '杜康君酿 · 浓香型 × 6瓶', userPhoneMasked: '138****8888' }, - { payAmount: 688, items: '杜康君酿 · 浓香型 × 2瓶', userPhoneMasked: '139****6666' }, - { payAmount: 188, items: '杜康君酿 · 浓香型 × 1瓶', userPhoneMasked: '137****1888' }, - ], - }; -} - -export function triggerBigScreenDemo() { - const payload = buildBigScreenDemoPayload(); - try { - localStorage.setItem(BIG_SCREEN_DEMO_STORAGE_KEY, JSON.stringify(payload)); - } catch { - /* ignore quota */ - } - try { - const ch = new BroadcastChannel(BIG_SCREEN_DEMO_CHANNEL); - ch.postMessage(payload); - ch.close(); - } catch { - /* BroadcastChannel 不可用时靠 localStorage 冷启动 */ - } - return payload; -} - -export function readPendingBigScreenDemo(maxAgeMs = 8000): BigScreenDemoPayload | null { - try { - const raw = localStorage.getItem(BIG_SCREEN_DEMO_STORAGE_KEY); - if (!raw) return null; - const parsed = JSON.parse(raw) as BigScreenDemoPayload; - if (parsed?.type !== 'demo' || !parsed.id || !Array.isArray(parsed.orders)) return null; - if (Date.now() - Number(parsed.at || 0) > maxAgeMs) return null; - return parsed; - } catch { - return null; - } -} - -export function clearPendingBigScreenDemo() { - try { - localStorage.removeItem(BIG_SCREEN_DEMO_STORAGE_KEY); - } catch { - /* ignore */ - } -} export function notifyPackageAuditChanged() { window.dispatchEvent(new Event(PACKAGE_AUDIT_CHANGED_EVENT)); diff --git a/apps/admin-web/src/pages/BigScreenPage.tsx b/apps/admin-web/src/pages/BigScreenPage.tsx index 0ba45f7..84a3b43 100644 --- a/apps/admin-web/src/pages/BigScreenPage.tsx +++ b/apps/admin-web/src/pages/BigScreenPage.tsx @@ -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 = { 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 ? : null} - ¥ {formatAmount(order.payAmount)} + + ¥ {formatAmount(order.payAmount)} + {order.items || '—'} - {formatHm(order.createdAt)} + {formatOrderTime(order.paidAt ?? order.createdAt)} {order.userPhoneMasked || '—'} ); @@ -253,11 +351,13 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v {tier >= 2 ?
: null}
{tier === 3 ? '高额成交' : tier === 2 ? '大额成交' : '新成交'}
-
¥ {formatAmount(displayAmount)}
+
+ ¥ {formatAmount(displayAmount)} +
{order.items || '—'}
{order.userPhoneMasked || '—'} - {formatHm(order.createdAt)} + {formatOrderTime(order.paidAt ?? order.createdAt)}
@@ -270,12 +370,16 @@ export default function BigScreenPage() { const [paused, setPaused] = useState(false); const [celebrate, setCelebrate] = useState(null); const seenIdsRef = useRef | null>(null); + const celebratedIdsRef = useRef>(new Set()); const queueRef = useRef([]); const celebratingRef = useRef(false); const viewportRef = useRef(null); - const demoSeenRef = useRef>(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) => { - 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 (
@@ -431,7 +499,7 @@ export default function BigScreenPage() {
下单金额 下单商品 - 下单时间 + 付款时间 下单人
diff --git a/apps/admin-web/src/pages/OrdersPage.tsx b/apps/admin-web/src/pages/OrdersPage.tsx index 7dfbf82..3282636 100644 --- a/apps/admin-web/src/pages/OrdersPage.tsx +++ b/apps/admin-web/src/pages/OrdersPage.tsx @@ -21,7 +21,6 @@ import { } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { request, type AdminOrderItem, type AdminOrderRow, type HqProfile, type Paginated } from '../lib/api'; -import { triggerBigScreenDemo } from '../lib/admin-events'; import { ADMIN_OPTIONS_PAGE_SIZE, DELIVERY_PROVIDER_LABELS, @@ -192,7 +191,6 @@ export default function OrdersPage() { const [trackOpen, setTrackOpen] = useState(false); const canDeleteOrders = (profile?.permissionKeys ?? []).includes('orders_delete'); const canProxyOrder = (profile?.permissionKeys ?? []).includes('orders'); - const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN'; const selectedOrders = useMemo( () => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)), @@ -545,17 +543,6 @@ export default function OrdersPage() { 订单监控 - {isSuperAdmin ? ( - - ) : null} {canProxyOrder ? (