docs: add settings tab to UI prototype
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
| 姿态与人员跟踪 | 全身可见人员显示骨架和稳定人员 ID;质量不足时明确为不确定而非报警。 | P0 |
|
| 姿态与人员跟踪 | 全身可见人员显示骨架和稳定人员 ID;质量不足时明确为不确定而非报警。 | P0 |
|
||||||
| 突发倒地事件 | 同一人员从正常姿态快速转为倒地且持续时,在 1–3 秒内确认事件。 | P0 |
|
| 突发倒地事件 | 同一人员从正常姿态快速转为倒地且持续时,在 1–3 秒内确认事件。 | P0 |
|
||||||
| 本地报警 | 确认事件后画面变红、播放声音、弹出窗口并保存标注截图。 | P0 |
|
| 本地报警 | 确认事件后画面变红、播放声音、弹出窗口并保存标注截图。 | P0 |
|
||||||
|
| 安全设置与配置版本 | 操作员可在设置页编辑非敏感事件参数,保存后仅在下一次开始监控时生效,监控与事件工件显示实际配置版本。 | P0 |
|
||||||
| 演示回归 | 同一组正反例录像可重复验证报警、无报警和延迟。 | P0 |
|
| 演示回归 | 同一组正反例录像可重复验证报警、无报警和延迟。 | P0 |
|
||||||
|
|
||||||
## 后续迭代
|
## 后续迭代
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
3. 当成年人安全模拟从站立或行走突然倒地时,系统在 1–3 秒内对该人员显示红色状态、播放声音、弹窗并保存截图。
|
3. 当成年人安全模拟从站立或行走突然倒地时,系统在 1–3 秒内对该人员显示红色状态、播放声音、弹窗并保存截图。
|
||||||
4. 当人员坐下、弯腰、捡物或短暂停留时,系统不把该动作确认成摔倒事件。
|
4. 当人员坐下、弯腰、捡物或短暂停留时,系统不把该动作确认成摔倒事件。
|
||||||
5. 当同一人员持续倒地时,系统保持事件状态但不重复弹窗;恢复稳定站立后可回到正常状态。
|
5. 当同一人员持续倒地时,系统保持事件状态但不重复弹窗;恢复稳定站立后可回到正常状态。
|
||||||
|
6. 作为演示操作员,我可以在设置页检查来源环境变量是否就绪、调整非敏感事件参数,并知道这些改动会在下一次开始监控时生效。
|
||||||
|
|
||||||
## MVP 验收标准
|
## MVP 验收标准
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
- **突发摔倒**:每段经过批准的正例录像或现场动作,报警延迟记录在 1–3 秒范围内。
|
- **突发摔倒**:每段经过批准的正例录像或现场动作,报警延迟记录在 1–3 秒范围内。
|
||||||
- **反例**:每段坐下、弯腰、捡物和短暂停留录像均不产生确认事件。
|
- **反例**:每段坐下、弯腰、捡物和短暂停留录像均不产生确认事件。
|
||||||
- **报警闭环**:每个确认事件只产生一次声音和弹窗,并保存一张含时间、ID、骨架和事件状态的截图。
|
- **报警闭环**:每个确认事件只产生一次声音和弹窗,并保存一张含时间、ID、骨架和事件状态的截图。
|
||||||
|
- **配置可追溯**:设置页不显示真实来源地址或密码;保存的草稿与正在监控的配置版本明确区分,事件记录包含实际生效的配置版本。
|
||||||
- **可复现性**:验收录像、配置版本、事件结果和实际命令保存在项目约定位置或 `progress.md`,可在同一机器重复运行。
|
- **可复现性**:验收录像、配置版本、事件结果和实际命令保存在项目约定位置或 `progress.md`,可在同一机器重复运行。
|
||||||
|
|
||||||
## 范围与风险
|
## 范围与风险
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ V1 的同一数据流既可接 RTSP,也可回放本地录像。V2 复用同一
|
|||||||
| PyQt UI | `v1/gui.py` | 渲染帧、骨架、状态、设置和弹窗 | 直接读 RTSP 或写判定规则 |
|
| PyQt UI | `v1/gui.py` | 渲染帧、骨架、状态、设置和弹窗 | 直接读 RTSP 或写判定规则 |
|
||||||
| 回归工具 | `v1/tests/` 与 `v1/scripts/` | 回放录像、断言事件和延迟 | 生产 UI |
|
| 回归工具 | `v1/tests/` 与 `v1/scripts/` | 回放录像、断言事件和延迟 | 生产 UI |
|
||||||
|
|
||||||
|
## UI 导航与配置生效生命周期
|
||||||
|
|
||||||
|
PyQt 主窗口只包含实时监控和设置两个顶部 Tab。实时监控 Tab 保持视频画面优先;设置 Tab 不能直接调用视频源、Pose 或状态机。
|
||||||
|
|
||||||
|
设置页面产生的是已校验的草稿配置。用户点击开始监控时,应用创建不可变的运行配置快照与配置版本,并将该快照传给视频源、Pose、证据和状态机。运行期间编辑设置不会修改该快照;保存后的草稿在下一次开始监控时才会成为新的运行配置。FallEvent 继续记录 config_version,使截图和 JSONL 可以追溯到实际阈值。
|
||||||
|
|
||||||
|
真实 RTSP 凭证仍只由环境变量或未跟踪本地配置提供。UI 只显示环境变量是否就绪,不能回显或记录具体值。
|
||||||
|
|
||||||
## 状态模型
|
## 状态模型
|
||||||
|
|
||||||
每个 `track_id` 独立维护状态:
|
每个 `track_id` 独立维护状态:
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
| 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-202 | 实现本地报警、弹窗、截图和 JSONL 日志 | T-201 | CONFIRMED 产生一次声音、一次弹窗、一张带标注截图和一条事件日志。 | TODO |
|
||||||
| T-203 | 接入海康 RTSP 与断流恢复 | T-202 | 现场有效流可预览;断流状态明确、可重连且不报警。 | TODO |
|
| T-203 | 接入海康 RTSP 与断流恢复 | T-202 | 现场有效流可预览;断流状态明确、可重连且不报警。 | TODO |
|
||||||
| T-204 | 建立正反例录像回归与现场验收记录 | T-203 | 摔倒、行走、坐下、弯腰、捡物和持续倒地都有预期结果;报警延迟记录在 1–3 秒。 | TODO |
|
| T-204 | 建立正反例录像回归与现场验收记录 | T-203 | 摔倒、行走、坐下、弯腰、捡物和持续倒地都有预期结果;报警延迟记录在 1–3 秒。 | TODO |
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- 旧生产基线:`demo/main.py`、`demo/fall_detection_gui.py`、`demo/detect_fall.py`、`demo/best.pt`。
|
- 旧生产基线:`demo/main.py`、`demo/fall_detection_gui.py`、`demo/detect_fall.py`、`demo/best.pt`。
|
||||||
- V1 代码:`v1/` 目录存在但尚无实现。
|
- V1 代码:`v1/` 目录存在但尚无实现。
|
||||||
- V2 代码:`v2/` 目录存在但尚无实现。
|
- 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` 已通过。
|
- 测试:暂无 pytest 测试;`python -m compileall -q demo` 已通过。
|
||||||
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
||||||
- 当前标准启动:`./init.ps1`。
|
- 当前标准启动:`./init.ps1`。
|
||||||
|
|||||||
+17
-1
@@ -1,6 +1,6 @@
|
|||||||
# 桌面页面与交互结构
|
# 桌面页面与交互结构
|
||||||
|
|
||||||
> V1 是单窗口 PyQt5 桌面应用,没有 Web 路由。本文件规定页面职责和导航,V2 选择本地网页壳或桌面壳时必须保持同等用户流程。
|
> V1 是单窗口 PyQt5 桌面应用,没有 Web 路由。主窗口使用顶部水平双 Tab;V2 选择本地网页壳或桌面壳时必须保持同等用户流程。
|
||||||
|
|
||||||
## 页面 / 视图
|
## 页面 / 视图
|
||||||
|
|
||||||
@@ -12,6 +12,21 @@
|
|||||||
| 事件提示区 | 显示最近事件和截图保存结果;V1 不做长期历史数据库。 |
|
| 事件提示区 | 显示最近事件和截图保存结果;V1 不做长期历史数据库。 |
|
||||||
| 连接错误提示 | 显示断流或配置错误以及可重试状态;不得使用“正常”掩盖错误。 |
|
| 连接错误提示 | 显示断流或配置错误以及可重试状态;不得使用“正常”掩盖错误。 |
|
||||||
|
|
||||||
|
## 顶层导航
|
||||||
|
|
||||||
|
顶部 Tab 只有两个固定目的地:实时监控和设置。默认进入实时监控,因为客户演示的主任务是看见画面与报警闭环。使用顶部 Tab 而非左侧导航,避免在 V1 的单路 16:9 视频旁占用横向空间;只有后续增加回放、诊断、设备管理等多个稳定模块时才评估左侧导航。
|
||||||
|
|
||||||
|
- 实时监控 Tab:视频、姿态、人员状态、事件证据、最近事件和报警弹窗。
|
||||||
|
- 设置 Tab:来源就绪状态、模型只读信息、事件时间窗、质量门槛、重连策略和配置版本。
|
||||||
|
|
||||||
|
## 设置视图
|
||||||
|
|
||||||
|
- 来源区域只显示环境变量名称与“已就绪 / 未就绪”,不得显示真实地址、账号或密码。
|
||||||
|
- 模型区域只读显示模型名称、哈希摘要、关键点格式与验证日期;模型替换不属于演示操作。
|
||||||
|
- 事件参数使用带单位和范围说明的数字控件:关键点质量阈值、疑似时间、确认时间、恢复时间和冷却时间。
|
||||||
|
- 配置编辑分为编辑中、已保存待下次启动、正在监控已生效三种文字状态,不能只靠颜色区分。
|
||||||
|
- 保存与应用操作只写入未跟踪的本地配置;当前运行的事件引擎继续使用开始监控时冻结的配置版本。
|
||||||
|
|
||||||
## 实时监控视图
|
## 实时监控视图
|
||||||
|
|
||||||
- 绿色:`NORMAL`。
|
- 绿色:`NORMAL`。
|
||||||
@@ -28,3 +43,4 @@
|
|||||||
3. 修改事件阈值后,UI 显示的值必须是下一次开始监控真正使用的配置版本。
|
3. 修改事件阈值后,UI 显示的值必须是下一次开始监控真正使用的配置版本。
|
||||||
4. 断流时显示错误与重连状态;恢复连接不自动生成摔倒事件。
|
4. 断流时显示错误与重连状态;恢复连接不自动生成摔倒事件。
|
||||||
5. 截图保存失败时,仍保留红色报警并在提示区记录错误。
|
5. 截图保存失败时,仍保留红色报警并在提示区记录错误。
|
||||||
|
6. 设置修改不会热改正在运行的事件阈值;操作员保存后可在监控页看到下一次启动将使用的版本。
|
||||||
|
|||||||
@@ -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.
|
> **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.
|
**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
|
## 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.
|
- 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.
|
- 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.
|
- Consistency: the three controller state names are consistently `normal`, `confirmed`, and `offline`; the dialog and acknowledgement element IDs are fixed across layout and behavior.
|
||||||
|
|||||||
@@ -7,20 +7,19 @@
|
|||||||
- 目标框架:Windows 上的 PyQt5;HTML 仅作离线交互视觉原型,不替代生产 UI。
|
- 目标框架:Windows 上的 PyQt5;HTML 仅作离线交互视觉原型,不替代生产 UI。
|
||||||
- 默认窗口:1280 × 800 epx;支持最小 960 × 640、Windows Snap 宽度和 125%/150% 缩放。
|
- 默认窗口:1280 × 800 epx;支持最小 960 × 640、Windows Snap 宽度和 125%/150% 缩放。
|
||||||
- 语言:简体中文;数据均为虚构演示数据,不包含 RTSP 地址、账号、客户图像或真实事件。
|
- 语言:简体中文;数据均为虚构演示数据,不包含 RTSP 地址、账号、客户图像或真实事件。
|
||||||
- 范围:单路摄像头、实时监控、确认摔倒告警、连接异常提示和最近事件。
|
- 范围:单路摄像头、实时监控、确认摔倒告警、连接异常提示、最近事件和非敏感设置的交互原型。
|
||||||
- 不在范围:真实 RTSP、模型推理、音频播放、文件系统写入、设置编辑、云端通知及历史事件库。
|
- 不在范围:真实 RTSP、模型推理、音频播放、文件系统写入、真实配置持久化、云端通知及历史事件库。
|
||||||
|
|
||||||
## 信息架构与窗口模型
|
## 信息架构与窗口模型
|
||||||
|
|
||||||
使用一个主窗口和一个只在首次确认事件出现时显示的警报弹窗。主窗口不使用多级导航:客户演示的主要任务是观看实时画面并理解事件证据,设置与历史管理不是此轮原型的重点。
|
使用一个主窗口和一个只在首次确认事件出现时显示的警报弹窗。主窗口以顶部水平双 Tab 在实时监控与设置之间切换;默认进入实时监控。当前只有两个固定目的地,顶部 Tab 比左侧导航更节省视频的横向空间。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
主窗口
|
主窗口
|
||||||
├── 标题与来源栏:产品名、摄像头名称、连接状态、当前时间
|
├── 标题与来源栏:产品名、摄像头名称、连接状态、当前时间
|
||||||
├── 演示状态切换:正常监控 / 确认摔倒 / 连接异常
|
├── 顶部 Tab:实时监控 / 设置
|
||||||
├── 实时画面:视频占位、人员 ID、骨架、每人状态与 FPS
|
├── 实时监控:演示状态切换、视频占位、人员 ID、骨架、事件证据和最近事件
|
||||||
├── 事件证据栏:状态说明、触发条件、报警副作用、截图路径
|
└── 设置:来源就绪状态、模型只读信息、事件参数、重连参数和配置应用状态
|
||||||
└── 最近事件:最近一次事件、保存结果和操作入口
|
|
||||||
|
|
||||||
确认摔倒弹窗
|
确认摔倒弹窗
|
||||||
├── 明确的“确认摔倒”标题和人员 ID
|
├── 明确的“确认摔倒”标题和人员 ID
|
||||||
@@ -33,6 +32,7 @@
|
|||||||
大于等于 1008 epx 时,内容采用 8:4 双栏:左侧为固定 16:9 比例的实时画面,右侧为状态和证据卡;最近事件横跨底部。641–1007 epx 时,右栏缩窄并保留画面优先级;小于等于 640 epx 时,右栏移至画面下方,顶部操作换行,保证状态与主操作仍可见。
|
大于等于 1008 epx 时,内容采用 8:4 双栏:左侧为固定 16:9 比例的实时画面,右侧为状态和证据卡;最近事件横跨底部。641–1007 epx 时,右栏缩窄并保留画面优先级;小于等于 640 epx 时,右栏移至画面下方,顶部操作换行,保证状态与主操作仍可见。
|
||||||
|
|
||||||
- 顶部栏高度 56 epx;使用系统标题栏相近的紧凑密度。
|
- 顶部栏高度 56 epx;使用系统标题栏相近的紧凑密度。
|
||||||
|
- Tab 位于标题栏下方,活动 Tab 同时用底边、文字和 aria-selected 表示;键盘左右方向键可在两个 Tab 间移动。
|
||||||
- 内容间距使用 4 epx 网格,常用间距为 8、12、16、24 epx。
|
- 内容间距使用 4 epx 网格,常用间距为 8、12、16、24 epx。
|
||||||
- 状态切换按钮显式带文字,普通鼠标、触摸、键盘均可触发。
|
- 状态切换按钮显式带文字,普通鼠标、触摸、键盘均可触发。
|
||||||
- 确认状态在画面上放置红色边框、文字标签和事件编号,不仅改变颜色。
|
- 确认状态在画面上放置红色边框、文字标签和事件编号,不仅改变颜色。
|
||||||
@@ -48,6 +48,14 @@
|
|||||||
| 关闭告警弹窗 | “我已知晓” | `QDialogButtonBox` / `QPushButton` | 只关闭视图,不清除事件或停止报警记录。 |
|
| 关闭告警弹窗 | “我已知晓” | `QDialogButtonBox` / `QPushButton` | 只关闭视图,不清除事件或停止报警记录。 |
|
||||||
| 最近事件 | 只读列表 | `QListView` 或轻量卡片 | 正式版读取 JSONL 摘要,不作为数据库表格。 |
|
| 最近事件 | 只读列表 | `QListView` 或轻量卡片 | 正式版读取 JSONL 摘要,不作为数据库表格。 |
|
||||||
|
|
||||||
|
## Tab 与设置控件
|
||||||
|
|
||||||
|
| 目的 | 原型控件 | 最终 PyQt5 对应 | 行为 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 切换顶层视图 | 两个顶部 Tab | QTabWidget 或 QToolButton 加 QStackedWidget | 实时监控默认显示;设置不抢占视频宽度。 |
|
||||||
|
| 编辑设置草稿 | 数字输入与只读状态卡 | QDoubleSpinBox、QSpinBox、QLabel | 修改立即标记为编辑中,不影响运行配置。 |
|
||||||
|
| 保存并用于下次启动 | 主操作按钮 | QPushButton | 校验通过后显示已保存待下次启动;不热改监控中的阈值。 |
|
||||||
|
|
||||||
## 视觉令牌
|
## 视觉令牌
|
||||||
|
|
||||||
采用克制的深色控制台主题,以视频画面和告警信息为主。最终 PyQt5 应将这些令牌集中到 QSS/主题资源,不在控件中散落硬编码色值。
|
采用克制的深色控制台主题,以视频画面和告警信息为主。最终 PyQt5 应将这些令牌集中到 QSS/主题资源,不在控件中散落硬编码色值。
|
||||||
@@ -79,6 +87,8 @@
|
|||||||
|
|
||||||
## 无障碍与输入
|
## 无障碍与输入
|
||||||
|
|
||||||
|
- 顶部 Tab 使用标准 tablist、tab 和 tabpanel 语义;左右方向键切换顶部 Tab,切换后焦点停在新 Tab。
|
||||||
|
- Tab 顺序:顶部 Tab → 监控状态切换 → 启动/停止 → 查看截图 → 最近事件;设置 Tab 中按视觉顺序经过来源、模型、事件参数、保存操作。
|
||||||
- 所有状态同时包含颜色、图标形状和中文文字;状态文字对比度不低于 4.5:1。
|
- 所有状态同时包含颜色、图标形状和中文文字;状态文字对比度不低于 4.5:1。
|
||||||
- Tab 顺序:状态切换 → 启动/停止 → 查看截图 → 最近事件 → 弹窗确认按钮。
|
- Tab 顺序:状态切换 → 启动/停止 → 查看截图 → 最近事件 → 弹窗确认按钮。
|
||||||
- `1`、`2`、`3` 是原型专用演示快捷键;Escape 只关闭可关闭的弹窗。
|
- `1`、`2`、`3` 是原型专用演示快捷键;Escape 只关闭可关闭的弹窗。
|
||||||
@@ -96,3 +106,4 @@
|
|||||||
3. 点击“连接异常”或按 `3` 后,仅显示重连状态,且明确说明未触发报警。
|
3. 点击“连接异常”或按 `3` 后,仅显示重连状态,且明确说明未触发报警。
|
||||||
4. 宽屏、约 900 epx 宽和约 600 epx 宽均不出现横向溢出或无法触达的主操作。
|
4. 宽屏、约 900 epx 宽和约 600 epx 宽均不出现横向溢出或无法触达的主操作。
|
||||||
5. 仅用键盘可切换三种状态、关闭弹窗并看到可见焦点。
|
5. 仅用键盘可切换三种状态、关闭弹窗并看到可见焦点。
|
||||||
|
6. 点击设置 Tab 或按左右方向键可切换视图;设置页不显示来源地址或密码,编辑参数后显示已修改,保存后显示下次启动生效。
|
||||||
|
|||||||
@@ -221,6 +221,76 @@
|
|||||||
white-space: nowrap;
|
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 {
|
.app-main {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 2fr) minmax(330px, 0.94fr);
|
grid-template-columns: minmax(0, 2fr) minmax(330px, 0.94fr);
|
||||||
@@ -229,7 +299,7 @@
|
|||||||
"monitor details"
|
"monitor details"
|
||||||
"events events";
|
"events events";
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
margin-top: var(--space-4);
|
padding-top: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar,
|
.toolbar,
|
||||||
@@ -251,6 +321,24 @@
|
|||||||
padding: 10px 12px;
|
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 {
|
.switcher {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1158,6 +1246,335 @@
|
|||||||
box-shadow: none;
|
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) {
|
@media (max-width: 1007px) {
|
||||||
.shell {
|
.shell {
|
||||||
width: min(100%, calc(100% - 28px));
|
width: min(100%, calc(100% - 28px));
|
||||||
@@ -1188,7 +1605,9 @@
|
|||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.app-chrome,
|
.app-chrome,
|
||||||
.toolbar {
|
.toolbar,
|
||||||
|
.settings-intro,
|
||||||
|
.settings-actions {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
@@ -1216,6 +1635,18 @@
|
|||||||
.details .card:last-child {
|
.details .card:last-child {
|
||||||
grid-column: 1 / -1;
|
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) {
|
@media (max-width: 640px) {
|
||||||
@@ -1248,6 +1679,19 @@
|
|||||||
gap: 10px;
|
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 {
|
.switcher {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -1265,6 +1709,15 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monitor-controls {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-controls .primary-action {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.monitor-header,
|
.monitor-header,
|
||||||
.card-header,
|
.card-header,
|
||||||
.event-log-header {
|
.event-log-header {
|
||||||
@@ -1322,6 +1775,34 @@
|
|||||||
.event-row:last-child {
|
.event-row:last-child {
|
||||||
border-bottom: 0;
|
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) {
|
@media (max-width: 600px) {
|
||||||
@@ -1400,7 +1881,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="app-main">
|
<nav class="top-tabs" role="tablist" aria-label="Silver Pose 主界面">
|
||||||
|
<button id="monitor-tab" class="top-tab" type="button" role="tab" aria-selected="true" aria-controls="monitor-panel" tabindex="0" data-tab-target="monitor">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"></rect><path d="m9 9 6 3-6 3Z"></path></svg>
|
||||||
|
<span>实时监控</span>
|
||||||
|
</button>
|
||||||
|
<button id="settings-tab" class="top-tab" type="button" role="tab" aria-selected="false" aria-controls="settings-panel" tabindex="-1" data-tab-target="settings">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.04 2.04-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.04 1.56v.1h-2.88v-.1a1.7 1.7 0 0 0-1.04-1.56 1.7 1.7 0 0 0-1.88.34l-.06.06-2.04-2.04.06-.06A1.7 1.7 0 0 0 7.28 15a1.7 1.7 0 0 0-1.56-1.04h-.1v-2.88h.1A1.7 1.7 0 0 0 7.28 10a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.04-2.04.06.06a1.7 1.7 0 0 0 1.88.34 1.7 1.7 0 0 0 1.04-1.56v-.1h2.88v.1A1.7 1.7 0 0 0 15.82 6.4a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.04 2.04-.06.06A1.7 1.7 0 0 0 19.4 10a1.7 1.7 0 0 0 1.56 1.04h.1v2.88h-.1A1.7 1.7 0 0 0 19.4 15Z"></path></svg>
|
||||||
|
<span>设置</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="workspace">
|
||||||
|
<section id="monitor-panel" class="tab-panel app-main" role="tabpanel" aria-labelledby="monitor-tab">
|
||||||
<section class="toolbar" aria-label="演示控制">
|
<section class="toolbar" aria-label="演示控制">
|
||||||
<div class="switcher" role="group" aria-label="演示状态切换">
|
<div class="switcher" role="group" aria-label="演示状态切换">
|
||||||
<span class="switcher-label">演示状态</span>
|
<span class="switcher-label">演示状态</span>
|
||||||
@@ -1417,10 +1910,13 @@
|
|||||||
<span>3 连接异常</span>
|
<span>3 连接异常</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="monitor-toggle" class="primary-action" type="button" aria-pressed="true">
|
<div class="monitor-controls">
|
||||||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"></rect><path d="m9 9 6 3-6 3Z"></path></svg>
|
<span id="monitor-config-version" class="version-chip">运行配置 cfg-20260720-01</span>
|
||||||
<span>停止模拟监控</span>
|
<button id="monitor-toggle" class="primary-action" type="button" aria-pressed="true">
|
||||||
</button>
|
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2"></rect><path d="m9 9 6 3-6 3Z"></path></svg>
|
||||||
|
<span>停止模拟监控</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="monitor-card" aria-labelledby="monitor-title">
|
<section class="monitor-card" aria-labelledby="monitor-title">
|
||||||
@@ -1563,6 +2059,106 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="settings-panel" class="tab-panel settings-panel" role="tabpanel" aria-labelledby="settings-tab" hidden data-config-state="clean">
|
||||||
|
<header class="settings-intro">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">CONFIGURATION · 非敏感演示设置</span>
|
||||||
|
<h1>设置</h1>
|
||||||
|
<p>编辑的是下一次开始监控时将使用的草稿配置。当前监控仍固定使用已显示的运行配置版本,以保证事件截图和日志可追溯。</p>
|
||||||
|
</div>
|
||||||
|
<span id="config-state" class="config-state" role="status"><span class="config-state-dot" aria-hidden="true"></span><span id="config-state-text">草稿与运行配置一致</span></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form id="settings-form" novalidate>
|
||||||
|
<div class="settings-grid">
|
||||||
|
<section class="settings-card" aria-labelledby="source-settings-title">
|
||||||
|
<header class="settings-card-header">
|
||||||
|
<div><span class="eyebrow">SOURCE</span><h2 id="source-settings-title">来源状态</h2><p>安全配置只证明本机来源已就绪。</p></div>
|
||||||
|
<span class="quiet-badge">不回显真实来源</span>
|
||||||
|
</header>
|
||||||
|
<div class="settings-card-body">
|
||||||
|
<div class="readiness-row">
|
||||||
|
<span class="readiness-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 12.5 9.1 16.5 19 7"></path></svg></span>
|
||||||
|
<div><strong>来源环境变量已就绪</strong><span>SILVER_POSE_RTSP_URL · 仅显示变量名</span></div>
|
||||||
|
</div>
|
||||||
|
<p class="settings-note">正式应用从本机环境变量或未跟踪的本地配置读取来源信息;该页面不显示、记录或复制真实连接内容。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-card" aria-labelledby="model-settings-title">
|
||||||
|
<header class="settings-card-header">
|
||||||
|
<div><span class="eyebrow">MODEL</span><h2 id="model-settings-title">模型信息</h2><p>仅供操作员确认受控工件状态。</p></div>
|
||||||
|
<span class="quiet-badge">只读</span>
|
||||||
|
</header>
|
||||||
|
<div class="settings-card-body">
|
||||||
|
<div class="metadata-list">
|
||||||
|
<div class="metadata-row"><span>任务</span><strong>person pose</strong></div>
|
||||||
|
<div class="metadata-row"><span>关键点格式</span><strong>17 × 3</strong></div>
|
||||||
|
<div class="metadata-row"><span>哈希摘要</span><strong>cd200948…bfa fdfc</strong></div>
|
||||||
|
<div class="metadata-row"><span>验证状态</span><strong>本机基线已检查</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-card wide" aria-labelledby="event-settings-title">
|
||||||
|
<header class="settings-card-header">
|
||||||
|
<div><span class="eyebrow">EVENT LOGIC</span><h2 id="event-settings-title">事件参数草稿</h2><p>范围用于演示配置;最终数值必须通过正反例录像和现场机位验证。</p></div>
|
||||||
|
<span class="quiet-badge">下次启动生效</span>
|
||||||
|
</header>
|
||||||
|
<div class="settings-card-body">
|
||||||
|
<div class="field-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label for="keypoint-threshold">关键点质量门槛</label>
|
||||||
|
<span id="keypoint-threshold-help" class="field-help">关节低于此值时不推进事件证据。</span>
|
||||||
|
<span class="number-control"><input id="keypoint-threshold" name="keypoint-threshold" type="number" min="0.10" max="0.90" step="0.05" value="0.40" aria-describedby="keypoint-threshold-help"><span>0–1</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="suspect-window">疑似持续时间</label>
|
||||||
|
<span class="field-help">倒地候选持续后保持疑似状态的时间窗。</span>
|
||||||
|
<span class="number-control"><input id="suspect-window" name="suspect-window" type="number" min="0.10" max="2.00" step="0.10" value="0.50"><span>秒</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="confirm-window">确认时间</label>
|
||||||
|
<span class="field-help">目标演示报警时延的一部分,需保持在 1–3 秒目标内。</span>
|
||||||
|
<span class="number-control"><input id="confirm-window" name="confirm-window" type="number" min="0.50" max="3.00" step="0.10" value="1.00"><span>秒</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="recovery-window">恢复稳定时间</label>
|
||||||
|
<span class="field-help">人员恢复后回到正常状态前的持续时间。</span>
|
||||||
|
<span class="number-control"><input id="recovery-window" name="recovery-window" type="number" min="0.50" max="10.00" step="0.10" value="2.00"><span>秒</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="cooldown-window">事件冷却时间</label>
|
||||||
|
<span class="field-help">同一人员恢复前不重复创建确认事件。</span>
|
||||||
|
<span class="number-control"><input id="cooldown-window" name="cooldown-window" type="number" min="1" max="60" step="1" value="10"><span>秒</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="reconnect-delay">重连间隔</label>
|
||||||
|
<span class="field-help">仅影响视频源重试节奏,不可作为事件证据。</span>
|
||||||
|
<span class="number-control"><input id="reconnect-delay" name="reconnect-delay" type="number" min="1" max="30" step="1" value="4"><span>秒</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="settings-actions">
|
||||||
|
<div class="config-version">
|
||||||
|
<span id="draft-version-label">草稿版本</span>
|
||||||
|
<strong id="draft-version">cfg-20260720-01 · 未修改</strong>
|
||||||
|
</div>
|
||||||
|
<div class="settings-button-group">
|
||||||
|
<button id="reset-settings" class="subtle-button" type="button">恢复演示默认值</button>
|
||||||
|
<button id="save-settings" class="save-button" type="submit">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h12l2 2v14H5Z"></path><path d="M8 4v6h8V4M8 19v-5h8v5"></path></svg>
|
||||||
|
<span>保存并用于下次启动</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<p class="footer-note">
|
<p class="footer-note">
|
||||||
@@ -1599,15 +2195,30 @@
|
|||||||
|
|
||||||
var app = document.querySelector("#app");
|
var app = document.querySelector("#app");
|
||||||
var stateButtons = Array.prototype.slice.call(document.querySelectorAll("[data-state-target]"));
|
var stateButtons = Array.prototype.slice.call(document.querySelectorAll("[data-state-target]"));
|
||||||
|
var topTabs = Array.prototype.slice.call(document.querySelectorAll("[data-tab-target]"));
|
||||||
|
var monitorPanel = document.querySelector("#monitor-panel");
|
||||||
|
var settingsPanel = document.querySelector("#settings-panel");
|
||||||
var dialog = document.querySelector("#fall-dialog");
|
var dialog = document.querySelector("#fall-dialog");
|
||||||
var acknowledgeButton = document.querySelector("#acknowledge-button");
|
var acknowledgeButton = document.querySelector("#acknowledge-button");
|
||||||
var monitorToggle = document.querySelector("#monitor-toggle");
|
var monitorToggle = document.querySelector("#monitor-toggle");
|
||||||
var artifactButton = document.querySelector("#artifact-button");
|
var artifactButton = document.querySelector("#artifact-button");
|
||||||
|
var settingsForm = document.querySelector("#settings-form");
|
||||||
|
var settingsInputs = Array.prototype.slice.call(settingsForm.querySelectorAll("input"));
|
||||||
|
var resetSettingsButton = document.querySelector("#reset-settings");
|
||||||
|
var configStateText = document.querySelector("#config-state-text");
|
||||||
|
var draftVersion = document.querySelector("#draft-version");
|
||||||
var toast = document.querySelector("#toast");
|
var toast = document.querySelector("#toast");
|
||||||
var liveAnnouncement = document.querySelector("#live-announcement");
|
var liveAnnouncement = document.querySelector("#live-announcement");
|
||||||
var lastInvoker = null;
|
var lastInvoker = null;
|
||||||
var confirmedDialogShown = false;
|
var confirmedDialogShown = false;
|
||||||
var toastTimer = null;
|
var toastTimer = null;
|
||||||
|
var activeTab = "monitor";
|
||||||
|
var draftRevision = 1;
|
||||||
|
var settingDefaults = {};
|
||||||
|
|
||||||
|
settingsInputs.forEach(function (input) {
|
||||||
|
settingDefaults[input.id] = input.value;
|
||||||
|
});
|
||||||
|
|
||||||
var views = {
|
var views = {
|
||||||
normal: {
|
normal: {
|
||||||
@@ -1739,6 +2350,49 @@
|
|||||||
document.querySelector(id).textContent = value;
|
document.querySelector(id).textContent = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setActiveTab(nextTab, moveFocus) {
|
||||||
|
if (nextTab !== "monitor" && nextTab !== "settings") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeTab = nextTab;
|
||||||
|
topTabs.forEach(function (tab) {
|
||||||
|
var selected = tab.dataset.tabTarget === nextTab;
|
||||||
|
tab.setAttribute("aria-selected", String(selected));
|
||||||
|
tab.setAttribute("tabindex", selected ? "0" : "-1");
|
||||||
|
if (selected && moveFocus) {
|
||||||
|
tab.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
monitorPanel.hidden = nextTab !== "monitor";
|
||||||
|
settingsPanel.hidden = nextTab !== "settings";
|
||||||
|
liveAnnouncement.textContent = nextTab === "settings"
|
||||||
|
? "已切换至设置。编辑的是草稿配置,保存后在下一次开始监控时生效。"
|
||||||
|
: "已切换至实时监控。当前运行配置保持不变。";
|
||||||
|
}
|
||||||
|
|
||||||
|
function markConfigDirty() {
|
||||||
|
settingsPanel.dataset.configState = "dirty";
|
||||||
|
configStateText.textContent = "设置已修改,尚未保存";
|
||||||
|
draftVersion.textContent = "cfg-20260720-0" + (draftRevision + 1) + " · 编辑中";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSettings() {
|
||||||
|
settingsInputs.forEach(function (input) {
|
||||||
|
input.value = settingDefaults[input.id];
|
||||||
|
});
|
||||||
|
markConfigDirty();
|
||||||
|
showToast("已恢复演示默认值;请保存后在下一次启动监控时应用。");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
draftRevision += 1;
|
||||||
|
settingsPanel.dataset.configState = "saved";
|
||||||
|
configStateText.textContent = "草稿已保存,下一次启动生效";
|
||||||
|
draftVersion.textContent = "cfg-20260720-0" + draftRevision + " · 已保存待应用";
|
||||||
|
liveAnnouncement.textContent = "设置草稿已保存。当前监控仍使用 cfg-20260720-01,下一次开始监控时才使用新版本。";
|
||||||
|
showToast("演示草稿已保存;运行配置未被热修改。");
|
||||||
|
}
|
||||||
|
|
||||||
function setDemoState(nextState, invoker) {
|
function setDemoState(nextState, invoker) {
|
||||||
var view = views[nextState];
|
var view = views[nextState];
|
||||||
if (!view) {
|
if (!view) {
|
||||||
@@ -1806,6 +2460,23 @@
|
|||||||
setText("#scene-time", time);
|
setText("#scene-time", time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
topTabs.forEach(function (tab) {
|
||||||
|
tab.addEventListener("click", function () {
|
||||||
|
setActiveTab(tab.dataset.tabTarget, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsInputs.forEach(function (input) {
|
||||||
|
input.addEventListener("input", markConfigDirty);
|
||||||
|
});
|
||||||
|
|
||||||
|
resetSettingsButton.addEventListener("click", resetSettings);
|
||||||
|
|
||||||
|
settingsForm.addEventListener("submit", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
stateButtons.forEach(function (button) {
|
stateButtons.forEach(function (button) {
|
||||||
button.addEventListener("click", function () {
|
button.addEventListener("click", function () {
|
||||||
setDemoState(button.dataset.stateTarget, button);
|
setDemoState(button.dataset.stateTarget, button);
|
||||||
@@ -1843,8 +2514,19 @@
|
|||||||
if (event.altKey || event.ctrlKey || event.metaKey) {
|
if (event.altKey || event.ctrlKey || event.metaKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var tabIndex = topTabs.indexOf(document.activeElement);
|
||||||
|
if (tabIndex !== -1 && (event.key === "ArrowLeft" || event.key === "ArrowRight")) {
|
||||||
|
event.preventDefault();
|
||||||
|
var direction = event.key === "ArrowRight" ? 1 : -1;
|
||||||
|
var nextIndex = (tabIndex + direction + topTabs.length) % topTabs.length;
|
||||||
|
setActiveTab(topTabs[nextIndex].dataset.tabTarget, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (document.activeElement && document.activeElement.matches("input, textarea, select")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var stateMap = { "1": "normal", "2": "confirmed", "3": "offline" };
|
var stateMap = { "1": "normal", "2": "confirmed", "3": "offline" };
|
||||||
if (stateMap[event.key]) {
|
if (activeTab === "monitor" && stateMap[event.key]) {
|
||||||
var targetButton = document.querySelector("[data-state-target='" + stateMap[event.key] + "']");
|
var targetButton = document.querySelector("[data-state-target='" + stateMap[event.key] + "']");
|
||||||
setDemoState(stateMap[event.key], targetButton);
|
setDemoState(stateMap[event.key], targetButton);
|
||||||
}
|
}
|
||||||
@@ -1853,6 +2535,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (window.location.hash === "#settings") {
|
||||||
|
setActiveTab("settings", false);
|
||||||
|
}
|
||||||
updateClock();
|
updateClock();
|
||||||
window.setInterval(updateClock, 1000);
|
window.setInterval(updateClock, 1000);
|
||||||
}());
|
}());
|
||||||
|
|||||||
@@ -44,3 +44,12 @@
|
|||||||
- 阻塞:无。该文件是离线视觉原型,已做静态和无界面浏览器布局验证,但不表示实时 RTSP、模型推理、声音播放或截图保存已实现。
|
- 阻塞:无。该文件是离线视觉原型,已做静态和无界面浏览器布局验证,但不表示实时 RTSP、模型推理、声音播放或截图保存已实现。
|
||||||
- 决策:原型采用单窗口深色控制台布局,以同一画面状态切换展示客户关心的“正常不报警、确认后完整报警闭环、断流不误报”三种结果。
|
- 决策:原型采用单窗口深色控制台布局,以同一画面状态切换展示客户关心的“正常不报警、确认后完整报警闭环、断流不误报”三种结果。
|
||||||
- 下一步:T-101;HTML 原型可作为后续 T-201 PyQt 界面实现的视觉与交互参考。
|
- 下一步:T-101;HTML 原型可作为后续 T-201 PyQt 界面实现的视觉与交互参考。
|
||||||
|
|
||||||
|
## 【2026-07-20】DESIGN 顶部 Tab 与设置原型修订
|
||||||
|
|
||||||
|
- 状态:DONE
|
||||||
|
- 变更:更新需求、页面导航、架构、T-201 验收、UI 规格和实施计划;将 HTML 原型改为顶部双 Tab。实时监控 Tab 保留姿态与报警演示;设置 Tab 显示来源环境变量就绪、模型只读信息、六项非敏感事件或重连参数、草稿版本与保存/重置操作。
|
||||||
|
- 验证:基线 ./init.ps1 通过;Node.js 22 解析唯一内联脚本,并检查 tablist、tab、tabpanel、设置表单、保存操作、运行配置版本、配置草稿控制器和三档响应式断点;敏感与网络边界扫描未发现凭证、模型路径或网络行为标记。Chrome 无界面模式实际渲染 1280 宽监控页、1280/900/600 宽设置页;宽屏与 600 宽截图已人工检查,未见横向溢出。
|
||||||
|
- 阻塞:无。设置页只模拟草稿编辑和保存反馈,不会写入本地配置,也不会热改监控中的运行配置。
|
||||||
|
- 决策:V1 固定采用顶部双 Tab,而非左侧导航;两个静态目的地不会挤压实时视频。设置草稿在下一次开始监控时才成为新的运行配置,事件工件继续追溯实际运行版本。
|
||||||
|
- 下一步:T-101;后续 T-201 按此交互和配置快照边界实现 PyQt 界面。
|
||||||
|
|||||||
Reference in New Issue
Block a user