feat: CSV alarm export — supports filters, Excel-compatible with BOM
This commit is contained in:
parent
3adc1e1a43
commit
232526de06
@ -26,7 +26,7 @@
|
||||
| 10 | 时序图表(告警趋势) | ✅ 已完成 | 2026-07-17 | 2026-07-17 |
|
||||
| 11 | 设备分组与标签 | ⬜ 待开始 | - | - |
|
||||
| 12 | 配置版本对比 | ⬜ 待开始 | - | - |
|
||||
| 13 | 报告导出(PDF/CSV) | ⬜ 待开始 | - | - |
|
||||
| 13 | 报告导出(CSV) | ✅ 已完成 | 2026-07-17 | 2026-07-17 |
|
||||
| 14 | 模型管理简化 | ⬜ 待开始 | - | - |
|
||||
| 15 | 人脸库优化(搜索+质量评分) | ⬜ 待开始 | - | - |
|
||||
|
||||
@ -42,6 +42,10 @@
|
||||
- 3 步引导:选设备 → 配检测(从设备读取运行通道,下拉选已有视频源或手动添加) → 一键下发
|
||||
- 标准模式可见,复用的 AutoConfigService 链路
|
||||
|
||||
- **P2-13 报告导出**
|
||||
- API:`/api/alarms/export?from=&to=&device=&rule_type=` 导出 CSV
|
||||
- Excel 兼容(BOM + UTF-8),支持筛选条件继承,告警页面一键下载
|
||||
|
||||
- **P2-10 告警趋势图**
|
||||
- 仪表盘新增 Canvas 柱状图:近 7 日告警数量
|
||||
- API:`/api/alarms/daily-count?days=7` 返回每日统计
|
||||
@ -89,4 +93,4 @@
|
||||
|
||||
---
|
||||
|
||||
**已完成:8/15(53.3%)**
|
||||
**已完成:9/15(60%)**
|
||||
|
||||
@ -425,3 +425,39 @@ ORDER BY day
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *AlarmCollector) ExportFiltered(from, to, deviceID, ruleType string) []AlarmRecord {
|
||||
if c.db == nil {
|
||||
return nil
|
||||
}
|
||||
fromTS := from + "T00:00:00"
|
||||
toTS := to + "T23:59:59"
|
||||
cond := "WHERE timestamp >= ? AND timestamp <= ?"
|
||||
args := []any{fromTS, toTS}
|
||||
if deviceID != "" {
|
||||
cond += " AND device_id = ?"
|
||||
args = append(args, deviceID)
|
||||
}
|
||||
if ruleType != "" {
|
||||
cond += " AND rule_type = ?"
|
||||
args = append(args, ruleType)
|
||||
}
|
||||
rows, err := c.db.Query(`
|
||||
SELECT id, device_id, channel, timestamp, rule_name, rule_type, object_label, confidence, snapshot_url, clip_url, duration_ms, collected_at, status, severity, acknowledged_at, acknowledged_by, resolved_at, resolved_by
|
||||
FROM alarm_records `+cond+`
|
||||
ORDER BY timestamp DESC
|
||||
`, args...)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
var alarms []AlarmRecord
|
||||
for rows.Next() {
|
||||
var a AlarmRecord
|
||||
if err := rows.Scan(&a.ID, &a.DeviceID, &a.Channel, &a.Timestamp, &a.RuleName, &a.RuleType, &a.ObjectLabel, &a.Confidence, &a.SnapshotURL, &a.ClipURL, &a.DurationMs, &a.CollectedAt, &a.Status, &a.Severity, &a.AcknowledgedAt, &a.AcknowledgedBy, &a.ResolvedAt, &a.ResolvedBy); err != nil {
|
||||
continue
|
||||
}
|
||||
alarms = append(alarms, a)
|
||||
}
|
||||
return alarms
|
||||
}
|
||||
|
||||
@ -682,6 +682,7 @@ func (u *UI) RegisterAlarmRoutes(r chi.Router) {
|
||||
r.Get("/api/alarms/stream", u.apiAlarmStream)
|
||||
r.Get("/api/alarms/unacknowledged-count", u.apiAlarmUnacknowledgedCount)
|
||||
r.Get("/api/alarms/daily-count", u.apiAlarmDailyCount)
|
||||
r.Get("/api/alarms/export", u.apiAlarmExport)
|
||||
}
|
||||
|
||||
func (u *UI) Routes() (chi.Router, error) {
|
||||
@ -2346,6 +2347,39 @@ func (u *UI) apiAlarmDailyCount(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
func (u *UI) apiAlarmExport(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
from := strings.TrimSpace(q.Get("from"))
|
||||
to := strings.TrimSpace(q.Get("to"))
|
||||
deviceID := strings.TrimSpace(q.Get("device"))
|
||||
ruleType := strings.TrimSpace(q.Get("rule_type"))
|
||||
if from == "" {
|
||||
from = time.Now().AddDate(0, 0, -7).Format("2006-01-02")
|
||||
}
|
||||
if to == "" {
|
||||
to = time.Now().Format("2006-01-02")
|
||||
}
|
||||
records := u.alarmCollector.ExportFiltered(from, to, deviceID, ruleType)
|
||||
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="alarms_%s_%s.csv"`, from, to))
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Write([]byte("\xef\xbb\xbf"))
|
||||
w.Write([]byte("时间,设备ID,通道,规则,目标,置信度,等级,状态\n"))
|
||||
for _, a := range records {
|
||||
fmt.Fprintf(w, "%s,%s,%s,%s,%s,%.2f,%s,%s\n",
|
||||
escapeCSV(a.Timestamp), escapeCSV(a.DeviceID), escapeCSV(a.Channel),
|
||||
escapeCSV(a.RuleName), escapeCSV(a.ObjectLabel), a.Confidence,
|
||||
escapeCSV(a.Severity), escapeCSV(a.Status))
|
||||
}
|
||||
}
|
||||
|
||||
func escapeCSV(s string) string {
|
||||
if strings.ContainsAny(s, ",\"\n") {
|
||||
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (u *UI) pageResources(w http.ResponseWriter, r *http.Request) {
|
||||
u.ensureDevicesLoaded()
|
||||
data := PageData{Title: "资源管理", Devices: u.registry.GetDevices()}
|
||||
|
||||
@ -26,6 +26,9 @@
|
||||
<a href="/ui/alarms?from={{.WeekStart}}&to={{.WeekEnd}}" class="btn ghost" style="font-size:11px">本周</a>
|
||||
<a href="/ui/alarms?from={{.MonthStart}}&to={{.MonthEnd}}" class="btn ghost" style="font-size:11px">本月</a>
|
||||
<a href="/ui/alarms?from=2020-01-01&to={{.SelectedTo}}" class="btn ghost" style="font-size:11px">全部</a>
|
||||
{{if .AlarmRecords}}
|
||||
<a class="btn ghost js-export-csv" style="font-size:11px;margin-left:8px">导出 CSV</a>
|
||||
{{end}}
|
||||
</span>
|
||||
<select name="per_page" onchange="this.form.submit()" style="font-size:11px;padding:2px 4px;width:90px;flex-shrink:0">
|
||||
<option value="10" {{if eq .PerPage 10}}selected{{end}}>10条/页</option>
|
||||
@ -149,4 +152,21 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var exportBtn = document.querySelector(".js-export-csv");
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener("click", function(){
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var exportParams = new URLSearchParams();
|
||||
if (params.get("from")) exportParams.set("from", params.get("from"));
|
||||
if (params.get("to")) exportParams.set("to", params.get("to"));
|
||||
if (params.get("device")) exportParams.set("device", params.get("device"));
|
||||
if (params.get("rule_type")) exportParams.set("rule_type", params.get("rule_type"));
|
||||
window.location.href = "/api/alarms/export?" + exportParams.toString();
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user