import { ref, type Ref } from 'vue' import type { AnnouncementState, DisplayPageKey } from './types' /** * 公告状态机:把「全屏推送」与「底栏常驻」拆开。 * * 公告一旦推送(或由服务端持久化的设置恢复)就留在本地,全屏只是 seconds 秒的临时态; * 计时结束后只退出全屏、回到原页面,正文继续给底栏用,直到下一条公告替换它或被清除。 */ /** 退出全屏后回到的页:没记下「进入公告前那一页」时用总积分榜兜底。 */ const DEFAULT_RETURN_PAGE: DisplayPageKey = 'standings' /** * 凭服务端持久化的公告恢复全屏时的时长。 * 设置里只存正文与推送时长两项,而契约只下发正文,所以这里给一个默认值。 */ export const DEFAULT_ANNOUNCE_SECONDS = 15 /** 没有任何公告时槽位显示的占位文案。 */ const ANNOUNCE_PLACEHOLDER = '暂无公告' export interface AnnouncementDeps { /** 当前页;公告全屏期间是 announcement。 */ page: Ref /** 进入公告全屏页(调用方负责停轮播)。 */ onFullscreenEnter: () => void /** 退出全屏并回到 returnPage(调用方负责恢复轮播)。 */ onFullscreenExit: (returnPage: DisplayPageKey) => void } export interface AnnouncementController { /** 当前公告;底栏与公告页都读它。null = 没有公告。 */ current: Ref /** 推送一条公告:先全屏 seconds 秒,结束后只退全屏,正文留着。 */ push: (text: string, seconds: number) => void /** 清除公告:退全屏并清空正文。 */ clear: () => void /** 组件卸载时停表。 */ dispose: () => void } export function createAnnouncement(deps: AnnouncementDeps): AnnouncementController { const current = ref(null) let timer: ReturnType | null = null // 第二条公告到来时 page 已经是 announcement,必须复用「进入公告前的真实页」, // 否则返回页会被记成公告页自身,计时结束后永远停在公告页。 let returnPage: DisplayPageKey | null = null function stopTimer(): void { if (timer !== null) clearTimeout(timer) timer = null } function leaveFullscreen(): void { stopTimer() const target = returnPage ?? DEFAULT_RETURN_PAGE returnPage = null deps.onFullscreenExit(target) } function push(text: string, seconds: number): void { const trimmed = text.trim() if (trimmed === '') return if (deps.page.value !== 'announcement') returnPage = deps.page.value current.value = { text: trimmed, seconds, startedAt: Date.now() } deps.onFullscreenEnter() stopTimer() // seconds <= 0 表示不定时全屏,只能由下一条公告或 clear 收场。 if (seconds > 0) timer = setTimeout(leaveFullscreen, seconds * 1000) } function clear(): void { // 全屏还停在公告页时要连全屏一起收掉;已经回到普通页时只清底栏正文。 const fullscreen = current.value !== null && deps.page.value === 'announcement' stopTimer() current.value = null if (fullscreen) leaveFullscreen() } function dispose(): void { stopTimer() returnPage = null } return { current, push, clear, dispose } } /** * 公告取值:推送中的实时值优先,其次是服务端持久化的当前公告 * (断线重连、新开的大屏只有它),两者都没有才回落占位。 */ export function resolveAnnounceText( current: AnnouncementState | null, persisted: string | null | undefined, ): string { const live = current?.text.trim() ?? '' if (live !== '') return live const saved = persisted?.trim() ?? '' return saved === '' ? ANNOUNCE_PLACEHOLDER : saved }