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);
+
})();