feat: alarm notification — topbar bell badge + title flash with unacknowledged count
This commit is contained in:
parent
13d22ce848
commit
3d7313585c
25
docs/实施跟踪.md
25
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%)**
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -70,9 +70,10 @@
|
||||
<button type="button" data-theme-option="graphite-gold">石墨金色</button>
|
||||
</div>
|
||||
</div>
|
||||
<a class="topbar-icon-btn" href="/ui/diagnostics" aria-label="日志审计" title="日志审计">
|
||||
<a class="topbar-icon-btn" href="/ui/alarms" aria-label="告警中心" title="告警中心" id="topbar-alarm-btn">
|
||||
{{icon "bell"}}
|
||||
<span class="topbar-dot" aria-hidden="true"></span>
|
||||
<span class="topbar-dot" id="alarm-dot" {{if eq .UnacknowledgedAlarmCount 0}}hidden{{end}} aria-hidden="true"></span>
|
||||
<span class="topbar-badge" id="alarm-badge" {{if eq .UnacknowledgedAlarmCount 0}}hidden{{end}}>{{.UnacknowledgedAlarmCount}}</span>
|
||||
</a>
|
||||
<a class="topbar-icon-btn" href="/ui/system" aria-label="系统" title="系统">
|
||||
{{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);
|
||||
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user