Files
XQKqueue/web/src/pages/PublicDisplayCenterPage.tsx
brother7 9d56c181fb 修复:补齐多项目公屏叫号播报
需求:管理员公屏在员工叫号后需播放项目名与号码。
实现:复用单项目号码播报格式,按项目跟踪新批次并播报项目名加号码,补充回归测试。
2026-07-31 17:14:33 +08:00

76 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef } from "react";
import { api } from "../api";
import { FeedbackBanner, FreshnessBanner, LoadingState } from "../components/Feedback";
import { usePollingResource } from "../hooks/usePollingResource";
import { formatCallAnnouncement, isTimestampStale } from "../lib/format";
import type { CallBatchDto } from "../types";
import { DisplayCenter } from "./AdminPage";
function projectCurrentBatch(value: unknown): CallBatchDto | null {
return value && typeof value === "object" ? value as CallBatchDto : null;
}
export function PublicDisplayCenterPage() {
const resource = usePollingResource((signal) => api.displayOverview(signal), { intervalMs: 5_000 });
const data = resource.data;
const stale = Boolean(data) && isTimestampStale(resource.lastClientSuccessAt, 30_000);
const lastCallsRef = useRef<Map<string, string | null> | null>(null);
useEffect(() => {
if (!data) return;
const previousCalls = lastCallsRef.current;
const nextCalls = new Map<string, string | null>();
const announcements: string[] = [];
for (const project of data.projects) {
const batch = projectCurrentBatch(project.current_batch);
const key = batch ? `${batch.batch_number}:${batch.called_at ?? ""}` : null;
nextCalls.set(project.id, key);
if (!previousCalls?.has(project.id) || !key || previousCalls.get(project.id) === key) continue;
const announcement = formatCallAnnouncement(batch);
if (announcement) announcements.push(`${project.name}${announcement}`);
}
lastCallsRef.current = nextCalls;
if (
!announcements.length
|| !("speechSynthesis" in window)
|| typeof SpeechSynthesisUtterance === "undefined"
) return;
window.speechSynthesis.cancel();
for (const announcement of announcements) {
const utterance = new SpeechSynthesisUtterance(announcement);
utterance.lang = "zh-CN";
utterance.rate = 0.9;
window.speechSynthesis.speak(utterance);
}
}, [data]);
return (
<div className="app-shell app-shell--admin app-shell--no-primary-action">
<a className="skip-link" href="#main-content"></a>
<header className="app-header">
<div className="brand-lockup" aria-label="景区排队叫号系统">
<span className="brand-logo-frame" aria-hidden="true"><img className="brand-logo" src="/xiaoqikong-logo.jpg" alt="" /></span>
<span className="brand-lockup__text"><strong></strong></span>
</div>
<div className="account-menu"><span><strong></strong></span></div>
</header>
<main id="main-content" className="app-main">
<section className="admin-content" aria-label="公开大屏内容">
{resource.loading && !data ? <LoadingState label="正在汇总项目状态" /> : null}
{resource.error && !data ? (
<FeedbackBanner tone="danger" title="大屏数据暂时不可用" action={<button className="button button--secondary button--small" onClick={resource.refresh}></button>}>
{resource.error.message}
</FeedbackBanner>
) : null}
{data ? (
<>
<FreshnessBanner offline={resource.offline} stale={stale} timestamp={resource.lastClientSuccessAt} refreshing={resource.refreshing} errorMessage={resource.error?.message} onRetry={resource.refresh} />
<DisplayCenter projects={data.projects} />
</>
) : null}
</section>
</main>
</div>
);
}