diff --git a/internal/web/ui.go b/internal/web/ui.go index 9c2d0d2..0c5351a 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -1128,30 +1128,26 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) { u.ensureDevicesLoaded() devices := u.registry.GetDevices() - // Step 1: persist feature selections to DB (source of truth). + // Step 1: collect form features per device. + formFeatures := map[string][]string{} for _, dev := range devices { if dev == nil || dev.DeviceID == "" { continue } features := cleanFormList(r.Form["device_"+dev.DeviceID+"_feature"]) - if err := u.preview.SaveDeviceFeatures(dev.DeviceID, features); err != nil { - http.Redirect(w, r, "/ui/console?error="+url.QueryEscape("保存功能配置失败: "+err.Error()), http.StatusFound) - return + if len(features) > 0 { + formFeatures[dev.DeviceID] = features } } - // Step 2: build requests from DB only. - // Features come from device_features table. - // Video sources come from existing device assignments and recognition units. + // Step 2: build requests using form features (user intent) and + // existing recognition unit video sources. var requests []service.AutoConfigRequest for _, dev := range devices { if dev == nil || dev.DeviceID == "" { continue } - features, err := u.preview.GetDeviceFeatures(dev.DeviceID) - if err != nil { - continue - } + features := formFeatures[dev.DeviceID] var sources []string if assignment, err := u.preview.GetDeviceAssignment(dev.DeviceID); err == nil && assignment != nil { for _, ref := range assignment.RecognitionUnits { @@ -1162,7 +1158,7 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) { } } } - if len(features) == 0 && len(sources) == 0 { + if len(features) == 0 { continue } requests = append(requests, service.AutoConfigRequest{ @@ -1173,12 +1169,22 @@ func (u *UI) actionConsoleSave(w http.ResponseWriter, r *http.Request) { } if len(requests) == 0 { - http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape("未选择检测功能或未分配视频源"), http.StatusFound) + http.Redirect(w, r, "/ui/console?msg="+url.QueryEscape("未选择检测功能"), http.StatusFound) return } + // Step 3: run pipeline. Only persist to DB on success. results, _ := u.autoConfig.BuildPipelineBatch(requests, true) + // Step 4: persist successful feature selections to DB. + for _, res := range results { + if res.Error == "" && res.TaskID != "" { + if feats, ok := formFeatures[res.DeviceID]; ok { + _ = u.preview.SaveDeviceFeatures(res.DeviceID, feats) + } + } + } + // Collect task IDs for redirect. taskIDs := make([]string, 0) var errs []string diff --git a/internal/web/ui/templates/console.html b/internal/web/ui/templates/console.html index e0e632b..a4509a5 100644 --- a/internal/web/ui/templates/console.html +++ b/internal/web/ui/templates/console.html @@ -356,6 +356,10 @@ }); }); if (!summary) { alert('未选择任何检测功能或视频源'); return; } + // Require at least one feature. + var hasFeature = false; + document.querySelectorAll('.console-device-row input[type=checkbox]:checked').forEach(function() { hasFeature = true; }); + if (!hasFeature) { alert('请至少选择一项检测功能'); return; } var overlay = document.createElement('div'); overlay.className = 'confirm-overlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center';