@@ -16,103 +16,10 @@ type AmountTier = 1 | 2 | 3;
const POLL_MS = 3000 ;
const ROW_MS = 2500 ;
/** 不足此条数时重复填充,保证滚动连贯 */
const MIN_SCROLL_ROWS = 10 ;
const FX_DURATION_MS = 10 _000 ;
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' ) ;
@@ -264,10 +171,9 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
const [ displayAmount , setDisplayAmount ] = useState ( tier === 3 ? 0 : order.payAmount ) ;
useEffect ( ( ) = > {
const duration = TIER_DURATION [ tier ] ;
const timer = window . setTimeout ( ( ) = > onDoneRef . current ( ) , duration ) ;
const timer = window . setTimeout ( ( ) = > onDoneRef . current ( ) , FX_DURATION_MS ) ;
return ( ) = > window . clearTimeout ( timer ) ;
} , [ order . id , tier ] ) ;
} , [ order . id ] ) ;
useEffect ( ( ) = > {
if ( tier !== 3 ) return ;
@@ -299,7 +205,6 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
let raf = 0 ;
let last = performance . now ( ) ;
const started = performance . now ( ) ;
const duration = TIER_DURATION [ tier ] ;
const gravity = tier === 3 ? 0.18 : 0.14 ;
const loop = ( now : number ) = > {
const dt = Math . min ( 32 , now - last ) / 16.6 ;
@@ -310,8 +215,12 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
p . x += p . vx * dt ;
p . y += p . vy * dt ;
p . rot += p . vr * dt ;
p . life -= ( tier === 3 ? 0.0032 : 0.005 ) * dt ;
if ( p . rain && ( p . y > canvas . clientHeight + 30 || p . life <= 0 ) && now - started < duration - 400 ) {
p . life -= ( tier === 3 ? 0.002 : 0.003 ) * dt ;
if (
p . rain &&
( p . y > canvas . clientHeight + 30 || p . life <= 0 ) &&
now - started < FX_DURATION_MS - 400
) {
p . x = Math . random ( ) * canvas . clientWidth ;
p . y = - 16 - Math . random ( ) * 60 ;
p . vx = ( Math . random ( ) - 0.5 ) * 1.4 ;
@@ -374,12 +283,9 @@ export default function BigScreenPage() {
const queueRef = useRef < BigScreenOrder [ ] > ( [ ] ) ;
const celebratingRef = useRef ( false ) ;
const viewportRef = useRef < HTMLDivElement | null > ( null ) ;
const dayKeyRef = useRef ( '' ) ;
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 ;
@@ -404,10 +310,22 @@ export default function BigScreenPage() {
) ;
const fetchData = useCallback ( ( ) = > {
const now = new Date ( ) ;
const todayKey = ` ${ now . getFullYear ( ) } - ${ pad ( now . getMonth ( ) + 1 ) } - ${ pad ( now . getDate ( ) ) } ` ;
if ( dayKeyRef . current && dayKeyRef . current !== todayKey ) {
seenIdsRef . current = null ;
celebratedIdsRef . current = new Set ( ) ;
queueRef . current = [ ] ;
celebratingRef . current = false ;
setCelebrate ( null ) ;
setPaused ( false ) ;
}
dayKeyRef . current = todayKey ;
return request < { items : BigScreenOrder [ ] } > ( '/admin/orders/big-screen?limit=2000' )
. then ( ( d ) = > {
const list = d . items ? ? [ ] ;
setItems ( mergeDisplayItems ( list ) ) ;
setItems ( list ) ;
setLoadError ( '' ) ;
const seen = seenIdsRef . current ;
if ( ! seen ) {
@@ -421,7 +339,6 @@ export default function BigScreenPage() {
} )
. catch ( ( e ) = > {
setLoadError ( e instanceof Error ? e . message : '加载失败' ) ;
setItems ( mergeDisplayItems ( [ ] ) ) ;
} ) ;
} , [ enqueueNew ] ) ;
@@ -457,7 +374,7 @@ export default function BigScreenPage() {
if ( ! items . length ) return [ ] ;
const rowH = 80 ;
const visible = Math . max ( 1 , Math . ceil ( ( viewportH || 480 ) / rowH ) ) ;
const minCount = visible + 1 ;
const minCount = Math . max ( MIN_SCROLL_ROWS , visible + 1 ) ;
const unit : BigScreenOrder [ ] = [ ] ;
while ( unit . length < minCount ) unit . push ( . . . items ) ;
return unit ;
@@ -466,8 +383,7 @@ 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 latestLive = items . find ( ( o ) = > ! isSeedOrder ( o ) ) ;
const latestId = latestLive ? . id ;
const latestId = items [ 0 ] ? . id ;
return (
< div className = "big-screen-page" >
@@ -497,14 +413,14 @@ export default function BigScreenPage() {
< div className = "big-screen-list" >
< div className = "big-screen-list-head" >
< span > 下 单 金 额 < / span >
< span > 下 单 商 品 < / span >
< span > 付 款 时 间 < / span >
< span > 下 单 人 < / span >
< span > 订 单 金 额 < / span >
< span > 订 单 商 品 < / span >
< span > 下 单 时 间 < / span >
< span > 下 单 账 号 < / span >
< / div >
< div className = "big-screen-list-body" ref = { viewportRef } >
{ items . length === 0 ? (
< div className = "big-screen-empty" > { loadError || '暂无订单' } < / div >
< div className = "big-screen-empty" > { loadError || '当日 暂无订单' } < / div >
) : (
< div
className = { ` big-screen-track ${ rolling ? ' is-rolling' : '' } ${ paused ? ' is-paused' : '' } ` }