diff --git a/docs/superpowers/specs/2026-04-19-device-config-ia-redesign.md b/docs/superpowers/specs/2026-04-19-device-config-ia-redesign.md index ef41dca..843c214 100644 --- a/docs/superpowers/specs/2026-04-19-device-config-ia-redesign.md +++ b/docs/superpowers/specs/2026-04-19-device-config-ia-redesign.md @@ -73,6 +73,64 @@ - 查看、管理、审计分开 - 风格统一、简洁,不做普通 CRUD 表单后台的拼贴感 +## 官方模板参考入口 + +后续所有 UI 细节实现,除了参考 ThingsBoard 这类整体风格方向,还必须优先参考当前后台实际采用的 Tabler 官方文档与示例页面。 + +统一参考入口: + +- Tabler UI 官方文档首页: + + +- Tabler Cards 组件文档: + + +这两个页面的作用不同: + +- `https://docs.tabler.io/ui` + 用来查看官方组件目录、整体视觉语言和页面级组织方式。 + +- `https://docs.tabler.io/ui/components/cards` + 用来查看卡片、标签页卡片、卡片内容分区等具体结构,尤其适合作为详情页、控制页、标签页容器的直接参考。 + +## 官方示例优先规则 + +后续实现 UI 时,必须遵守这条规则: + +1. 先看 Tabler 官方页面的实际效果 +2. 再看官方示例代码结构 +3. 能直接复用官方结构时,不允许先自定义一个“看起来差不多”的版本 +4. 只有官方没有提供合适结构时,才允许做小范围本地样式补充 + +这条规则尤其适用于下面这些高频区域: + +- tabs / nav-tabs +- card-tabs +- card + card-body / card-header +- 表格工具栏 +- 按钮层级 +- 详情页与控制页的内容容器 + +例如: + +- 如果要做标签页,就优先参考官方 `card-tabs + nav nav-tabs + tab-content + card tab-pane` 结构 +- 如果要做卡片分区,就优先参考官方 `card / card-header / card-body` 结构 +- 如果要做按钮、表单、tab 组合,不应先手写一套近似样式,再去慢慢调整 + +## 文档使用约定 + +以后只要文档里写到“按模板实现”“使用官方组件结构”“参考 Tabler 示例”,默认就是指向上面这两个官方入口。 + +也就是说,后续如果要继续调整: + +- 设备详情页 +- 设备控制页 +- 配置页 +- 审计页 +- 任务页 + +都应该先打开官方页面看示例效果和代码,再决定如何落地,而不是只凭组件名称或经验去猜结构。 + ## UI 视觉系统约束 这次重设计不仅要重做信息架构,也要约束 UI 本身的表现方式,避免后续页面继续变成“每页一个风格”。 diff --git a/internal/models/device.go b/internal/models/device.go index cdfde1e..bfbb618 100644 --- a/internal/models/device.go +++ b/internal/models/device.go @@ -10,6 +10,7 @@ type Device struct { MediaPort int `json:"media_port"` DeviceName string `json:"device_name"` Version string `json:"version"` + BuildID string `json:"build_id,omitempty"` GitSha string `json:"git_sha"` UptimeSec int64 `json:"uptime_sec,omitempty"` LastSeenMs int64 `json:"last_seen_ms"` diff --git a/internal/service/discovery.go b/internal/service/discovery.go index 999f2b4..28ee199 100644 --- a/internal/service/discovery.go +++ b/internal/service/discovery.go @@ -124,6 +124,7 @@ func (s *DiscoveryService) Search(timeoutMs int) ([]*models.Device, error) { AgentPort int `json:"agent_port"` MediaPort int `json:"media_port"` Version string `json:"version"` + BuildID string `json:"build_id"` GitSha string `json:"git_sha"` UptimeSec int64 `json:"uptime_sec"` } @@ -142,6 +143,7 @@ func (s *DiscoveryService) Search(timeoutMs int) ([]*models.Device, error) { AgentPort: reply.AgentPort, MediaPort: reply.MediaPort, Version: reply.Version, + BuildID: reply.BuildID, GitSha: reply.GitSha, UptimeSec: reply.UptimeSec, } diff --git a/internal/web/ui.go b/internal/web/ui.go index ea7270e..9a87769 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -64,6 +64,8 @@ type PageData struct { SelectedDeviceIDs []string SelectedDevices []*models.Device SelectedQuery string + SelectedDevicesURL string + BatchConfigURL string RawJSON string RawText string @@ -1329,6 +1331,14 @@ func selectedQueryString(ids []string) string { return values.Encode() } +func selectedURL(path string, ids []string) string { + query := selectedQueryString(ids) + if query == "" { + return path + } + return path + "?" + query +} + func (u *UI) deviceOverviewPageData(r *http.Request, selectedIDs []string, errMsg string) PageData { u.ensureDevicesLoaded() devices := u.registry.GetDevices() @@ -1364,17 +1374,19 @@ func (u *UI) deviceOverviewPageData(r *http.Request, selectedIDs []string, errMs } selectedIDs = filterSelectedDeviceIDs(devices, selectedIDs) data := PageData{ - Title: "设备", - Devices: devices, - DeviceRows: rows, - DeviceCount: len(devices), - OnlineCount: online, - OfflineCount: len(devices) - online, - RunningTaskCount: 0, - FailedTaskCount: failedTasks, - FoundCount: attention, - SelectedDeviceIDs: selectedIDs, - SelectedQuery: selectedQueryString(selectedIDs), + Title: "设备", + Devices: devices, + DeviceRows: rows, + DeviceCount: len(devices), + OnlineCount: online, + OfflineCount: len(devices) - online, + RunningTaskCount: 0, + FailedTaskCount: failedTasks, + FoundCount: attention, + SelectedDeviceIDs: selectedIDs, + SelectedQuery: selectedQueryString(selectedIDs), + SelectedDevicesURL: selectedURL("/ui/devices", selectedIDs), + BatchConfigURL: selectedURL("/ui/devices/batch-config", selectedIDs), } if errMsg != "" { data.Error = errMsg diff --git a/internal/web/ui/assets/style.css b/internal/web/ui/assets/style.css index b64f6ee..1872b4d 100644 --- a/internal/web/ui/assets/style.css +++ b/internal/web/ui/assets/style.css @@ -9,7 +9,15 @@ --border-strong:#d1d5db; --text:#111827; --muted:#6b7280; - --primary:#2563eb; + --primary:#475569; + --primary-strong:#334155; + --primary-strong-hover:#273444; + --button-soft:#e5e7eb; + --button-soft-hover:#dbe1e8; + --button-soft-text:#374151; + --danger-soft:#f3e8e8; + --danger-soft-hover:#eadbdb; + --danger-soft-text:#7c4a4a; --teal:#0f766e; --green:#15803d; --amber:#b45309; @@ -57,9 +65,14 @@ main{padding:24px 28px 36px;max-width:1440px} .small{font-size:12px} .btn,button,input,select,textarea{font:inherit} -.btn,button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:7px 11px;border-radius:8px;border:1px solid var(--border-strong);background:#fff;color:var(--text);cursor:pointer;font-size:12px;font-weight:500} -.btn.primary,button[type=submit]{background:var(--primary);border-color:var(--primary);color:#fff} -.btn.ghost{background:#fff;color:#374151} +.btn,button{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:5px 10px;border-radius:7px;border:1px solid var(--border-strong);background:var(--button-soft);color:var(--button-soft-text);cursor:pointer;font-size:11px;font-weight:500;line-height:1.15;min-height:32px;transition:background-color .16s ease,border-color .16s ease,color .16s ease,box-shadow .16s ease} +.btn:hover,button:hover{background:var(--button-soft-hover);border-color:#c3ccd6} +.btn.primary,button.primary{background:var(--primary-strong);border-color:var(--primary-strong);color:#f8fafc} +.btn.primary:hover,button.primary:hover{background:var(--primary-strong-hover);border-color:var(--primary-strong-hover)} +.btn.secondary,button.secondary,.btn.ghost{background:var(--button-soft);border-color:var(--border-strong);color:var(--button-soft-text)} +.btn.secondary:hover,button.secondary:hover,.btn.ghost:hover{background:var(--button-soft-hover);border-color:#c3ccd6} +.btn.danger,button.danger{background:var(--danger-soft);border-color:#e7d5d5;color:var(--danger-soft-text)} +.btn.danger:hover,button.danger:hover{background:var(--danger-soft-hover);border-color:#dbc4c4} button:disabled,.btn:disabled{opacity:.55;cursor:not-allowed} input,select,textarea{width:100%;padding:8px 10px;border-radius:8px;border:1px solid var(--border-strong);background:#fff;color:var(--text);font-size:12px} textarea{min-height:140px;line-height:1.55} @@ -109,12 +122,17 @@ tbody tr:hover{background:#f9fafb} .actions{display:flex;flex-wrap:wrap;gap:8px} .actions.compact{gap:6px} -.actions.compact .btn,.actions.compact button{padding:5px 9px;font-size:11px} +.actions.compact .btn,.actions.compact button{padding:4px 8px;font-size:11px;line-height:1.1;min-height:28px} .btn .ui-icon{width:14px;height:14px} .stack{flex-direction:column;align-items:flex-start} .device-context-head{display:flex;align-items:center;gap:12px} .device-context-icon{width:34px;height:34px;border-radius:8px;background:#e5eefc;color:#1d4ed8;display:grid;place-items:center} .device-context-icon .ui-icon{width:18px;height:18px} +.device-tab-wrap{margin-top:16px} +.device-tab-card{margin-top:-1px;padding:0} +.device-panel-body{padding:16px} +.device-panel-body>.card,.device-panel-body>details.card{margin:0 0 16px} +.device-panel-body>.card:last-child,.device-panel-body>details.card:last-child{margin-bottom:0} .detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px} .quad-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px} @@ -151,7 +169,14 @@ pre{margin-top:12px;padding:12px;border-radius:8px;border:1px solid #1f2937;back .row{display:flex;gap:12px;flex-wrap:wrap} .row>*{flex:1 1 220px} .subnav{display:flex;gap:8px;flex-wrap:wrap} -.subnav a{display:inline-flex;align-items:center;padding:7px 10px;border:1px solid var(--border);border-radius:999px;background:#fff;color:#374151;font-size:12px;font-weight:500} +.device-tab-wrap{margin-top:16px} +.device-tabs .nav-link{font-size:12px;font-weight:500;color:#64748b} +.device-tabs .nav-link:hover{color:#334155} +.device-tabs .nav-link.active{color:#111827} +.device-tab-card{margin-top:0} +.device-panel-body{padding:16px} +.device-panel-body>.card,.device-panel-body>details.card{margin:0 0 16px} +.device-panel-body>.card:last-child,.device-panel-body>details.card:last-child{margin-bottom:0} .ui-icon{display:block;flex:0 0 auto} .batch-toolbar{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:14px 16px;border:1px solid var(--border);border-radius:8px;background:var(--surface-soft);margin:0 0 12px} diff --git a/internal/web/ui/templates/api.html b/internal/web/ui/templates/api.html index caaa621..629783a 100644 --- a/internal/web/ui/templates/api.html +++ b/internal/web/ui/templates/api.html @@ -1,7 +1,6 @@ {{define "api"}}

高级调试

-
用于工程排障和接口验证。普通运维流程建议优先使用设备列表、配置管理和识别配置页面。
@@ -75,7 +74,6 @@

任务实时事件

-
用于观察批量任务在各节点上的实时执行状态。
@@ -86,7 +84,6 @@

模型上传调试

-
向指定节点上传检测、识别或人脸模型文件。
模型名称
diff --git a/internal/web/ui/templates/audit.html b/internal/web/ui/templates/audit.html index 51e65fd..a868d13 100644 --- a/internal/web/ui/templates/audit.html +++ b/internal/web/ui/templates/audit.html @@ -1,17 +1,8 @@ {{define "audit"}} -
-
-
操作审计
-

所有关键操作的统一留痕入口

-
这里只看谁做了什么、对哪台设备做的、结果如何。
-
-
-

{{icon "audit"}}审计记录

-
当前展示后台已记录的任务与设备操作结果。
diff --git a/internal/web/ui/templates/config_friendly.html b/internal/web/ui/templates/config_friendly.html index e918c71..ee6da59 100644 --- a/internal/web/ui/templates/config_friendly.html +++ b/internal/web/ui/templates/config_friendly.html @@ -2,7 +2,7 @@ {{template "device_nav" .}}

识别方案配置

-
节点:{{.Device.DeviceID}}({{.Device.DeviceName}})。用于编辑视频通道、模型路径和推理参数。
+
节点:{{.Device.DeviceID}}({{.Device.DeviceName}})。
@@ -14,7 +14,6 @@

选择节点

-
当前目标节点已经确定。后续步骤会基于该节点读取配置结构和当前状态。
节点名称
{{.Device.DeviceName}}
节点标识
{{.Device.DeviceID}}
@@ -25,17 +24,14 @@

编辑视频通道

-
在下方通道编辑器中维护识别模板、视频流地址、模型路径和推理参数。

预览变更

-
点击“预览变更”后,会显示设备端生成的变更计划。

部署结果

-
点击“部署到设备”后,执行结果会显示在页面底部。
diff --git a/internal/web/ui/templates/config_preview.html b/internal/web/ui/templates/config_preview.html index d9acdf1..4d4f5bd 100644 --- a/internal/web/ui/templates/config_preview.html +++ b/internal/web/ui/templates/config_preview.html @@ -5,15 +5,10 @@

{{icon "preview"}}配置预览

-
选择模板、Profile 和 Overlay,生成可上传的候选配置。
{{if .ConfigSources.Root}}
{{.ConfigSources.Root}}
{{end}}
-
- config_id 是配置名,默认会带上 device_id(当前设备:{{.Device.DeviceID}});config_version 表示本次生成版本;SHA256 是最终文件内容指纹。 -
-