diff --git a/docs/实施跟踪.md b/docs/实施跟踪.md index d91e60b..3b94a3f 100644 --- a/docs/实施跟踪.md +++ b/docs/实施跟踪.md @@ -5,8 +5,8 @@ | # | 事项 | 状态 | 开始 | 完成 | |---|------|------|------|------| | 1 | 告警中心增强 | ✅ 已完成 | 2026-07-17 | 2026-07-17 | -| 2 | 仪表盘重做(实时告警流+统计卡片) | 🔄 进行中 | 2026-07-17 | - | -| 3 | 一键配置向导(场景化部署 3 步流程) | ⬜ 待开始 | - | - | +| 2 | 仪表盘重做(实时告警流+统计卡片) | ✅ 已完成 | 2026-07-17 | 2026-07-17 | +| 3 | 一键配置向导(场景化部署 3 步流程) | 🔄 进行中 | 2026-07-17 | - | | 4 | 基础登录鉴权 | ⬜ 待开始 | - | - | | 5 | 监控页 HLS.js 本地化 | ✅ 已完成 | 2026-07-17 | 2026-07-17 | @@ -32,18 +32,23 @@ ## 进行中 -### P0-2 仪表盘重做 +### P0-3 一键配置向导 | # | 子任务 | 状态 | |---|--------|------| -| 2.1 | 统计卡片 — 今日告警/未处理告警/在线设备/检测路数 | ✅ | -| 2.2 | SSE 实时告警流 — EventSource 连接 /api/alarms/stream | ✅ | -| 2.3 | 保留原有 — 异常设备列表 + 最近任务 | ✅ | +| 3.1 | 路由 + 3 个 Handler(页面、创建设备、下发配置) | ✅ | +| 3.2 | 3 步模板:选设备→配检测→确认下发 | ✅ | +| 3.3 | 内联添加摄像头(RTSP URL) | ✅ | +| 3.4 | 菜单项(标准模式可见) | ✅ | ## 已完成事项 ### 2026-07-17 +- **P0-2 仪表盘重做** + - 统计卡片:今日告警/未处理告警/在线设备/检测路数 + - SSE 实时告警流:EventSource 连接 /api/alarms/stream,新告警实时推入 + - **P0-1 告警中心增强** - DB 迁移:`alarm_records` 增加 `status`, `severity`, `acknowledged_at/by`, `resolved_at/by` 列 - 服务层:`AlarmRecord` 扩展 + SSE 广播 + 确认/关闭/改等级 API @@ -62,4 +67,4 @@ --- -**已完成:2/15(13.3%)** | **进行中:1(P0-2)** +**已完成:2/15(13.3%)** | **进行中:1(P0-3)** diff --git a/internal/web/ui.go b/internal/web/ui.go index 5eab371..1342601 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -109,6 +109,7 @@ type PageData struct { ConsoleDevices []ConsoleDeviceData ConsoleVideoSources []service.ConfigVideoSourceAsset ConsoleAllVideoSources []service.ConfigVideoSourceAsset + WizardVideoSourcesJSON template.HTML ConsoleUnassignedUnits []service.RecognitionUnitAsset FaceGalleryPersons []storage.PersonRecord Templates []service.Template @@ -705,6 +706,10 @@ func (u *UI) Routes() (chi.Router, error) { r.Get("/dashboard", u.pageDashboard) r.Get("/console", u.pageConsole) r.Post("/console", u.actionConsoleSave) + r.Get("/wizard", u.pageWizard) + r.Get("/wizard/device-info", u.apiWizardDeviceInfo) + r.Post("/wizard/apply", u.actionWizardApply) + r.Post("/wizard/create-source", u.actionWizardCreateSource) r.Get("/devices", u.pageDevices) r.Get("/devices/{id}/control", u.pageDeviceControl) r.Get("/plans", u.redirectPlansToSceneTemplates) @@ -1307,6 +1312,141 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape("配置已保存"), http.StatusFound) } +func (u *UI) pageWizard(w http.ResponseWriter, r *http.Request) { + u.ensureDevicesLoaded() + data := PageData{Title: "部署向导"} + data.Devices = u.registry.GetDevices() + if u.preview != nil { + if sources, err := u.preview.ListVideoSources(); err == nil { + data.ConsoleAllVideoSources = sources + type wizardSource struct { + Name string `json:"name"` + URL string `json:"url"` + } + var list []wizardSource + for _, s := range sources { + list = append(list, wizardSource{Name: s.Name, URL: s.Config.URL}) + } + if b, err := json.Marshal(list); err == nil { + data.WizardVideoSourcesJSON = template.HTML(b) + } + } + } + u.render(w, r, "wizard", data) +} + +func (u *UI) actionWizardApply(w http.ResponseWriter, r *http.Request) { + if u.autoConfig == nil { + http.Error(w, "not available", http.StatusServiceUnavailable) + return + } + var req struct { + DeviceID string `json:"device_id"` + Features []string `json:"features"` + SourceNames []string `json:"source_names"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + if u.preview != nil && len(req.Features) > 0 { + _ = u.preview.SaveDeviceFeatures(req.DeviceID, req.Features) + } + result, err := u.autoConfig.BuildPipeline(service.AutoConfigRequest{ + DeviceID: req.DeviceID, + Features: req.Features, + SourceNames: req.SourceNames, + }, true) + if err != nil { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +func (u *UI) actionWizardCreateSource(w http.ResponseWriter, r *http.Request) { + if u.preview == nil { + http.Error(w, "not available", http.StatusServiceUnavailable) + return + } + name := strings.TrimSpace(r.FormValue("name")) + rtspURL := strings.TrimSpace(r.FormValue("url")) + if name == "" || rtspURL == "" { + http.Error(w, "name and url required", http.StatusBadRequest) + return + } + asset := service.ConfigVideoSourceAsset{ + Name: name, + SourceType: "rtsp", + Config: service.VideoSourceConfig{ + URL: rtspURL, + Resolution: "1920x1080", + FPS: "25", + }, + } + if err := u.preview.SaveVideoSourceAsset(asset); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"name": name}) +} + +func (u *UI) apiWizardDeviceInfo(w http.ResponseWriter, r *http.Request) { + deviceID := strings.TrimSpace(r.URL.Query().Get("device_id")) + if deviceID == "" { + http.Error(w, "device_id required", http.StatusBadRequest) + return + } + var dev *models.Device + for _, d := range u.registry.GetDevices() { + if d != nil && d.DeviceID == deviceID { + dev = d + break + } + } + if dev == nil { + http.Error(w, "device not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/json") + resp := map[string]any{ + "online": dev.Online, + "device_id": dev.DeviceID, + "name": dev.DisplayName(), + } + if u.preview != nil { + if assignment, _ := u.preview.GetDeviceAssignment(deviceID); assignment != nil { + var sourceNames []string + for _, ref := range assignment.RecognitionUnits { + if unit, _ := u.preview.GetRecognitionUnit(ref); unit != nil { + sourceNames = append(sourceNames, unit.VideoSourceRef) + } + } + resp["sources"] = sourceNames + } + } + if u.agent != nil && dev.Online { + body, code, _ := u.agent.Do("GET", dev.IP, dev.AgentPort, "/v1/capabilities", nil) + if code == 200 { + var capsResp struct { + Capabilities []struct { + Key string `json:"key"` + Label string `json:"label"` + Available bool `json:"available"` + Description string `json:"description"` + } `json:"capabilities"` + } + if json.Unmarshal(body, &capsResp) == nil { + resp["capabilities"] = capsResp.Capabilities + } + } + } + json.NewEncoder(w).Encode(resp) +} + func (u *UI) pageDevices(w http.ResponseWriter, r *http.Request) { u.render(w, r, "devices", u.deviceOverviewPageData(r, nil, "")) } diff --git a/internal/web/ui/templates/layout.html b/internal/web/ui/templates/layout.html index 16aa208..ffffb19 100644 --- a/internal/web/ui/templates/layout.html +++ b/internal/web/ui/templates/layout.html @@ -21,6 +21,7 @@