From 46d021eba355d9a77f0aabecf1a0436858b5a385 Mon Sep 17 00:00:00 2001 From: QiuSW Date: Mon, 20 Jul 2026 22:50:57 +0800 Subject: [PATCH] docs: add settings tab to UI prototype --- docs/02-requirements.md | 3 + docs/04-architecture.md | 8 + docs/06-tasks.md | 2 +- docs/current-state.md | 2 +- docs/routes.md | 18 +- docs/ui/2026-07-20-html-prototype-plan.md | 8 +- docs/ui/silver-pose-ui-ux-spec.md | 25 +- docs/ui/silver-pose-v1-prototype.html | 701 +++++++++++++++++++++- progress.md | 9 + 9 files changed, 756 insertions(+), 20 deletions(-) diff --git a/docs/02-requirements.md b/docs/02-requirements.md index f4062bb..481c68a 100644 --- a/docs/02-requirements.md +++ b/docs/02-requirements.md @@ -29,6 +29,7 @@ | 姿态与人员跟踪 | 全身可见人员显示骨架和稳定人员 ID;质量不足时明确为不确定而非报警。 | P0 | | 突发倒地事件 | 同一人员从正常姿态快速转为倒地且持续时,在 1–3 秒内确认事件。 | P0 | | 本地报警 | 确认事件后画面变红、播放声音、弹出窗口并保存标注截图。 | P0 | +| 安全设置与配置版本 | 操作员可在设置页编辑非敏感事件参数,保存后仅在下一次开始监控时生效,监控与事件工件显示实际配置版本。 | P0 | | 演示回归 | 同一组正反例录像可重复验证报警、无报警和延迟。 | P0 | ## 后续迭代 @@ -47,6 +48,7 @@ 3. 当成年人安全模拟从站立或行走突然倒地时,系统在 1–3 秒内对该人员显示红色状态、播放声音、弹窗并保存截图。 4. 当人员坐下、弯腰、捡物或短暂停留时,系统不把该动作确认成摔倒事件。 5. 当同一人员持续倒地时,系统保持事件状态但不重复弹窗;恢复稳定站立后可回到正常状态。 +6. 作为演示操作员,我可以在设置页检查来源环境变量是否就绪、调整非敏感事件参数,并知道这些改动会在下一次开始监控时生效。 ## MVP 验收标准 @@ -55,6 +57,7 @@ - **突发摔倒**:每段经过批准的正例录像或现场动作,报警延迟记录在 1–3 秒范围内。 - **反例**:每段坐下、弯腰、捡物和短暂停留录像均不产生确认事件。 - **报警闭环**:每个确认事件只产生一次声音和弹窗,并保存一张含时间、ID、骨架和事件状态的截图。 +- **配置可追溯**:设置页不显示真实来源地址或密码;保存的草稿与正在监控的配置版本明确区分,事件记录包含实际生效的配置版本。 - **可复现性**:验收录像、配置版本、事件结果和实际命令保存在项目约定位置或 `progress.md`,可在同一机器重复运行。 ## 范围与风险 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 823fd75..886f0ce 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -39,6 +39,14 @@ V1 的同一数据流既可接 RTSP,也可回放本地录像。V2 复用同一 | PyQt UI | `v1/gui.py` | 渲染帧、骨架、状态、设置和弹窗 | 直接读 RTSP 或写判定规则 | | 回归工具 | `v1/tests/` 与 `v1/scripts/` | 回放录像、断言事件和延迟 | 生产 UI | +## UI 导航与配置生效生命周期 + +PyQt 主窗口只包含实时监控和设置两个顶部 Tab。实时监控 Tab 保持视频画面优先;设置 Tab 不能直接调用视频源、Pose 或状态机。 + +设置页面产生的是已校验的草稿配置。用户点击开始监控时,应用创建不可变的运行配置快照与配置版本,并将该快照传给视频源、Pose、证据和状态机。运行期间编辑设置不会修改该快照;保存后的草稿在下一次开始监控时才会成为新的运行配置。FallEvent 继续记录 config_version,使截图和 JSONL 可以追溯到实际阈值。 + +真实 RTSP 凭证仍只由环境变量或未跟踪本地配置提供。UI 只显示环境变量是否就绪,不能回显或记录具体值。 + ## 状态模型 每个 `track_id` 独立维护状态: diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 263f596..cc4e080 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -24,7 +24,7 @@ | ID | 任务 | 依赖 | 验收要点 | 状态 | | --- | --- | --- | --- | --- | -| T-201 | 实现 PyQt 实时监控界面与状态叠加 | T-105 | 显示视频、骨架、ID、NORMAL/SUSPECT/CONFIRMED 和连接状态;UI 不直接执行事件计算。 | TODO | +| T-201 | 实现 PyQt 顶部双 Tab 的监控与设置界面 | T-105 | 实时监控 Tab 显示视频、骨架、ID、NORMAL/SUSPECT/CONFIRMED 和连接状态;设置 Tab 只编辑非敏感草稿并明确下次启动生效;UI 不直接执行事件计算。 | TODO | | T-202 | 实现本地报警、弹窗、截图和 JSONL 日志 | T-201 | CONFIRMED 产生一次声音、一次弹窗、一张带标注截图和一条事件日志。 | TODO | | T-203 | 接入海康 RTSP 与断流恢复 | T-202 | 现场有效流可预览;断流状态明确、可重连且不报警。 | TODO | | T-204 | 建立正反例录像回归与现场验收记录 | T-203 | 摔倒、行走、坐下、弯腰、捡物和持续倒地都有预期结果;报警延迟记录在 1–3 秒。 | TODO | diff --git a/docs/current-state.md b/docs/current-state.md index e87f9e5..8ad95f1 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -10,7 +10,7 @@ - 旧生产基线:`demo/main.py`、`demo/fall_detection_gui.py`、`demo/detect_fall.py`、`demo/best.pt`。 - V1 代码:`v1/` 目录存在但尚无实现。 - V2 代码:`v2/` 目录存在但尚无实现。 -- 非代码设计工件:docs/ui/silver-pose-ui-ux-spec.md、docs/ui/2026-07-20-html-prototype-plan.md 与 docs/ui/silver-pose-v1-prototype.html 已建立。HTML 可离线演示正常、确认摔倒和连接异常三种 UI 状态;其中画面、事件和时间均为模拟数据,不连接真实摄像头、模型或网络,也不改变 Phase 1 任务顺序。 +- 非代码设计工件:docs/ui/silver-pose-ui-ux-spec.md、docs/ui/2026-07-20-html-prototype-plan.md 与 docs/ui/silver-pose-v1-prototype.html 已建立。HTML 使用顶部双 Tab:实时监控演示正常、确认摔倒和连接异常三种状态;设置页演示安全来源状态、模型只读信息和下次启动生效的配置草稿。画面、事件和时间均为模拟数据,不连接真实摄像头、模型或网络,也不改变 Phase 1 任务顺序。 - 测试:暂无 pytest 测试;`python -m compileall -q demo` 已通过。 - 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。 - 当前标准启动:`./init.ps1`。 diff --git a/docs/routes.md b/docs/routes.md index c375ff9..8a20ffe 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -1,6 +1,6 @@ # 桌面页面与交互结构 -> V1 是单窗口 PyQt5 桌面应用,没有 Web 路由。本文件规定页面职责和导航,V2 选择本地网页壳或桌面壳时必须保持同等用户流程。 +> V1 是单窗口 PyQt5 桌面应用,没有 Web 路由。主窗口使用顶部水平双 Tab;V2 选择本地网页壳或桌面壳时必须保持同等用户流程。 ## 页面 / 视图 @@ -12,6 +12,21 @@ | 事件提示区 | 显示最近事件和截图保存结果;V1 不做长期历史数据库。 | | 连接错误提示 | 显示断流或配置错误以及可重试状态;不得使用“正常”掩盖错误。 | +## 顶层导航 + +顶部 Tab 只有两个固定目的地:实时监控和设置。默认进入实时监控,因为客户演示的主任务是看见画面与报警闭环。使用顶部 Tab 而非左侧导航,避免在 V1 的单路 16:9 视频旁占用横向空间;只有后续增加回放、诊断、设备管理等多个稳定模块时才评估左侧导航。 + +- 实时监控 Tab:视频、姿态、人员状态、事件证据、最近事件和报警弹窗。 +- 设置 Tab:来源就绪状态、模型只读信息、事件时间窗、质量门槛、重连策略和配置版本。 + +## 设置视图 + +- 来源区域只显示环境变量名称与“已就绪 / 未就绪”,不得显示真实地址、账号或密码。 +- 模型区域只读显示模型名称、哈希摘要、关键点格式与验证日期;模型替换不属于演示操作。 +- 事件参数使用带单位和范围说明的数字控件:关键点质量阈值、疑似时间、确认时间、恢复时间和冷却时间。 +- 配置编辑分为编辑中、已保存待下次启动、正在监控已生效三种文字状态,不能只靠颜色区分。 +- 保存与应用操作只写入未跟踪的本地配置;当前运行的事件引擎继续使用开始监控时冻结的配置版本。 + ## 实时监控视图 - 绿色:`NORMAL`。 @@ -28,3 +43,4 @@ 3. 修改事件阈值后,UI 显示的值必须是下一次开始监控真正使用的配置版本。 4. 断流时显示错误与重连状态;恢复连接不自动生成摔倒事件。 5. 截图保存失败时,仍保留红色报警并在提示区记录错误。 +6. 设置修改不会热改正在运行的事件阈值;操作员保存后可在监控页看到下一次启动将使用的版本。 diff --git a/docs/ui/2026-07-20-html-prototype-plan.md b/docs/ui/2026-07-20-html-prototype-plan.md index 9eb7b5a..e808ff8 100644 --- a/docs/ui/2026-07-20-html-prototype-plan.md +++ b/docs/ui/2026-07-20-html-prototype-plan.md @@ -2,9 +2,9 @@ > **For agentic workers:** REQUIRED SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Create one offline HTML prototype that lets a customer switch between normal monitoring, confirmed-fall, and connection-error views without accessing a camera or model. +**Goal:** Create one offline HTML prototype with top-level monitoring and settings Tabs, while retaining the three monitoring demonstration states without accessing a camera or model. -**Architecture:** One self-contained HTML document holds semantic layout, CSS tokens, inline SVG scene, and a small state controller. The controller changes visible copy, status semantics, scene treatment, event evidence, and an idempotent demonstration dialog; it does not fetch data, access files, or claim to perform inference. +**Architecture:** One self-contained HTML document holds a semantic top Tab bar, monitoring and settings panels, CSS tokens, inline SVG scene, and a small controller. The controller changes visible copy, active Tab, status semantics, scene treatment, event evidence, configuration draft state, and an idempotent demonstration dialog; it does not fetch data, access files, or claim to perform inference. **Tech Stack:** HTML5, CSS custom properties, inline SVG, vanilla JavaScript, Node.js 22 static syntax checks. @@ -96,6 +96,10 @@ Expected: no whitespace errors and a commit containing only the prototype docume ## Self-review +## Tab revision + +The revised prototype has a horizontal tablist with monitoring and settings tabs. The settings panel shows only an environment-variable readiness state for the source, read-only model metadata, editable non-sensitive event and reconnect fields, a current running configuration version, a saved draft version, and save/reset controls. Editing marks the draft dirty; saving says the draft takes effect on the next monitoring start and cannot change the monitoring panel's running configuration version. + - Spec coverage: Task 1 implements all specified views, dialog behavior, responsive breakpoints, keyboard paths, state text, offline limitation, and validation conditions. - Placeholder scan: no unresolved placeholder markers or file paths appear in this plan. - Consistency: the three controller state names are consistently `normal`, `confirmed`, and `offline`; the dialog and acknowledgement element IDs are fixed across layout and behavior. diff --git a/docs/ui/silver-pose-ui-ux-spec.md b/docs/ui/silver-pose-ui-ux-spec.md index fd10885..c37e3e1 100644 --- a/docs/ui/silver-pose-ui-ux-spec.md +++ b/docs/ui/silver-pose-ui-ux-spec.md @@ -7,20 +7,19 @@ - 目标框架:Windows 上的 PyQt5;HTML 仅作离线交互视觉原型,不替代生产 UI。 - 默认窗口:1280 × 800 epx;支持最小 960 × 640、Windows Snap 宽度和 125%/150% 缩放。 - 语言:简体中文;数据均为虚构演示数据,不包含 RTSP 地址、账号、客户图像或真实事件。 -- 范围:单路摄像头、实时监控、确认摔倒告警、连接异常提示和最近事件。 -- 不在范围:真实 RTSP、模型推理、音频播放、文件系统写入、设置编辑、云端通知及历史事件库。 +- 范围:单路摄像头、实时监控、确认摔倒告警、连接异常提示、最近事件和非敏感设置的交互原型。 +- 不在范围:真实 RTSP、模型推理、音频播放、文件系统写入、真实配置持久化、云端通知及历史事件库。 ## 信息架构与窗口模型 -使用一个主窗口和一个只在首次确认事件出现时显示的警报弹窗。主窗口不使用多级导航:客户演示的主要任务是观看实时画面并理解事件证据,设置与历史管理不是此轮原型的重点。 +使用一个主窗口和一个只在首次确认事件出现时显示的警报弹窗。主窗口以顶部水平双 Tab 在实时监控与设置之间切换;默认进入实时监控。当前只有两个固定目的地,顶部 Tab 比左侧导航更节省视频的横向空间。 ```text 主窗口 ├── 标题与来源栏:产品名、摄像头名称、连接状态、当前时间 -├── 演示状态切换:正常监控 / 确认摔倒 / 连接异常 -├── 实时画面:视频占位、人员 ID、骨架、每人状态与 FPS -├── 事件证据栏:状态说明、触发条件、报警副作用、截图路径 -└── 最近事件:最近一次事件、保存结果和操作入口 +├── 顶部 Tab:实时监控 / 设置 +├── 实时监控:演示状态切换、视频占位、人员 ID、骨架、事件证据和最近事件 +└── 设置:来源就绪状态、模型只读信息、事件参数、重连参数和配置应用状态 确认摔倒弹窗 ├── 明确的“确认摔倒”标题和人员 ID @@ -33,6 +32,7 @@ 大于等于 1008 epx 时,内容采用 8:4 双栏:左侧为固定 16:9 比例的实时画面,右侧为状态和证据卡;最近事件横跨底部。641–1007 epx 时,右栏缩窄并保留画面优先级;小于等于 640 epx 时,右栏移至画面下方,顶部操作换行,保证状态与主操作仍可见。 - 顶部栏高度 56 epx;使用系统标题栏相近的紧凑密度。 +- Tab 位于标题栏下方,活动 Tab 同时用底边、文字和 aria-selected 表示;键盘左右方向键可在两个 Tab 间移动。 - 内容间距使用 4 epx 网格,常用间距为 8、12、16、24 epx。 - 状态切换按钮显式带文字,普通鼠标、触摸、键盘均可触发。 - 确认状态在画面上放置红色边框、文字标签和事件编号,不仅改变颜色。 @@ -48,6 +48,14 @@ | 关闭告警弹窗 | “我已知晓” | `QDialogButtonBox` / `QPushButton` | 只关闭视图,不清除事件或停止报警记录。 | | 最近事件 | 只读列表 | `QListView` 或轻量卡片 | 正式版读取 JSONL 摘要,不作为数据库表格。 | +## Tab 与设置控件 + +| 目的 | 原型控件 | 最终 PyQt5 对应 | 行为 | +| --- | --- | --- | --- | +| 切换顶层视图 | 两个顶部 Tab | QTabWidget 或 QToolButton 加 QStackedWidget | 实时监控默认显示;设置不抢占视频宽度。 | +| 编辑设置草稿 | 数字输入与只读状态卡 | QDoubleSpinBox、QSpinBox、QLabel | 修改立即标记为编辑中,不影响运行配置。 | +| 保存并用于下次启动 | 主操作按钮 | QPushButton | 校验通过后显示已保存待下次启动;不热改监控中的阈值。 | + ## 视觉令牌 采用克制的深色控制台主题,以视频画面和告警信息为主。最终 PyQt5 应将这些令牌集中到 QSS/主题资源,不在控件中散落硬编码色值。 @@ -79,6 +87,8 @@ ## 无障碍与输入 +- 顶部 Tab 使用标准 tablist、tab 和 tabpanel 语义;左右方向键切换顶部 Tab,切换后焦点停在新 Tab。 +- Tab 顺序:顶部 Tab → 监控状态切换 → 启动/停止 → 查看截图 → 最近事件;设置 Tab 中按视觉顺序经过来源、模型、事件参数、保存操作。 - 所有状态同时包含颜色、图标形状和中文文字;状态文字对比度不低于 4.5:1。 - Tab 顺序:状态切换 → 启动/停止 → 查看截图 → 最近事件 → 弹窗确认按钮。 - `1`、`2`、`3` 是原型专用演示快捷键;Escape 只关闭可关闭的弹窗。 @@ -96,3 +106,4 @@ 3. 点击“连接异常”或按 `3` 后,仅显示重连状态,且明确说明未触发报警。 4. 宽屏、约 900 epx 宽和约 600 epx 宽均不出现横向溢出或无法触达的主操作。 5. 仅用键盘可切换三种状态、关闭弹窗并看到可见焦点。 +6. 点击设置 Tab 或按左右方向键可切换视图;设置页不显示来源地址或密码,编辑参数后显示已修改,保存后显示下次启动生效。 diff --git a/docs/ui/silver-pose-v1-prototype.html b/docs/ui/silver-pose-v1-prototype.html index 7d3244f..ebd2fc3 100644 --- a/docs/ui/silver-pose-v1-prototype.html +++ b/docs/ui/silver-pose-v1-prototype.html @@ -221,6 +221,76 @@ white-space: nowrap; } + .top-tabs { + display: flex; + align-items: end; + gap: 4px; + margin-top: var(--space-4); + padding: 6px 8px 0; + border: 1px solid rgba(65, 93, 132, 0.75); + border-radius: var(--radius-card) var(--radius-card) 0 0; + background: rgba(19, 31, 50, 0.74); + box-shadow: var(--shadow-card); + } + + .top-tab { + position: relative; + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 42px; + padding: 10px 14px; + border: 0; + border-radius: 8px 8px 0 0; + color: var(--text-secondary); + background: transparent; + font-size: 13px; + } + + .top-tab::after { + position: absolute; + right: 12px; + bottom: 0; + left: 12px; + height: 3px; + border-radius: 3px 3px 0 0; + background: transparent; + content: ""; + } + + .top-tab:hover { + color: var(--text-primary); + background: rgba(52, 80, 118, 0.5); + transform: none; + } + + .top-tab[aria-selected="true"] { + color: var(--text-primary); + background: rgba(31, 53, 82, 0.9); + } + + .top-tab[aria-selected="true"]::after { + background: var(--accent); + } + + .top-tab svg { + width: 17px; + height: 17px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.8; + } + + .workspace { + margin-top: 0; + } + + .tab-panel[hidden] { + display: none; + } + .app-main { display: grid; grid-template-columns: minmax(0, 2fr) minmax(330px, 0.94fr); @@ -229,7 +299,7 @@ "monitor details" "events events"; gap: var(--space-4); - margin-top: var(--space-4); + padding-top: var(--space-4); } .toolbar, @@ -251,6 +321,24 @@ padding: 10px 12px; } + .monitor-controls { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + } + + .version-chip { + padding: 6px 8px; + border: 1px solid var(--stroke); + border-radius: 999px; + color: var(--text-secondary); + background: rgba(8, 17, 30, 0.28); + font-family: var(--font-number); + font-size: 10px; + white-space: nowrap; + } + .switcher { display: flex; align-items: center; @@ -1158,6 +1246,335 @@ box-shadow: none; } + .settings-panel { + display: grid; + gap: var(--space-4); + padding-top: var(--space-4); + } + + .settings-intro { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-4); + padding: 18px 20px; + border: 1px solid rgba(65, 93, 132, 0.75); + border-radius: var(--radius-card); + background: rgba(19, 31, 50, 0.88); + box-shadow: var(--shadow-card); + } + + .settings-intro h1 { + margin-top: 2px; + font-size: 20px; + } + + .settings-intro p { + max-width: 690px; + margin-top: 7px; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.55; + } + + .config-state { + display: inline-flex; + align-items: center; + gap: 8px; + flex: 0 0 auto; + padding: 9px 11px; + border: 1px solid rgba(73, 209, 125, 0.34); + border-radius: 9px; + color: #dfffea; + background: rgba(20, 66, 43, 0.48); + font-size: 12px; + font-weight: 650; + } + + .config-state-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--success); + } + + .settings-panel[data-config-state="dirty"] .config-state { + border-color: rgba(244, 201, 93, 0.55); + color: #fff3cc; + background: rgba(86, 65, 21, 0.44); + } + + .settings-panel[data-config-state="dirty"] .config-state-dot { + background: var(--caution); + } + + .settings-panel[data-config-state="saved"] .config-state { + border-color: rgba(112, 165, 255, 0.56); + color: #e3edff; + background: rgba(32, 64, 111, 0.44); + } + + .settings-panel[data-config-state="saved"] .config-state-dot { + background: var(--accent); + } + + .settings-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--space-4); + } + + .settings-card { + overflow: hidden; + border: 1px solid rgba(65, 93, 132, 0.75); + border-radius: var(--radius-card); + background: rgba(19, 31, 50, 0.88); + box-shadow: var(--shadow-card); + } + + .settings-card.wide { + grid-column: 1 / -1; + } + + .settings-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-3); + padding: 16px; + border-bottom: 1px solid rgba(65, 93, 132, 0.52); + } + + .settings-card-header h2 { + margin-top: 2px; + } + + .settings-card-header p { + margin-top: 4px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.45; + } + + .settings-card-body { + padding: 16px; + } + + .readiness-row { + display: grid; + grid-template-columns: 36px minmax(0, 1fr); + gap: 10px; + align-items: center; + padding: 11px; + border: 1px solid rgba(73, 209, 125, 0.3); + border-radius: 9px; + background: rgba(12, 47, 31, 0.38); + } + + .readiness-icon { + display: grid; + width: 36px; + height: 36px; + place-items: center; + border-radius: 50%; + color: var(--success); + background: rgba(73, 209, 125, 0.14); + } + + .readiness-icon svg { + width: 18px; + height: 18px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; + } + + .readiness-row strong, + .readiness-row span { + display: block; + } + + .readiness-row strong { + color: var(--text-primary); + font-size: 13px; + } + + .readiness-row span { + margin-top: 3px; + color: var(--text-secondary); + font-family: var(--font-number); + font-size: 11px; + overflow-wrap: anywhere; + } + + .settings-note { + margin-top: 10px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.55; + } + + .metadata-list { + display: grid; + gap: 9px; + } + + .metadata-row { + display: flex; + justify-content: space-between; + gap: 16px; + padding-bottom: 9px; + border-bottom: 1px solid rgba(65, 93, 132, 0.4); + font-size: 12px; + } + + .metadata-row:last-child { + padding-bottom: 0; + border-bottom: 0; + } + + .metadata-row span { + color: var(--text-secondary); + } + + .metadata-row strong { + overflow-wrap: anywhere; + color: var(--text-primary); + font-family: var(--font-number); + font-size: 11px; + font-weight: 650; + text-align: right; + } + + .field-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + } + + .field { + display: grid; + gap: 6px; + } + + .field label { + color: var(--text-primary); + font-size: 12px; + font-weight: 650; + } + + .field-help { + min-height: 30px; + color: var(--text-subtle); + font-size: 11px; + line-height: 1.35; + } + + .number-control { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + overflow: hidden; + border: 1px solid var(--stroke-strong); + border-radius: 8px; + background: rgba(8, 17, 30, 0.32); + } + + .number-control:focus-within { + border-color: var(--focus); + box-shadow: 0 0 0 2px rgba(169, 201, 255, 0.24); + } + + .number-control input { + min-width: 0; + height: 38px; + padding: 6px 9px; + color: var(--text-primary); + border: 0; + outline: 0; + background: transparent; + font: 600 13px var(--font-number); + } + + .number-control span { + padding-right: 10px; + color: var(--text-secondary); + font-size: 11px; + } + + .settings-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: 14px 16px; + border: 1px solid rgba(65, 93, 132, 0.75); + border-radius: var(--radius-card); + background: rgba(19, 31, 50, 0.88); + box-shadow: var(--shadow-card); + } + + .config-version { + display: grid; + gap: 3px; + } + + .config-version span { + color: var(--text-secondary); + font-size: 12px; + } + + .config-version strong { + color: var(--text-primary); + font-family: var(--font-number); + font-size: 12px; + } + + .settings-button-group { + display: flex; + align-items: center; + gap: 10px; + } + + .subtle-button { + border-color: var(--stroke-strong); + background: var(--surface-raised); + color: var(--text-primary); + font-size: 12px; + } + + .subtle-button:hover { + border-color: var(--accent); + background: #213653; + } + + .save-button { + display: inline-flex; + align-items: center; + gap: 8px; + border-color: #5488e3; + background: var(--accent); + color: #071224; + font-size: 12px; + } + + .save-button:hover { + border-color: var(--accent-hover); + background: var(--accent-hover); + } + + .save-button svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; + } + @media (max-width: 1007px) { .shell { width: min(100%, calc(100% - 28px)); @@ -1188,7 +1605,9 @@ @media (max-width: 760px) { .app-chrome, - .toolbar { + .toolbar, + .settings-intro, + .settings-actions { align-items: flex-start; flex-wrap: wrap; } @@ -1216,6 +1635,18 @@ .details .card:last-child { grid-column: 1 / -1; } + + .settings-intro .config-state { + width: 100%; + } + + .field-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .settings-actions { + align-items: stretch; + } } @media (max-width: 640px) { @@ -1248,6 +1679,19 @@ gap: 10px; } + .top-tabs { + margin-top: 10px; + padding-right: 5px; + padding-left: 5px; + overflow-x: auto; + } + + .top-tab { + flex: 1 0 auto; + justify-content: center; + min-width: 150px; + } + .switcher { width: 100%; overflow-x: auto; @@ -1265,6 +1709,15 @@ width: 100%; } + .monitor-controls { + width: 100%; + flex-wrap: wrap; + } + + .monitor-controls .primary-action { + flex: 1; + } + .monitor-header, .card-header, .event-log-header { @@ -1322,6 +1775,34 @@ .event-row:last-child { border-bottom: 0; } + + .settings-intro, + .settings-card-header, + .settings-card-body, + .settings-actions { + padding-right: 12px; + padding-left: 12px; + } + + .settings-grid { + grid-template-columns: 1fr; + } + + .settings-card.wide { + grid-column: auto; + } + + .field-grid { + grid-template-columns: 1fr; + } + + .settings-button-group { + width: 100%; + } + + .settings-button-group button { + flex: 1; + } } @media (max-width: 600px) { @@ -1400,7 +1881,19 @@ -
+ + +
+
演示状态 @@ -1417,10 +1910,13 @@ 3 连接异常
- +
+ 运行配置 cfg-20260720-01 + +
@@ -1563,6 +2059,106 @@
+
+ +