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 | null>(null); useEffect(() => { if (!data) return; const previousCalls = lastCallsRef.current; const nextCalls = new Map(); 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 (
跳到主要内容
景区排队叫号系统
大屏中心
{resource.loading && !data ? : null} {resource.error && !data ? ( 重试}> {resource.error.message} ) : null} {data ? ( <> ) : null}
); }