
@@ -63,7 +65,9 @@ export function ProjectScreenTile({ project, standalone = false, onFullscreen }:
{project.name}
- {!standalone && onFullscreen ? (
+ {displayHref ? (
+
进入项目大屏
+ ) : !standalone && onFullscreen ? (
) : null}
@@ -83,6 +87,21 @@ export function ProjectScreenTile({ project, standalone = false, onFullscreen }:
{visible.map((item) => - {item.range}{item.wait}
)}
) :
后续区间暂不可估算
}
-
+ >
);
+
+ if (displayHref) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return
{content};
}
diff --git a/web/src/hooks/useSpeechAnnouncements.test.ts b/web/src/hooks/useSpeechAnnouncements.test.ts
deleted file mode 100644
index ce31a0f..0000000
--- a/web/src/hooks/useSpeechAnnouncements.test.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { act, renderHook } from "@testing-library/react";
-import { afterEach, describe, expect, it, vi } from "vitest";
-import { useSpeechAnnouncements } from "./useSpeechAnnouncements";
-
-class SpeechSynthesisUtteranceMock {
- lang = "";
- rate = 1;
-
- constructor(public text: string) {}
-}
-
-describe("useSpeechAnnouncements", () => {
- afterEach(() => {
- vi.restoreAllMocks();
- vi.unstubAllGlobals();
- });
-
- it("把叫号文本用中文语音连续播报三次", () => {
- const speak = vi.fn();
- const cancel = vi.fn();
- vi.stubGlobal("SpeechSynthesisUtterance", SpeechSynthesisUtteranceMock);
- vi.stubGlobal("speechSynthesis", { speak, cancel, paused: false, resume: vi.fn() });
- const { result } = renderHook(() => useSpeechAnnouncements());
-
- act(() => result.current.announce(["请零零零一六号,前往入口。"]));
-
- expect(cancel).toHaveBeenCalledOnce();
- expect(speak).toHaveBeenCalledTimes(3);
- for (const [utterance] of speak.mock.calls) {
- expect(utterance).toMatchObject({
- text: "请零零零一六号,前往入口。",
- lang: "zh-CN",
- rate: 0.9,
- });
- }
- });
-
- it("浏览器不支持语音合成时安全跳过", () => {
- const { result } = renderHook(() => useSpeechAnnouncements());
-
- expect(() => result.current.announce(["请一号,前往入口。"])).not.toThrow();
- });
-});
diff --git a/web/src/hooks/useSpeechAnnouncements.ts b/web/src/hooks/useSpeechAnnouncements.ts
deleted file mode 100644
index 474eab9..0000000
--- a/web/src/hooks/useSpeechAnnouncements.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useCallback } from "react";
-
-const ANNOUNCEMENT_REPEAT_COUNT = 3;
-
-function speechSupported(): boolean {
- return typeof window !== "undefined"
- && typeof window.speechSynthesis?.speak === "function"
- && typeof SpeechSynthesisUtterance !== "undefined";
-}
-
-function createUtterance(text: string): SpeechSynthesisUtterance {
- const utterance = new SpeechSynthesisUtterance(text);
- utterance.lang = "zh-CN";
- utterance.rate = 0.9;
- return utterance;
-}
-
-export function useSpeechAnnouncements() {
- const announce = useCallback((announcements: string[]) => {
- if (!speechSupported() || !announcements.length) return;
-
- const synthesis = window.speechSynthesis;
- synthesis.cancel();
- if (synthesis.paused) synthesis.resume();
- for (const announcement of announcements) {
- for (let repeat = 0; repeat < ANNOUNCEMENT_REPEAT_COUNT; repeat += 1) {
- synthesis.speak(createUtterance(announcement));
- }
- }
- }, []);
-
- return { announce };
-}
diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts
index e671ce2..ec0a269 100644
--- a/web/src/lib/format.ts
+++ b/web/src/lib/format.ts
@@ -1,18 +1,6 @@
import type { CallBatchDto, ProjectStatus, TicketStatus } from "../types";
const numberFormatter = new Intl.NumberFormat("zh-CN");
-const SPOKEN_DIGITS: Record
= {
- "0": "零",
- "1": "一",
- "2": "二",
- "3": "三",
- "4": "四",
- "5": "五",
- "6": "六",
- "7": "七",
- "8": "八",
- "9": "九",
-};
export function formatNumber(value: unknown, fallback = "暂无"): string {
const number = typeof value === "number" ? value : Number(value);
@@ -119,16 +107,6 @@ export function formatTicketNumberRange(numbers: Array ticket.ticket_number.trim()).filter(Boolean) ?? [];
- if (!numbers.length) return null;
- const spokenNumber = (number: string) => Array.from(number, (character) => SPOKEN_DIGITS[character] ?? character).join("");
- const calledNumbers = numbers.length === 1
- ? `${spokenNumber(numbers[0])}号`
- : `${spokenNumber(numbers[0])}号至${spokenNumber(numbers[numbers.length - 1])}号`;
- return `请${calledNumbers},前往入口。`;
-}
-
export function isTimestampStale(value?: string | null, thresholdMs = 30_000, now = Date.now()): boolean {
if (!value) return true;
const timestamp = new Date(value).getTime();
diff --git a/web/src/pages/AdminPage.tsx b/web/src/pages/AdminPage.tsx
index f2119df..0a88298 100644
--- a/web/src/pages/AdminPage.tsx
+++ b/web/src/pages/AdminPage.tsx
@@ -344,7 +344,12 @@ function ProjectMaintenance({ project, onRefresh }: { project?: AdminProjectDto;
;
}
-export function DisplayCenter({ projects }: { projects: AdminProjectDto[] }) {
+interface DisplayCenterProps {
+ projects: AdminProjectDto[];
+ projectHref?: (project: AdminProjectDto) => string | undefined;
+}
+
+export function DisplayCenter({ projects, projectHref }: DisplayCenterProps) {
const [fullscreenError, setFullscreenError] = useState(false);
const enterFullscreen = async (projectId: string) => {
const tile = document.querySelector(`[data-project-screen="${projectId}"]`);
@@ -365,7 +370,14 @@ export function DisplayCenter({ projects }: { projects: AdminProjectDto[] }) {
{fullscreenError ?