From 6279b8ac32d2a12a243b292a4d1b14bc098802f5 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 17 Jun 2026 02:18:49 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=99=E7=BB=88=E7=AB=AF=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=85=AC=E5=91=8A=E5=8A=A0=E6=9C=AA=E8=AF=BB=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UpdateAnnouncementButton.tsx | 68 ++++++++++++++++++- .../__tests__/updateAnnouncement.test.ts | 9 +++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/UpdateAnnouncementButton.tsx b/frontend/components/dashboard/scan-terminal/UpdateAnnouncementButton.tsx index 09940dab..2b0c5a67 100644 --- a/frontend/components/dashboard/scan-terminal/UpdateAnnouncementButton.tsx +++ b/frontend/components/dashboard/scan-terminal/UpdateAnnouncementButton.tsx @@ -20,6 +20,8 @@ type UpdateAnnouncementButtonProps = { isEn: boolean; }; +const UPDATE_ANNOUNCEMENT_SEEN_KEY = "polyweather_update_announcement_seen_v1"; + const STATIC_UPDATE_ANNOUNCEMENTS: StaticUpdateAnnouncement[] = [ { id: "live-observation-chart-2026-06", @@ -71,10 +73,40 @@ function formatUpdatedAt(value: string | undefined, isEn: boolean) { }); } +function loadSeenAnnouncementIds() { + if (typeof window === "undefined") return new Set(); + try { + const raw = window.localStorage.getItem(UPDATE_ANNOUNCEMENT_SEEN_KEY); + const parsed = raw ? JSON.parse(raw) : []; + return new Set(Array.isArray(parsed) ? parsed.map(String) : []); + } catch { + return new Set(); + } +} + +function saveSeenAnnouncementIds(ids: Set) { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem( + UPDATE_ANNOUNCEMENT_SEEN_KEY, + JSON.stringify(Array.from(ids).slice(-50)), + ); + } catch { + // Ignore storage failures; the unread dot may reappear but the announcement remains usable. + } +} + export function UpdateAnnouncementButton({ isEn }: UpdateAnnouncementButtonProps) { const [open, setOpen] = useState(false); + const [seenIds, setSeenIds] = useState>(new Set()); + const [seenLoaded, setSeenLoaded] = useState(false); const shellRef = useRef(null); + useEffect(() => { + setSeenIds(loadSeenAnnouncementIds()); + setSeenLoaded(true); + }, []); + useEffect(() => { if (!open) return; const handlePointerDown = (event: PointerEvent) => { @@ -98,6 +130,17 @@ export function UpdateAnnouncementButton({ isEn }: UpdateAnnouncementButtonProps () => formatUpdatedAt(announcement?.publishedAt, isEn), [announcement?.publishedAt, isEn], ); + const hasUnread = Boolean(seenLoaded && announcement?.id && !seenIds.has(announcement.id)); + + const markAnnouncementAsSeen = (announcementId: string) => { + setSeenIds((current) => { + if (current.has(announcementId)) return current; + const next = new Set(current); + next.add(announcementId); + saveSeenAnnouncementIds(next); + return next; + }); + }; if (!announcement || (!text.title && !text.body)) return null; @@ -105,13 +148,34 @@ export function UpdateAnnouncementButton({ isEn }: UpdateAnnouncementButtonProps
{open && (
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/updateAnnouncement.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/updateAnnouncement.test.ts index 06ef73e6..d787d93d 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/updateAnnouncement.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/updateAnnouncement.test.ts @@ -60,6 +60,15 @@ export function runTests() { !componentSource.includes("setInterval("), "announcement component must use hardcoded zh/en release notes with an expiry time and no backend polling", ); + assert( + componentSource.includes("polyweather_update_announcement_seen_v1") && + componentSource.includes("loadSeenAnnouncementIds") && + componentSource.includes("saveSeenAnnouncementIds") && + componentSource.includes("markAnnouncementAsSeen") && + componentSource.includes("announcement.id") && + componentSource.includes("scan-update-announcement-unread"), + "announcement component must persist seen announcement ids and render an unread indicator for unseen updates", + ); assert( componentSource.includes("实时观测链路已和模型缓存拆分") && componentSource.includes("SSE") &&