From 3d7313585cfc9227b997c6b909a9c49d9a06b29a Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Fri, 17 Jul 2026 17:32:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20alarm=20notification=20=E2=80=94=20topb?= =?UTF-8?q?ar=20bell=20badge=20+=20title=20flash=20with=20unacknowledged?= =?UTF-8?q?=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/实施跟踪.md | 25 ++++++++++++--------- internal/web/ui.go | 13 +++++++++++ internal/web/ui/assets/style.css | 1 + internal/web/ui/templates/layout.html | 32 +++++++++++++++++++++++++-- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/docs/实施跟踪.md b/docs/实施跟踪.md index 3b94a3f..4963370 100644 --- a/docs/实施跟踪.md +++ b/docs/实施跟踪.md @@ -6,7 +6,7 @@ |---|------|------|------|------| | 1 | 告警中心增强 | ✅ 已完成 | 2026-07-17 | 2026-07-17 | | 2 | 仪表盘重做(实时告警流+统计卡片) | ✅ 已完成 | 2026-07-17 | 2026-07-17 | -| 3 | 一键配置向导(场景化部署 3 步流程) | 🔄 进行中 | 2026-07-17 | - | +| 3 | 一键配置向导(场景化部署 3 步流程) | ✅ 已完成 | 2026-07-17 | 2026-07-17 | | 4 | 基础登录鉴权 | ⬜ 待开始 | - | - | | 5 | 监控页 HLS.js 本地化 | ✅ 已完成 | 2026-07-17 | 2026-07-17 | @@ -14,7 +14,7 @@ | # | 事项 | 状态 | 开始 | 完成 | |---|------|------|------|------| -| 6 | 告警浏览器通知(SSE + 标题闪烁) | ⬜ 待开始 | - | - | +| 6 | 告警浏览器通知(SSE / 标题闪烁) | ✅ 已完成 | 2026-07-17 | 2026-07-17 | | 7 | 设备列表服务健康指示 | ⬜ 待开始 | - | - | | 8 | 操作结果提示优化(toast 替代 URL 消息) | ⬜ 待开始 | - | - | | 9 | 术语统一 | ⬜ 待开始 | - | - | @@ -32,19 +32,22 @@ ## 进行中 -### P0-3 一键配置向导 - -| # | 子任务 | 状态 | -|---|--------|------| -| 3.1 | 路由 + 3 个 Handler(页面、创建设备、下发配置) | ✅ | -| 3.2 | 3 步模板:选设备→配检测→确认下发 | ✅ | -| 3.3 | 内联添加摄像头(RTSP URL) | ✅ | -| 3.4 | 菜单项(标准模式可见) | ✅ | +(无) ## 已完成事项 ### 2026-07-17 +- **P0-3 一键配置向导** + - 3 步引导:选设备 → 配检测(从设备读取运行通道,下拉选已有视频源或手动添加) → 一键下发 + - 标准模式可见,复用的 AutoConfigService 链路 + +- **P1-6 告警浏览器通知** + - 顶栏铃铛图标显示未处理告警数(红点+数字徽章) + - 页面标题闪烁 `(N) SafeSight` + - 每 15 秒轮询 `/api/alarms/unacknowledged-count` + - `render` 统一注入计数,所有页面生效 + - **P0-2 仪表盘重做** - 统计卡片:今日告警/未处理告警/在线设备/检测路数 - SSE 实时告警流:EventSource 连接 /api/alarms/stream,新告警实时推入 @@ -67,4 +70,4 @@ --- -**已完成:2/15(13.3%)** | **进行中:1(P0-3)** +**已完成:4/15(26.7%)** diff --git a/internal/web/ui.go b/internal/web/ui.go index 4de8e04..c4877d4 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -802,6 +802,7 @@ func (u *UI) Routes() (chi.Router, error) { r.Post("/alarms/{id}/resolve", u.actionResolveAlarm) r.Post("/alarms/{id}/severity", u.actionUpdateAlarmSeverity) r.Get("/api/alarms/stream", u.apiAlarmStream) + r.Get("/api/alarms/unacknowledged-count", u.apiAlarmUnacknowledgedCount) r.Get("/face-gallery", u.pageFaceGallery) r.Get("/face-photo/*", u.serveFacePhoto) r.Post("/face-gallery/import", u.actionFaceGalleryImport) @@ -826,6 +827,9 @@ func (u *UI) Routes() (chi.Router, error) { func (u *UI) render(w http.ResponseWriter, r *http.Request, content string, data PageData) { data.Version = version data.Year = time.Now().Year() + if u.alarmCollector != nil { + data.UnacknowledgedAlarmCount = u.alarmCollector.GetUnacknowledgedCount() + } var buf bytes.Buffer if err := u.tpl.ExecuteTemplate(&buf, content, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -2319,6 +2323,15 @@ func (u *UI) apiAlarmStream(w http.ResponseWriter, r *http.Request) { } } +func (u *UI) apiAlarmUnacknowledgedCount(w http.ResponseWriter, r *http.Request) { + count := 0 + if u.alarmCollector != nil { + count = u.alarmCollector.GetUnacknowledgedCount() + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]int{"count": count}) +} + func (u *UI) pageResources(w http.ResponseWriter, r *http.Request) { u.ensureDevicesLoaded() data := PageData{Title: "资源管理", Devices: u.registry.GetDevices()} diff --git a/internal/web/ui/assets/style.css b/internal/web/ui/assets/style.css index 2011beb..d3240ba 100644 --- a/internal/web/ui/assets/style.css +++ b/internal/web/ui/assets/style.css @@ -149,6 +149,7 @@ code,.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace} .topbar-icon-btn:hover{background:var(--button-soft-hover);border-color:var(--primary);color:var(--text)} .topbar-icon-btn .ui-icon{width:16px;height:16px} .topbar-dot{position:absolute;right:6px;top:6px;width:6px;height:6px;border-radius:999px;background:var(--red)} +.topbar-badge{position:absolute;right:-4px;top:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--red);color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center} .theme-menu{position:relative} .theme-menu-panel{position:fixed;right:28px;top:54px;min-width:132px;padding:6px;border:1px solid var(--border-strong);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow);z-index:9999} .theme-menu-panel button{display:flex;width:100%;justify-content:flex-start;min-height:28px;border-color:transparent;background:transparent;color:var(--text)} diff --git a/internal/web/ui/templates/layout.html b/internal/web/ui/templates/layout.html index ffffb19..397700f 100644 --- a/internal/web/ui/templates/layout.html +++ b/internal/web/ui/templates/layout.html @@ -70,9 +70,10 @@ - + {{icon "bell"}} - + + {{.UnacknowledgedAlarmCount}} {{icon "system"}} @@ -361,6 +362,33 @@ applyMode(current === "expert" ? "standard" : "expert", true); }); + var originalTitle = document.title; + var alarmDot = document.getElementById("alarm-dot"); + var alarmBadge = document.getElementById("alarm-badge"); + + function updateAlarmBadge(count) { + if (!alarmDot || !alarmBadge) return; + if (count > 0) { + alarmDot.hidden = false; + alarmBadge.hidden = false; + alarmBadge.textContent = count > 99 ? "99+" : count; + document.title = "(" + count + ") " + originalTitle; + } else { + alarmDot.hidden = true; + alarmBadge.hidden = true; + document.title = originalTitle; + } + } + + function pollAlarmCount() { + fetch("/api/alarms/unacknowledged-count", {credentials:"same-origin"}) + .then(function(r){return r.json()}) + .then(function(data){updateAlarmBadge(data.count||0)}) + .catch(function(){}); + } + + setInterval(pollAlarmCount, 15000); + })();