统一单项目大屏与管理端全屏布局
需求:/display/{项目编码} 参照管理端项目大屏布局展示。
实现:抽取共享大屏组件,补充公开累计取号与最新号码字段,并完善桌面及窄屏适配。
验证:前端61项测试、TypeScript、Vite构建以及Go测试、vet、build均通过。
This commit is contained in:
88
web/src/components/ProjectScreenTile.tsx
Normal file
88
web/src/components/ProjectScreenTile.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { currentBatchNumbers, forecastRows } from "../lib/display";
|
||||
import type { AdminProjectDto, CallBatchDto } from "../types";
|
||||
|
||||
interface ProjectScreenTileProps {
|
||||
project: AdminProjectDto;
|
||||
standalone?: boolean;
|
||||
onFullscreen?: (projectId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ProjectScreenTile({ project, standalone = false, onFullscreen }: ProjectScreenTileProps) {
|
||||
const current = project.current_batch && typeof project.current_batch === "object"
|
||||
? project.current_batch as CallBatchDto
|
||||
: null;
|
||||
const waitingTicketCount = project.waiting_ticket_count ?? project.waiting_count ?? 0;
|
||||
const waitingPeopleCount = project.waiting_people_count ?? 0;
|
||||
const forecasts = forecastRows(current, waitingTicketCount, project.estimated_wait);
|
||||
const pageCount = Math.max(1, Math.ceil(forecasts.length / 4));
|
||||
const [page, setPage] = useState(0);
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
|
||||
useEffect(() => {
|
||||
setPage((value) => Math.min(value, pageCount - 1));
|
||||
if (pageCount <= 1) return undefined;
|
||||
const timer = window.setInterval(() => setPage((value) => (value + 1) % pageCount), 6_000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [pageCount]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => setNow(new Date()), 1_000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const visible = forecasts.slice(page * 4, (page + 1) * 4);
|
||||
const latestTicketNumber = project.latest_ticket_number
|
||||
?? forecasts.at(-1)?.range.split(" 至 ").at(-1)
|
||||
?? "暂无";
|
||||
const issuedTicketCount = project.issued_ticket_count ?? Number(latestTicketNumber.match(/\d+$/)?.[0] ?? 0);
|
||||
const experiencedPeople = project.experienced_people ?? project.experienced_people_start ?? 0;
|
||||
const dateLabel = new Intl.DateTimeFormat("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
weekday: "long",
|
||||
}).format(now);
|
||||
const timeLabel = new Intl.DateTimeFormat("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
}).format(now);
|
||||
|
||||
return (
|
||||
<article className={`screen-tile${standalone ? " screen-tile--standalone" : ""}`} data-project-screen={project.id}>
|
||||
<header className="screen-tile__header">
|
||||
<div className="screen-tile__clock" aria-label={`当前时间 ${dateLabel} ${timeLabel}`}>
|
||||
<img className="screen-tile__logo" src="/xiaoqikong-logo.jpg" alt="" aria-hidden="true" />
|
||||
<div className="screen-tile__clock-copy">
|
||||
<time dateTime={now.toISOString()}>{timeLabel}</time>
|
||||
<span>{dateLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="screen-tile__project">
|
||||
<h2>{project.name}</h2>
|
||||
</div>
|
||||
{!standalone && onFullscreen ? (
|
||||
<button className="button button--primary screen-tile__fullscreen" onClick={() => void onFullscreen(project.id)}>全屏展示</button>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="screen-tile__current">
|
||||
<strong>{currentBatchNumbers(project.current_batch)}</strong>
|
||||
<small>{project.current_batch ? "当前叫号" : "等待下一次叫号"}</small>
|
||||
<div className="screen-tile__current-metrics">
|
||||
<h3 className="screen-tile__metric" aria-label={`最新取号码:${latestTicketNumber}${latestTicketNumber === "暂无" ? "" : "号"}`}><span>最新取号码:</span><strong>{latestTicketNumber}</strong>{latestTicketNumber === "暂无" ? null : <small>号</small>}</h3>
|
||||
<div className="screen-tile__metric" aria-label={`累计取号数:${issuedTicketCount} 个`}><span>累计取号数:</span><strong>{issuedTicketCount}</strong><small>个</small></div>
|
||||
<div className="screen-tile__metric" aria-label={`累计等待人数:${waitingPeopleCount} 人`}><span>累计等待人数:</span><strong>{waitingPeopleCount}</strong><small>人</small></div>
|
||||
<div className="screen-tile__metric" aria-label={`已体验人数:${experiencedPeople} 人`}><span>已体验人数:</span><strong>{experiencedPeople}</strong><small>人</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<section className="screen-tile__forecast" aria-label={`${project.name}最新取号信息`}>
|
||||
<div className="screen-tile__forecast-list-heading" aria-hidden="true"><span>后续号段</span><span>预计等待时间</span></div>
|
||||
{visible.length ? (
|
||||
<ol>{visible.map((item) => <li key={item.range}><strong>{item.range}</strong><span>{item.wait}</span></li>)}</ol>
|
||||
) : <p>后续区间暂不可估算</p>}
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user