Merge pull request 'v3.5.2 大屏调整' (#23) from dev into main
CI / verify (push) Has been cancelled
CI / verify (push) Has been cancelled
Reviewed-on: https://git.yqidian.com/jacy/dukang/pulls/23
This commit was merged in pull request #23.
This commit is contained in:
@@ -291,18 +291,21 @@ body:has(.big-screen-page),
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 8px;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.big-screen-list-head,
|
.big-screen-list-head,
|
||||||
.big-screen-row {
|
.big-screen-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 208px 1fr 160px 200px;
|
grid-template-columns: minmax(180px, 18%) minmax(0, 1fr) minmax(140px, 16%) minmax(160px, 18%);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 16px;
|
column-gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.big-screen-list-head {
|
.big-screen-list-head {
|
||||||
@@ -457,10 +460,16 @@ body:has(.big-screen-page),
|
|||||||
}
|
}
|
||||||
|
|
||||||
.big-screen-empty {
|
.big-screen-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: rgba(255, 255, 255, 0.45);
|
color: rgba(145, 190, 230, 0.42);
|
||||||
padding: 48px 20px;
|
padding: 48px 40px;
|
||||||
font-size: 18px;
|
font-size: 60px;
|
||||||
|
letter-spacing: 6px;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.big-screen-fx {
|
.big-screen-fx {
|
||||||
@@ -715,7 +724,7 @@ body:has(.big-screen-page),
|
|||||||
}
|
}
|
||||||
.big-screen-list-head,
|
.big-screen-list-head,
|
||||||
.big-screen-row {
|
.big-screen-row {
|
||||||
grid-template-columns: 172px 1fr 120px 160px;
|
grid-template-columns: minmax(140px, 20%) minmax(0, 1fr) minmax(110px, 16%) minmax(130px, 18%);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.big-screen-amount--6d {
|
.big-screen-amount--6d {
|
||||||
|
|||||||
@@ -16,103 +16,10 @@ type AmountTier = 1 | 2 | 3;
|
|||||||
|
|
||||||
const POLL_MS = 3000;
|
const POLL_MS = 3000;
|
||||||
const ROW_MS = 2500;
|
const ROW_MS = 2500;
|
||||||
|
/** 不足此条数时重复填充,保证滚动连贯 */
|
||||||
|
const MIN_SCROLL_ROWS = 10;
|
||||||
|
const FX_DURATION_MS = 10_000;
|
||||||
const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
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) {
|
function pad(n: number) {
|
||||||
return String(n).padStart(2, '0');
|
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);
|
const [displayAmount, setDisplayAmount] = useState(tier === 3 ? 0 : order.payAmount);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const duration = TIER_DURATION[tier];
|
const timer = window.setTimeout(() => onDoneRef.current(), FX_DURATION_MS);
|
||||||
const timer = window.setTimeout(() => onDoneRef.current(), duration);
|
|
||||||
return () => window.clearTimeout(timer);
|
return () => window.clearTimeout(timer);
|
||||||
}, [order.id, tier]);
|
}, [order.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tier !== 3) return;
|
if (tier !== 3) return;
|
||||||
@@ -299,7 +205,6 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
|
|||||||
let raf = 0;
|
let raf = 0;
|
||||||
let last = performance.now();
|
let last = performance.now();
|
||||||
const started = performance.now();
|
const started = performance.now();
|
||||||
const duration = TIER_DURATION[tier];
|
|
||||||
const gravity = tier === 3 ? 0.18 : 0.14;
|
const gravity = tier === 3 ? 0.18 : 0.14;
|
||||||
const loop = (now: number) => {
|
const loop = (now: number) => {
|
||||||
const dt = Math.min(32, now - last) / 16.6;
|
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.x += p.vx * dt;
|
||||||
p.y += p.vy * dt;
|
p.y += p.vy * dt;
|
||||||
p.rot += p.vr * dt;
|
p.rot += p.vr * dt;
|
||||||
p.life -= (tier === 3 ? 0.0032 : 0.005) * dt;
|
p.life -= (tier === 3 ? 0.002 : 0.003) * dt;
|
||||||
if (p.rain && (p.y > canvas.clientHeight + 30 || p.life <= 0) && now - started < duration - 400) {
|
if (
|
||||||
|
p.rain &&
|
||||||
|
(p.y > canvas.clientHeight + 30 || p.life <= 0) &&
|
||||||
|
now - started < FX_DURATION_MS - 400
|
||||||
|
) {
|
||||||
p.x = Math.random() * canvas.clientWidth;
|
p.x = Math.random() * canvas.clientWidth;
|
||||||
p.y = -16 - Math.random() * 60;
|
p.y = -16 - Math.random() * 60;
|
||||||
p.vx = (Math.random() - 0.5) * 1.4;
|
p.vx = (Math.random() - 0.5) * 1.4;
|
||||||
@@ -374,12 +283,9 @@ export default function BigScreenPage() {
|
|||||||
const queueRef = useRef<BigScreenOrder[]>([]);
|
const queueRef = useRef<BigScreenOrder[]>([]);
|
||||||
const celebratingRef = useRef(false);
|
const celebratingRef = useRef(false);
|
||||||
const viewportRef = useRef<HTMLDivElement | null>(null);
|
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const dayKeyRef = useRef('');
|
||||||
const [viewportH, setViewportH] = useState(0);
|
const [viewportH, setViewportH] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
for (const o of BIG_SCREEN_SEED_ORDERS) celebratedIdsRef.current.add(o.id);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const playNext = useCallback(() => {
|
const playNext = useCallback(() => {
|
||||||
const next = queueRef.current.shift() ?? null;
|
const next = queueRef.current.shift() ?? null;
|
||||||
celebratingRef.current = !!next;
|
celebratingRef.current = !!next;
|
||||||
@@ -404,10 +310,22 @@ export default function BigScreenPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fetchData = useCallback(() => {
|
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')
|
return request<{ items: BigScreenOrder[] }>('/admin/orders/big-screen?limit=2000')
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
const list = d.items ?? [];
|
const list = d.items ?? [];
|
||||||
setItems(mergeDisplayItems(list));
|
setItems(list);
|
||||||
setLoadError('');
|
setLoadError('');
|
||||||
const seen = seenIdsRef.current;
|
const seen = seenIdsRef.current;
|
||||||
if (!seen) {
|
if (!seen) {
|
||||||
@@ -421,7 +339,6 @@ export default function BigScreenPage() {
|
|||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
setLoadError(e instanceof Error ? e.message : '加载失败');
|
setLoadError(e instanceof Error ? e.message : '加载失败');
|
||||||
setItems(mergeDisplayItems([]));
|
|
||||||
});
|
});
|
||||||
}, [enqueueNew]);
|
}, [enqueueNew]);
|
||||||
|
|
||||||
@@ -457,7 +374,7 @@ export default function BigScreenPage() {
|
|||||||
if (!items.length) return [];
|
if (!items.length) return [];
|
||||||
const rowH = 80;
|
const rowH = 80;
|
||||||
const visible = Math.max(1, Math.ceil((viewportH || 480) / rowH));
|
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[] = [];
|
const unit: BigScreenOrder[] = [];
|
||||||
while (unit.length < minCount) unit.push(...items);
|
while (unit.length < minCount) unit.push(...items);
|
||||||
return unit;
|
return unit;
|
||||||
@@ -466,8 +383,7 @@ export default function BigScreenPage() {
|
|||||||
const trackItems = useMemo(() => unitItems.concat(unitItems), [unitItems]);
|
const trackItems = useMemo(() => unitItems.concat(unitItems), [unitItems]);
|
||||||
const marqueeMs = Math.max(unitItems.length, 1) * ROW_MS;
|
const marqueeMs = Math.max(unitItems.length, 1) * ROW_MS;
|
||||||
const rolling = items.length > 0 && unitItems.length > 0;
|
const rolling = items.length > 0 && unitItems.length > 0;
|
||||||
const latestLive = items.find((o) => !isSeedOrder(o));
|
const latestId = items[0]?.id;
|
||||||
const latestId = latestLive?.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="big-screen-page">
|
<div className="big-screen-page">
|
||||||
@@ -497,14 +413,14 @@ export default function BigScreenPage() {
|
|||||||
|
|
||||||
<div className="big-screen-list">
|
<div className="big-screen-list">
|
||||||
<div className="big-screen-list-head">
|
<div className="big-screen-list-head">
|
||||||
<span>下单金额</span>
|
<span>订单金额</span>
|
||||||
<span>下单商品</span>
|
<span>订单商品</span>
|
||||||
<span>付款时间</span>
|
<span>下单时间</span>
|
||||||
<span>下单人</span>
|
<span>下单账号</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="big-screen-list-body" ref={viewportRef}>
|
<div className="big-screen-list-body" ref={viewportRef}>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<div className="big-screen-empty">{loadError || '暂无订单'}</div>
|
<div className="big-screen-empty">{loadError || '当日暂无订单'}</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`big-screen-track${rolling ? ' is-rolling' : ''}${paused ? ' is-paused' : ''}`}
|
className={`big-screen-track${rolling ? ' is-rolling' : ''}${paused ? ' is-paused' : ''}`}
|
||||||
|
|||||||
@@ -25,13 +25,16 @@ export class AdminOrdersService {
|
|||||||
private readonly adminRedeemService: AdminRedeemService,
|
private readonly adminRedeemService: AdminRedeemService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** v3.5.1 #1:发布会大屏,仅已付款订单;按付款时间倒序 */
|
/** v3.5.1 #1:发布会大屏,仅当日已付款订单;按付款时间倒序 */
|
||||||
async listBigScreen(limit?: string) {
|
async listBigScreen(limit?: string) {
|
||||||
const take = Math.min(Math.max(Number(limit) || 2000, 1), 2000);
|
const take = Math.min(Math.max(Number(limit) || 2000, 1), 2000);
|
||||||
|
const startOfToday = new Date();
|
||||||
|
startOfToday.setHours(0, 0, 0, 0);
|
||||||
const orders = await this.prisma.order.findMany({
|
const orders = await this.prisma.order.findMany({
|
||||||
where: {
|
where: {
|
||||||
payAmount: { gte: 100 },
|
payAmount: { gte: 100 },
|
||||||
payStatus: 'PAID',
|
payStatus: 'PAID',
|
||||||
|
paidAt: { gte: startOfToday },
|
||||||
},
|
},
|
||||||
orderBy: [{ paidAt: 'desc' }, { createdAt: 'desc' }],
|
orderBy: [{ paidAt: 'desc' }, { createdAt: 'desc' }],
|
||||||
take,
|
take,
|
||||||
|
|||||||
Reference in New Issue
Block a user