Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
027227d253 | ||
|
|
1d8f775226 | ||
|
|
31209ecb8b | ||
|
|
242efe8ee7 | ||
|
|
61d428a2df | ||
|
|
8997348847 | ||
|
|
6b970ccf19 | ||
|
|
45d316dcbf | ||
|
|
67df831bfe | ||
|
|
34bd2f937b | ||
|
|
0e574fe12c | ||
|
|
1e724e9731 | ||
|
|
0ce30e53a4 | ||
|
|
be2c1e3c81 | ||
|
|
8486d90001 | ||
|
|
2285fb0a6d | ||
|
|
7dac43436a | ||
|
|
6ff12e6a27 |
@@ -116,6 +116,11 @@ Gitea 不可用时,输出完整工单草稿并说明阻塞。未经用户明
|
||||
|
||||
## 6. Git 与验证
|
||||
|
||||
- `explore` 是旧实现的只读聚合快照,不接受新功能、修复或文档演进;需要恢复旧行为时从其来源提交读取证据,不在该分支续写产品。
|
||||
- `main` 是用户审核基线,禁止直接开发、直接提交或未经用户明确审核的合并。
|
||||
- 新任务分支必须从当前 `dev` 创建,完成后通过 PR 合回 `dev`;不得把功能分支直接合入 `main`。
|
||||
- `dev` 完成集成测试后仍不能自动进入 `main`;只有用户明确表示审核/验收通过,Agent 才能执行 `dev → main`。
|
||||
- 紧急修复也必须建单并取得用户对分支与合并路径的明确授权,不默认绕过 `dev`。
|
||||
- 提交只包含当前工单相关文件。
|
||||
- 实现提交信息引用工单号,例如:`fix: 修复登录超时 (#123)`。
|
||||
- 不为流程制造空提交。
|
||||
@@ -180,6 +185,30 @@ MVP 内所有单元任务通过后才能做 MVP 集成验收;MVP 通过后才
|
||||
- 16 路是默认交付配额,不得成为数据库、数组、循环、分页、批处理或单机容量的硬上限。
|
||||
- 开始子项目工作前还必须读取对应目录的 `AGENTS.md`;共享契约工作读取 `contracts/AGENTS.md`。
|
||||
|
||||
### go-admin / go-admin-ui 精简与复用
|
||||
|
||||
- Sense、Bell 使用 go-admin 和 go-admin-ui 时遵循“最小可见、最小启用”:只呈现当前产品需要的菜单、路由、权限和接口,不把框架演示或无关管理能力暴露给用户。
|
||||
- 快速交付阶段优先使用配置、菜单权限或路由开关隐藏不用模块。隐藏只移除用户入口,不等于禁用;权限或安全相关能力必须同时关闭前端入口与后端访问能力。
|
||||
- 永久删除默认模块必须单独建立清理工单,先核对代码依赖、数据库对象、权限记录、构建影响、框架升级影响和回退方式,不在业务页面工单中顺手删除。
|
||||
- 新页面优先复用现有布局、表格、表单、弹窗、上传、权限控制和请求封装;现有组件无法满足明确需求时才新增组件,并在工单中说明复用缺口。
|
||||
- 新组件必须沿用项目现有的颜色、字号、间距、状态反馈、交互方式、命名和目录结构;不得引入与整体风格冲突的独立视觉体系。
|
||||
- 不为单个页面直接改变底层公共组件的全局行为。确需全局修改或同时影响 Sense、Bell 时,必须单独建单并明确受影响页面、兼容方式和回归验证;跨产品影响按协同工单处理。
|
||||
- UI 验收至少检查:没有多余入口、没有可见但不可用的失效入口、没有重复实现已有组件,并且视觉与交互保持一致。
|
||||
|
||||
### Sense/Bell GoAdmin 技术基线
|
||||
|
||||
- `goadmin-baseline.json` 是 Sense、Bell 共用的 GoAdmin 上游与工具链版本事实来源;上游 URL 加完整 commit 是可复现标识,tag、`package.json` version、本机目录内容或本机已安装版本都不能替代 commit。
|
||||
- `D:\github\goadmin\go-admin`、`go-admin-ui`、`go-admin-doc` 是当前机器的只读参考副本。Agent 可以读取和验证,不得在这些目录开发、修改或提交 yovision 产品代码。
|
||||
- “基于 go-admin / go-admin-ui 二次开发”必须表现为从 `goadmin-baseline.json` 指定的两个完整上游 commit 导入或派生产品源码,并保留可核对的目录结构、框架启动链、通用认证/RBAC、菜单路由、请求封装及许可证来源;仅采用 Go、Vue、Element Plus、相似目录、页面外观或交互不算满足基线。
|
||||
- 禁止用自建 `net/http`、自建认证/RBAC/会话框架或自制管理端外壳替代 go-admin/go-admin-ui 后仍声称是 GoAdmin 二次开发。确有上游能力无法满足时,必须在独立工单中说明缺口、保留上游扩展边界并等待用户确认。
|
||||
- 每个 Sense/Bell 骨架、基础能力或框架相关工单开始实施前,必须读取任务相关的冻结 `go-admin-doc` 内容,并在工单中记录参考的文档主题或文件、继承的上游路径以及计划隐藏/禁用的默认模块。
|
||||
- 初始化验收必须包含来源证据:上游 URL/commit、实际导入路径、保留的 MIT 文件、相对上游的裁剪/修改清单和构建测试;缺少任一项不得标记为 GoAdmin 骨架完成。
|
||||
- Sense、Bell 必须从同一冻结基线分别初始化,保留独立代码、配置、构建、测试、版本和发布;不得通过共同修改参考仓库形成隐藏的共享实现。
|
||||
- 复制或派生上游代码时必须保留对应 MIT 许可证和版权声明,并记录实际复制来源的 commit。
|
||||
- `go-admin-doc` 只用于理解上游框架用法;yovision 的 Gitea Wiki、`AGENTS.md` 和 `goadmin-baseline.json` 仍是本项目事实来源。
|
||||
- 变更任一上游 commit、Go、Node 或 pnpm 版本必须建立 Sense/Bell 协同升级工单,验证两个产品后再更新基线;不得在单端业务工单中静默升级。
|
||||
- 当前冻结后端要求 Go 1.26.5;本机 Go 1.23.0 不满足要求,安装隔离的正确工具链并完成构建验证前,不得声称后端基线可构建。
|
||||
|
||||
### 三项目并行建单顺序
|
||||
|
||||
客户交付需要 Sense、Brain、Bell 并行推进时,固定使用以下顺序:
|
||||
|
||||
@@ -14,3 +14,21 @@
|
||||
- 每个工单必须写明任务类型、主项目 `Bell`、主 agent、精确 `write_paths`、独立运行方式、测试命令和在 Sense/Brain 未启动时使用合成或第三方事件的验收标准。
|
||||
- 首轮并行建单只创建 Bell 独立工单;发现需要外部事件/证据契约、producer identity、Sense connector、根级部署或端到端测试时,停止扩写本工单,向主 agent 提交协同需求摘要。
|
||||
- 协同需求摘要至少包含生产者、消费者、接口目的、候选事实源、阻塞的 Bell 工单和建议验证;Bell agent 不直接创建契约实现或取得 `contracts/`、`Sense/`、`Brain/` 的写入权。
|
||||
|
||||
## go-admin / go-admin-ui 约束
|
||||
|
||||
- Bell 管理面只启用事件预警、告警处置和通知运维所需的默认能力;框架演示页和无关管理模块优先通过配置、菜单权限或路由开关隐藏。
|
||||
- 涉及权限或安全的不用模块不能只隐藏菜单,必须同时验证前端入口和后端接口均不可访问;永久删除默认模块必须单独建立清理工单。
|
||||
- 新页面优先复用 Bell/go-admin-ui 已有的布局、表格、表单、弹窗、上传、权限控制和请求封装;新增组件前在工单中说明现有组件为何不能满足需求。
|
||||
- 新增组件沿用 Bell 现有颜色、字号、间距、状态反馈、交互方式、命名和目录结构,并验证没有多余入口、失效入口或重复组件。
|
||||
- 修改底层公共组件的全局行为时,必须单独建单并列出所有受影响页面;同时影响 Sense 或共享基线时升级为协同工单。
|
||||
|
||||
## GoAdmin 技术基线
|
||||
|
||||
- 初始化或升级 Bell 前必须读取根 `goadmin-baseline.json`,按其中的 go-admin、go-admin-ui commit 和 Go/Node/pnpm 精确版本实施。
|
||||
- Bell 必须由冻结 go-admin 后端与 go-admin-ui 前端源码直接派生;旧 `explore` 实现只能用于业务语义对照,禁止把其中自研认证、RBAC、会话、HTTP 平台或前端外壳复制为新基础。
|
||||
- 开始每个骨架、认证权限、菜单路由、代码生成、ORM 或通用 UI 工单前,必须读取任务相关的冻结 go-admin-doc,并把参考主题/文件及采用方式写回工单。
|
||||
- 骨架验收必须证明上游来源、继承路径、许可证、裁剪清单和相对上游差异;只有相似技术栈或视觉不能通过验收。
|
||||
- `D:\github\goadmin` 下的仓库仅可只读参考;Bell 产品代码只能写入 `Bell/` 或工单授权路径,不得在参考仓库原地开发。
|
||||
- 派生上游代码必须保留 MIT 许可证和版权声明;上游 `go-admin-doc` 只作框架用法参考,不覆盖 yovision Wiki 和本目录规则。
|
||||
- 变更冻结基线必须升级为 Sense/Bell 协同工单;Bell 可以独立发布,但不得单独漂移上游或工具链版本。
|
||||
|
||||
@@ -9,3 +9,12 @@ YoVision 是一个单仓多项目的智能视频事件平台,包含三个可
|
||||
Sense 与 Bell 是账户、数据、部署和发布边界完全独立的两个销售产品;Brain 是无界面的独立推理交付单元,默认随 Sense 部署。跨项目协作只通过 `contracts/` 中的版本化契约。
|
||||
|
||||
开始工作前阅读 [AGENTS.md](AGENTS.md) 和 [文档中心](docs/README.md)。本仓库采用 DevHarness:Gitea 工单记录任务过程,Gitea Wiki 保存长期文档,`docs/` 是 Wiki 的只读镜像。
|
||||
|
||||
## 分支模型
|
||||
|
||||
- `explore`:2026-08-14 前实现的只读聚合快照,用于迁移对照和回退。
|
||||
- `main`:用户审核通过的基线,禁止直接开发。
|
||||
- `dev`:集成开发与测试分支;任务分支从 `dev` 创建并通过 PR 返回 `dev`。
|
||||
- 只有用户明确审核通过,才能将 `dev` 合入 `main`。
|
||||
|
||||
Sense 与 Bell 的新实现必须从根 `goadmin-baseline.json` 冻结的 go-admin 和 go-admin-ui 完整提交派生,开发时必须核对对应 commit 的 go-admin-doc。仅参考 GoAdmin 的技术栈、布局或视觉不属于本项目认可的二次开发。
|
||||
|
||||
@@ -14,3 +14,21 @@
|
||||
- 每个工单必须写明任务类型、主项目 `Sense`、主 agent、精确 `write_paths`、独立运行方式、测试命令和在 Bell/Brain 未启动时的验收标准。
|
||||
- 首轮并行建单只创建 Sense 独立工单;发现需要 Sense→Brain 源/配置契约、Sense/Brain→Bell 事件/证据契约、根级部署或端到端测试时,停止扩写本工单,向主 agent 提交协同需求摘要。
|
||||
- 协同需求摘要至少包含生产者、消费者、接口目的、候选事实源、阻塞的 Sense 工单和建议验证;Sense agent 不直接创建契约实现或取得 `contracts/`、`Brain/`、`Bell/` 的写入权。
|
||||
|
||||
## go-admin / go-admin-ui 约束
|
||||
|
||||
- Sense 管理面只启用智能 NVR、视频感知和本地运维所需的默认能力;框架演示页和无关管理模块优先通过配置、菜单权限或路由开关隐藏。
|
||||
- 涉及权限或安全的不用模块不能只隐藏菜单,必须同时验证前端入口和后端接口均不可访问;永久删除默认模块必须单独建立清理工单。
|
||||
- 新页面优先复用 Sense/go-admin-ui 已有的布局、表格、表单、弹窗、上传、权限控制和请求封装;新增组件前在工单中说明现有组件为何不能满足需求。
|
||||
- 新增组件沿用 Sense 现有颜色、字号、间距、状态反馈、交互方式、命名和目录结构,并验证没有多余入口、失效入口或重复组件。
|
||||
- 修改底层公共组件的全局行为时,必须单独建单并列出所有受影响页面;同时影响 Bell 或共享基线时升级为协同工单。
|
||||
|
||||
## GoAdmin 技术基线
|
||||
|
||||
- 初始化或升级 Sense 前必须读取根 `goadmin-baseline.json`,按其中的 go-admin、go-admin-ui commit 和 Go/Node/pnpm 精确版本实施。
|
||||
- Sense 必须由冻结 go-admin 后端与 go-admin-ui 前端源码直接派生;旧 `explore` 实现只能用于需求和行为对照,禁止把其中自研 HTTP、认证、RBAC、会话或前端外壳复制为新基础。
|
||||
- 开始每个骨架、认证权限、菜单路由、代码生成、ORM 或通用 UI 工单前,必须读取任务相关的冻结 go-admin-doc,并把参考主题/文件及采用方式写回工单。
|
||||
- 骨架验收必须证明上游来源、继承路径、许可证、裁剪清单和相对上游差异;只有相似技术栈或视觉不能通过验收。
|
||||
- `D:\github\goadmin` 下的仓库仅可只读参考;Sense 产品代码只能写入 `Sense/` 或工单授权路径,不得在参考仓库原地开发。
|
||||
- 派生上游代码必须保留 MIT 许可证和版权声明;上游 `go-admin-doc` 只作框架用法参考,不覆盖 yovision Wiki 和本目录规则。
|
||||
- 变更冻结基线必须升级为 Sense/Bell 协同工单;Sense 可以独立发布,但不得单独漂移上游或工具链版本。
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
@@ -47,9 +48,11 @@ CORE_DOCUMENT_REQUIREMENTS = {
|
||||
"## 阅读入口",
|
||||
"## 常用命令",
|
||||
"## 环境、配置与凭据",
|
||||
"## Sense/Bell GoAdmin 固定技术基线",
|
||||
),
|
||||
"docs/01-workflow.md": (
|
||||
"## 面向初级维护者的修改边界",
|
||||
"## go-admin / go-admin-ui 开发约束",
|
||||
"## 每个任务的文档影响",
|
||||
"## 需求记录与流转",
|
||||
"## 稳定文档与任务归档",
|
||||
@@ -74,6 +77,7 @@ CORE_DOCUMENT_REQUIREMENTS = {
|
||||
),
|
||||
"docs/04-local-development-and-verification.md": (
|
||||
"## 环境要求",
|
||||
"## Sense/Bell 固定工具链与只读参考源",
|
||||
"## 第一次运行",
|
||||
"## 常用调试方式",
|
||||
"## 完成修改前",
|
||||
@@ -139,6 +143,7 @@ REQUIRED_FILES = (
|
||||
"docs/templates/task-archive.md",
|
||||
*CORE_DOCUMENT_REQUIREMENTS,
|
||||
"wiki-docs.json",
|
||||
"goadmin-baseline.json",
|
||||
"dev_scripts/wiki_docs.py",
|
||||
"dev_scripts/sync_wiki_docs.py",
|
||||
".gitea/issue_template/epic.md",
|
||||
@@ -286,6 +291,170 @@ def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None:
|
||||
errors.append(f"AGENTS.md 缺少:{section}")
|
||||
|
||||
|
||||
def check_go_admin_ui_rules(errors: list[str], root: Path = ROOT) -> None:
|
||||
"""检查 Sense、Bell 共用的框架精简和组件复用规则。"""
|
||||
|
||||
requirements = {
|
||||
"AGENTS.md": (
|
||||
"### go-admin / go-admin-ui 精简与复用",
|
||||
"最小可见、最小启用",
|
||||
"隐藏只移除用户入口,不等于禁用",
|
||||
"永久删除默认模块必须单独建立清理工单",
|
||||
"新页面优先复用现有布局",
|
||||
"不得引入与整体风格冲突的独立视觉体系",
|
||||
"跨产品影响按协同工单处理",
|
||||
"没有可见但不可用的失效入口",
|
||||
),
|
||||
"Sense/AGENTS.md": (
|
||||
"## go-admin / go-admin-ui 约束",
|
||||
"菜单权限或路由开关隐藏",
|
||||
"不能只隐藏菜单",
|
||||
"新增组件前在工单中说明",
|
||||
"同时影响 Bell 或共享基线时升级为协同工单",
|
||||
),
|
||||
"Bell/AGENTS.md": (
|
||||
"## go-admin / go-admin-ui 约束",
|
||||
"菜单权限或路由开关隐藏",
|
||||
"不能只隐藏菜单",
|
||||
"新增组件前在工单中说明",
|
||||
"同时影响 Sense 或共享基线时升级为协同工单",
|
||||
),
|
||||
}
|
||||
for relative_path, required in requirements.items():
|
||||
path = root / relative_path
|
||||
if not path.is_file():
|
||||
errors.append(f"缺少 go-admin 规则文件:{relative_path}")
|
||||
continue
|
||||
content = path.read_text(encoding="utf-8")
|
||||
for section in missing_sections(content, required):
|
||||
errors.append(f"{relative_path} 缺少 go-admin 规则:{section}")
|
||||
|
||||
|
||||
GOADMIN_BASELINE_REQUIREMENTS = {
|
||||
"runtimes": {
|
||||
"go": "1.26.5",
|
||||
"node": "22.22.1",
|
||||
"package_manager": "pnpm",
|
||||
"pnpm": "9.15.1",
|
||||
},
|
||||
"sources": {
|
||||
"go-admin": {
|
||||
"commit": "f06540883b41d03782bb6b2c4150f298f328c6b6",
|
||||
"upstream_url": "https://github.com/go-admin-team/go-admin.git",
|
||||
"branch": "master",
|
||||
"describe": "v2.2.0-25-gf065408",
|
||||
"exact_tag": None,
|
||||
"local_reference_path": r"D:\github\goadmin\go-admin",
|
||||
"license": "MIT",
|
||||
},
|
||||
"go-admin-ui": {
|
||||
"commit": "67d393d713877572fab0b897296a4c1d525fc81d",
|
||||
"upstream_url": "https://github.com/go-admin-team/go-admin-ui.git",
|
||||
"branch": "master",
|
||||
"describe": "v2.0.9-170-g67d393d",
|
||||
"exact_tag": None,
|
||||
"local_reference_path": r"D:\github\goadmin\go-admin-ui",
|
||||
"package_version": "2.0.9",
|
||||
"package_version_is_authoritative": False,
|
||||
"license": "MIT",
|
||||
},
|
||||
"go-admin-doc": {
|
||||
"commit": "424855aacf6905f3fde860c3331385cb25529a0d",
|
||||
"upstream_url": "https://github.com/go-admin-team/go-admin-doc.git",
|
||||
"branch": "master",
|
||||
"describe": "424855a",
|
||||
"exact_tag": None,
|
||||
"local_reference_path": r"D:\github\goadmin\go-admin-doc",
|
||||
"package_version_is_authoritative": False,
|
||||
"license": "MIT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def check_goadmin_baseline(errors: list[str], root: Path = ROOT) -> None:
|
||||
"""检查 Sense/Bell 共用上游和工具链基线可复现且规则完整。"""
|
||||
|
||||
path = root / "goadmin-baseline.json"
|
||||
if not path.is_file():
|
||||
errors.append("缺少 GoAdmin 技术基线:goadmin-baseline.json")
|
||||
return
|
||||
try:
|
||||
baseline = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
errors.append(f"GoAdmin 技术基线不是有效 JSON:{exc}")
|
||||
return
|
||||
|
||||
for group, entries in GOADMIN_BASELINE_REQUIREMENTS.items():
|
||||
actual_group = baseline.get(group)
|
||||
if not isinstance(actual_group, dict):
|
||||
errors.append(f"GoAdmin 技术基线缺少对象:{group}")
|
||||
continue
|
||||
for name, expected in entries.items():
|
||||
actual = actual_group.get(name)
|
||||
if isinstance(expected, dict):
|
||||
if not isinstance(actual, dict):
|
||||
errors.append(f"GoAdmin 技术基线缺少来源:{name}")
|
||||
continue
|
||||
for field, value in expected.items():
|
||||
if actual.get(field) != value:
|
||||
errors.append(
|
||||
f"GoAdmin 技术基线不匹配:{name}.{field} 应为 {value}"
|
||||
)
|
||||
elif actual != expected:
|
||||
errors.append(
|
||||
f"GoAdmin 技术基线不匹配:{group}.{name} 应为 {expected}"
|
||||
)
|
||||
|
||||
if baseline.get("authority") != "upstream_url_and_commit":
|
||||
errors.append("GoAdmin 技术基线必须以上游 URL 和 commit 为权威标识")
|
||||
rules = baseline.get("rules")
|
||||
required_rules = (
|
||||
"local_repositories_are_read_only",
|
||||
"product_code_must_not_be_developed_in_reference_repositories",
|
||||
"preserve_upstream_license_and_copyright",
|
||||
"upgrades_require_coordination_issue",
|
||||
"sense_and_bell_release_independently",
|
||||
)
|
||||
if not isinstance(rules, dict):
|
||||
errors.append("GoAdmin 技术基线缺少 rules")
|
||||
else:
|
||||
for rule in required_rules:
|
||||
if rules.get(rule) is not True:
|
||||
errors.append(f"GoAdmin 技术基线规则必须为 true:{rule}")
|
||||
|
||||
agent_requirements = {
|
||||
"AGENTS.md": (
|
||||
"### Sense/Bell GoAdmin 技术基线",
|
||||
"`goadmin-baseline.json`",
|
||||
"上游 URL 加完整 commit 是可复现标识",
|
||||
"当前机器的只读参考副本",
|
||||
"必须建立 Sense/Bell 协同升级工单",
|
||||
"本机 Go 1.23.0 不满足要求",
|
||||
),
|
||||
"Sense/AGENTS.md": (
|
||||
"## GoAdmin 技术基线",
|
||||
"读取根 `goadmin-baseline.json`",
|
||||
"不得在参考仓库原地开发",
|
||||
"不得单独漂移上游或工具链版本",
|
||||
),
|
||||
"Bell/AGENTS.md": (
|
||||
"## GoAdmin 技术基线",
|
||||
"读取根 `goadmin-baseline.json`",
|
||||
"不得在参考仓库原地开发",
|
||||
"不得单独漂移上游或工具链版本",
|
||||
),
|
||||
}
|
||||
for relative_path, required in agent_requirements.items():
|
||||
agent_path = root / relative_path
|
||||
if not agent_path.is_file():
|
||||
errors.append(f"缺少 GoAdmin 基线规则文件:{relative_path}")
|
||||
continue
|
||||
content = agent_path.read_text(encoding="utf-8")
|
||||
for section in missing_sections(content, required):
|
||||
errors.append(f"{relative_path} 缺少 GoAdmin 基线规则:{section}")
|
||||
|
||||
|
||||
def check_claude_code_entry(errors: list[str], root: Path = ROOT) -> None:
|
||||
"""检查 Claude Code 入口直接复用共同 Agent 规则。"""
|
||||
|
||||
@@ -382,6 +551,8 @@ def main() -> int:
|
||||
check_core_documents(errors)
|
||||
check_task_template(errors)
|
||||
check_agent_efficiency_rules(errors)
|
||||
check_go_admin_ui_rules(errors)
|
||||
check_goadmin_baseline(errors)
|
||||
check_claude_code_entry(errors)
|
||||
check_archives(errors)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Project-Profile
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Project-Profile.-
|
||||
wiki_revision: 4a78dc8673d88b05f1c62b254242180a68465ccd
|
||||
synchronized_at: 2026-08-11T10:30:26Z
|
||||
wiki_revision: d32f00a86bb3127485b8ad4da8436bf2117352e0
|
||||
synchronized_at: 2026-08-14T01:06:22Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 项目档案
|
||||
@@ -19,7 +19,7 @@ synchronized_at: 2026-08-11T10:30:26Z
|
||||
| 首期客户场景 | 民办寄宿学校,默认 16 路高风险点位 |
|
||||
| 首期规则 | 越线、危险区域、聚集等匿名安全规则;不启用人脸 |
|
||||
| 产品形态 | Sense 与 Bell 两个独立销售产品,Brain 为独立推理交付单元 |
|
||||
| 当前阶段 | 新仓库文档与开发流程初始化,业务代码尚未迁移 |
|
||||
| 当前阶段 | 进入 GoAdmin 源码派生重建:旧实现归档于 `explore`,`main` 为审核基线,`dev` 为集成开发分支 |
|
||||
| 历史来源 | `D:\OPC\yovision_old`,只读追溯 |
|
||||
|
||||
## 子项目与交付单元
|
||||
@@ -42,7 +42,7 @@ synchronized_at: 2026-08-11T10:30:26Z
|
||||
- 证据:客户侧 MinIO/S3 兼容对象存储;常态录像优先留在客户已有 NVR。
|
||||
- 首期验证平台:NVIDIA x86/Jetson;M1-M3 不承诺 GB/T 28181、信创或原生 App。
|
||||
|
||||
业务代码尚未初始化,具体 Go/Python/Node 版本必须在对应骨架工单中冻结后更新本页。
|
||||
2026-08-14 起,原 Sense、Bell 实现只在 `explore` 和原功能分支中作为迁移参考,不再作为新开发基础。新的 Sense、Bell 必须从下述冻结 go-admin/go-admin-ui 完整提交派生;实施时必须核对冻结 go-admin-doc。Brain 尚未初始化。
|
||||
|
||||
## 阅读入口
|
||||
|
||||
@@ -78,3 +78,37 @@ Sense、Brain、Bell 的构建、运行和测试命令尚待各自骨架工单
|
||||
- 单仓包含 Sense、Brain、Bell 三个开发项目。
|
||||
- 计划由三名主 agent 尽量并行处理各自独立工单;需要跨项目协作时再建立协调工单并启用新 agent。
|
||||
- 旧仓库在新纵切验收前保留,不删除、不覆盖。
|
||||
|
||||
## Sense/Bell GoAdmin 固定技术基线
|
||||
|
||||
Sense、Bell 共用的可复现技术基线记录在仓库根 `goadmin-baseline.json`。正式版本标识是上游 URL 与完整 commit 的组合;本机目录、分支名、最近 tag 和 `package.json` version 只作辅助信息。
|
||||
|
||||
| 参考源 | 用途 | 冻结提交 | 描述版本 | 本机只读位置 |
|
||||
|---|---|---|---|---|
|
||||
| go-admin | 后端骨架 | `f06540883b41d03782bb6b2c4150f298f328c6b6` | `v2.2.0-25-gf065408` | `D:\github\goadmin\go-admin` |
|
||||
| go-admin-ui | 前端骨架 | `67d393d713877572fab0b897296a4c1d525fc81d` | `v2.0.9-170-g67d393d` | `D:\github\goadmin\go-admin-ui` |
|
||||
| go-admin-doc | 只读用法参考 | `424855aacf6905f3fde860c3331385cb25529a0d` | 无精确 tag | `D:\github\goadmin\go-admin-doc` |
|
||||
|
||||
固定工具链为 Go 1.26.5、Node.js 22.22.1、pnpm 9.15.1。go-admin-ui 的 `package.json` version 仍为 2.0.9,但冻结提交位于 `v2.0.9` 后 170 个提交,因此不得只用“2.0.9”代表当前基线。
|
||||
|
||||
三个上游仓库均使用 MIT 许可证。派生代码必须保留许可证和版权声明。Sense、Bell 使用同一上游基线,但分别维护代码、配置、构建、测试、版本和发布。任何上游或工具链升级必须建立影响 Sense/Bell 的协同工单。
|
||||
|
||||
<!-- sense-runtime:start -->
|
||||
## Sense 重建状态
|
||||
|
||||
当前可运行的旧 Sense 实现保存在 `explore`,不进入新 `main` / `dev` 基线。新 Sense 尚未初始化;后续必须从冻结的 go-admin 后端和 go-admin-ui 前端源码派生,保留上游骨架、许可证和来源证据,并在工单记录实际参考的 go-admin-doc 页面。
|
||||
<!-- sense-runtime:end -->
|
||||
|
||||
|
||||
<!-- bell-runtime:start -->
|
||||
## Bell 重建状态
|
||||
|
||||
当前 Bell MVP 实现在 `explore` 和原功能分支中保留,只作需求、行为和迁移对照。新 Bell 尚未初始化;后续必须从与 Sense 相同的冻结 go-admin/go-admin-ui 基线独立派生,不能复制旧自研基础框架或共享 Sense 的认证与数据库。
|
||||
<!-- bell-runtime:end -->
|
||||
|
||||
## 分支治理
|
||||
|
||||
- `explore`:2026-08-14 前实现的只读聚合快照;不接受新功能。
|
||||
- `main`:用户审核通过的最小/发布基线;禁止直接开发和未经用户明确审核的合并。
|
||||
- `dev`:集成开发与测试分支;功能分支从 `dev` 派生,并通过 PR 合回 `dev`。
|
||||
- 只有用户明确审核通过,才能把 `dev` 合入 `main`。
|
||||
|
||||
+48
-2
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Development-Workflow
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Development-Workflow.-
|
||||
wiki_revision: 5bb7c174f711aa5655cb8c9b28afed7cfacd60d5
|
||||
synchronized_at: 2026-08-11T10:30:31Z
|
||||
wiki_revision: 2bb60b79073439babfda05140ce813d197de81d9
|
||||
synchronized_at: 2026-08-14T01:06:22Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 开发工作流
|
||||
@@ -238,3 +238,49 @@ python dev_scripts/new_task_archive.py 123 "修复登录超时"
|
||||
| 测试结果与未验证内容 | 是 | 任务归档 | 镜像 |
|
||||
| 提交哈希 | 是 | 任务归档 | 镜像 |
|
||||
| 与具体代码版本绑定的说明 | 可链接 | 提供入口 | 是 |
|
||||
|
||||
## go-admin / go-admin-ui 开发约束
|
||||
|
||||
Sense、Bell 使用 go-admin 和 go-admin-ui 时,采用以下统一流程。“基于 GoAdmin”必须是源码派生,不得解释为仅参考技术栈、目录、页面风格或交互:
|
||||
|
||||
1. 初始化时从 `goadmin-baseline.json` 指定的 go-admin、go-admin-ui 完整 commit 导入/派生产品源码,保留 MIT 许可证、版权和来源清单;不得以自建 `net/http`、自建认证/RBAC 或仅使用 Vue + Element Plus 替代上游骨架。
|
||||
2. 实施工单开始前必须核对冻结 go-admin-doc,并在工单证据中记录参考的文档主题或文件;文档只解释上游用法,不改变本项目需求事实源。
|
||||
|
||||
在满足上述来源门禁后:
|
||||
|
||||
3. 先列出产品实际需要的菜单、路由、权限和接口,只呈现必要能力,框架演示页和无关管理模块不进入产品导航。
|
||||
4. 快速交付阶段优先使用配置、菜单权限或路由开关隐藏模块。隐藏只代表用户看不到入口;涉及权限或安全时,还必须禁用对应后端访问能力并验证直接请求不可用。
|
||||
5. 永久删除默认模块必须单独建立清理工单,核对代码依赖、数据库对象、权限记录、构建、框架升级和回退方式,不能混入普通业务页面工单。
|
||||
6. 新页面先盘点并复用已有布局、表格、表单、弹窗、上传、权限控制和请求封装。只有现有组件无法满足明确需求时才新增组件,并在工单记录复用缺口。
|
||||
7. 新组件沿用所属产品现有的颜色、字号、间距、状态反馈、交互方式、命名和目录结构,不建立独立于整体界面的视觉体系。
|
||||
8. 单页需求不得直接改变底层公共组件的全局行为。确需全局修改时单独建单,列出受影响页面、兼容方式和回归测试;同时影响 Sense、Bell 时按协同工单处理。
|
||||
|
||||
验收至少检查:没有多余入口、没有可见但不可用的失效入口、没有重复实现已有组件,且新增页面与同类页面的视觉和交互一致。
|
||||
|
||||
这里的三个处理级别必须明确区分:
|
||||
|
||||
- 隐藏:保留代码与能力,只移除普通用户入口,便于快速交付和以后恢复。
|
||||
- 禁用:前端入口与后端访问能力均不可用,适用于权限、安全或明确停用的能力。
|
||||
- 删除:清理代码、依赖、数据对象和权限记录,风险较高,必须由独立清理工单实施。
|
||||
|
||||
|
||||
|
||||
## 分支模型与审核门禁
|
||||
|
||||
```text
|
||||
explore(旧实现只读快照)
|
||||
|
||||
dev
|
||||
├── feature/<issue>-sense-...
|
||||
├── feature/<issue>-brain-...
|
||||
├── feature/<issue>-bell-...
|
||||
└── coordination/<issue>-...
|
||||
│
|
||||
└── PR + 测试 → dev → 用户明确审核 → main
|
||||
```
|
||||
|
||||
- `explore` 仅供对照、迁移和回退,不继续开发。
|
||||
- `main` 只接收用户明确审核通过的 `dev` 结果;禁止直接在 `main` 修改、提交或推送。
|
||||
- 新任务分支必须从当前 `dev` 创建,实施完成后 PR 目标为 `dev`。
|
||||
- Agent 的“待验收”不等于允许进入 `main`;只有用户明确审核/验收通过才可执行 `dev → main`。
|
||||
- 紧急修复也必须建单并取得用户对分支与合并路径的明确授权,不默认绕过 `dev`。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Architecture-and-Code-Map
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Architecture-and-Code-Map.-
|
||||
wiki_revision: 04b5eaea579729e427f1b2b8962399e861d5eb53
|
||||
synchronized_at: 2026-08-11T10:30:34Z
|
||||
wiki_revision: 119fb7b2a994d54e4d0a40f7bc2ef6238b162387
|
||||
synchronized_at: 2026-08-14T01:06:22Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 架构与代码地图
|
||||
@@ -31,15 +31,15 @@ Bell 也可接收合成事件、传感器平台或第三方系统事件;Sense
|
||||
|
||||
| 能力 | 路径 | 首个阅读入口 | 验证位置 | 风险 |
|
||||
|---|---|---|---|---|
|
||||
| Sense 产品 | `Sense/` | `Sense/AGENTS.md`;后续 README/入口 | `Sense/` 内测试 | 高:设备、媒体、凭据、并发对账 |
|
||||
| Sense 产品 | `Sense/` | `Sense/AGENTS.md`;待 GoAdmin 派生工单建立 README/入口 | 待新骨架建立 | 高:GoAdmin 派生、设备、媒体、凭据 |
|
||||
| Brain 推理 | `Brain/` | `Brain/AGENTS.md`;后续包入口 | `Brain/` 内测试与契约测试 | 高:模型、GPU、隐私、事件语义 |
|
||||
| Bell 产品 | `Bell/` | `Bell/AGENTS.md`;后续 README/入口 | `Bell/` 内测试 | 高:认证、事件、告警状态机、通知 |
|
||||
| Bell 产品 | `Bell/` | `Bell/AGENTS.md`;待 GoAdmin 派生工单建立 README/入口 | 待新骨架建立 | 高:GoAdmin 派生、认证、事件与告警状态机 |
|
||||
| 共享契约 | `contracts/` | `contracts/AGENTS.md` | 三端消费者/生产者测试 | 高:兼容性与跨项目影响 |
|
||||
| Harness | `dev_scripts/` | `check_harness.py` | `tests/` | 中 |
|
||||
| 工单模板 | `.gitea/issue_template/` | `task.md` | Harness 严格检查 | 中 |
|
||||
| Wiki 镜像 | `docs/` | `docs/README.md` | `sync_wiki_docs.py --check` | 低;禁止直接编辑 |
|
||||
|
||||
业务代码尚未迁入;具体函数、路由和测试入口必须随各项目骨架工单更新,不复制旧仓库代码地图。
|
||||
旧 Sense、Bell 入口仅存在于 `explore` 快照,不是 `main` / `dev` 当前代码地图。三个项目的新入口必须随新骨架工单建立;不得把旧自研基础框架复制回 `dev`。
|
||||
|
||||
## 两条主要执行路径
|
||||
|
||||
@@ -79,3 +79,22 @@ Sense/Brain 生成事件
|
||||
- 业务预警与运维告警分开。
|
||||
- Area/设备/规则在各独立产品内有自己的业务投影,不以共享数据库同步。
|
||||
- 单项目工单不得顺带修改其他项目;共享契约变更必须由协调工单统筹。
|
||||
|
||||
<!-- sense-runtime:start -->
|
||||
## Sense 新代码入口
|
||||
|
||||
Sense 当前处于重建前的空基线。新入口必须由 GoAdmin 派生工单从冻结 go-admin/go-admin-ui 源码建立,并在来源清单记录上游 commit;不得沿用 `explore` 中自研的 HTTP、认证、RBAC 或前端外壳作为基础框架。
|
||||
<!-- sense-runtime:end -->
|
||||
|
||||
<!-- sense-mvp:start -->
|
||||
## Sense 旧 MVP 对照
|
||||
|
||||
旧设备、ONVIF/RTSP、MediaMTX、实时监看和区域配置实现保存在 `explore`。迁移时只提取已确认业务需求和必要业务适配器;通用认证、权限、菜单、请求封装和管理端外壳必须优先使用 GoAdmin/go-admin-ui。
|
||||
<!-- sense-mvp:end -->
|
||||
|
||||
|
||||
<!-- bell-mvp:start -->
|
||||
## Bell 旧 MVP 对照
|
||||
|
||||
旧 Event → Rule → Alert → ack/close 实现在 `explore`。迁移时保留业务语义、不可变与幂等约束,但新的通用认证、RBAC、菜单、审计和管理端外壳必须基于冻结 GoAdmin 源码。
|
||||
<!-- bell-mvp:end -->
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: 7a9645a7ca7f4b3478d27afb2d441238431f501e
|
||||
synchronized_at: 2026-08-11T10:30:38Z
|
||||
wiki_revision: 85ddfe99c2f9c97b18d5e39849a50b8c5fca276e
|
||||
synchronized_at: 2026-08-14T01:17:28Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
@@ -66,3 +66,22 @@ synchronized_at: 2026-08-11T10:30:38Z
|
||||
- 三个交付单元的正式版本与发布流程。
|
||||
|
||||
未确认项不得从旧代码或模板默认值推断为当前事实。
|
||||
|
||||
<!-- sense-mvp:start -->
|
||||
## Sense 旧 MVP 规则状态
|
||||
|
||||
2026-08-14 前的 Sense 账户、设备、发现、Profile、MediaMTX、播放会话和区域版本实现规则仅适用于 `explore` 快照,不是新 `main` / `dev` 的运行事实。迁移工单应从这些记录提取已确认业务与安全约束,但认证、RBAC、管理端外壳和通用 CRUD 必须基于冻结 GoAdmin 源码重新实现并重新验收。
|
||||
<!-- sense-mvp:end -->
|
||||
|
||||
|
||||
<!-- bell-mvp:start -->
|
||||
## Bell 旧 MVP 规则状态
|
||||
|
||||
2026-08-14 前的 Bell 独立身份、Event/Receipt、规则、Alert、ack/close 和审计实现仅保存在 `explore`。不可变、幂等、并发处置和产品独立边界仍是待迁移业务要求,但新实现必须从冻结 GoAdmin/go-admin-ui 源码派生并重新验收。
|
||||
<!-- bell-mvp:end -->
|
||||
|
||||
|
||||
|
||||
## 旧实现迁移边界
|
||||
|
||||
旧 Sense 中已验证的中文设备字段、请求白名单、凭据隔离和媒体链路行为可作为迁移验收用例;不得把旧自研基础框架直接复制到新 `dev`。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Local-Development-and-Verification
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Local-Development-and-Verification.-
|
||||
wiki_revision: be0864a5bdbe06bd8358f2d013aa7313ab13e73b
|
||||
synchronized_at: 2026-08-11T10:30:41Z
|
||||
wiki_revision: 1730ec258fb54c1b4be15ce0f6e1a18e44281810
|
||||
synchronized_at: 2026-08-14T01:06:22Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 本地开发与验证
|
||||
@@ -17,7 +17,7 @@ synchronized_at: 2026-08-11T10:30:41Z
|
||||
| Gitea | 工单与 Wiki | 浏览仓库或调用 MCP |
|
||||
| Gitea PAT | Wiki 写入 | 仅由 MCP 或 `GITEA_TOKEN` 提供,不打印 |
|
||||
|
||||
Go、Node、CUDA、PostgreSQL、MediaMTX 的精确要求将在对应项目骨架工单冻结;当前不把旧仓库环境当作新项目事实。
|
||||
Sense/Bell 的 Go、Node 与 pnpm 基线已冻结并记录于下文;Brain 的 Python/CUDA 以及 PostgreSQL、MediaMTX 精确版本仍将在对应骨架工单冻结。旧仓库环境不自动成为新项目事实。
|
||||
|
||||
## 第一次运行
|
||||
|
||||
@@ -59,7 +59,7 @@ Go、Node、CUDA、PostgreSQL、MediaMTX 的精确要求将在对应项目骨架
|
||||
|---|---|---|---|---|
|
||||
| Sense | 待骨架工单冻结 | 待冻结 | 待冻结 | 未初始化业务代码 |
|
||||
| Brain | 待骨架工单冻结 | 待冻结 | 待冻结 | 未初始化业务代码 |
|
||||
| Bell | 待骨架工单冻结 | 待冻结 | 待冻结 | 未初始化业务代码 |
|
||||
| Bell | 待 GoAdmin 派生工单建立 | 待建立 | 待建立 | 旧实现仅在 `explore`;新基线未初始化 |
|
||||
|
||||
不得复制旧仓库命令来填空。每个骨架工单必须同时建立 README、可复制命令和最小测试。
|
||||
|
||||
@@ -73,8 +73,8 @@ Go、Node、CUDA、PostgreSQL、MediaMTX 的精确要求将在对应项目骨架
|
||||
|
||||
## 测试数据与日志
|
||||
|
||||
- 只使用合成或脱敏事件、合成 RTSP 和明确授权的实验室设备。
|
||||
- 不提交真实视频、客户名称、地址、手机号、摄像头密码或通知凭据。
|
||||
- 只使用合成或脱敏事件、合成 RTSP 和明确授权的实验室设备。ONVIF 自动化测试需覆盖 Basic、Digest challenge、Media 服务发现、跨主机地址归一化、拒绝地址凭据和禁止重定向。
|
||||
- 不提交真实视频、客户名称、地址、手机号、摄像头密码或通知凭据;真实设备验证只记录状态与 Profile 数量,不记录设备地址、Authorization 或 Stream URI。
|
||||
- 日志必须可按 request/event/alert ID 追踪,但不得记录 Authorization、Cookie 或连接密钥。
|
||||
|
||||
## 完成修改前
|
||||
@@ -88,3 +88,55 @@ git status --short
|
||||
```
|
||||
|
||||
业务工单还必须运行其声明的子项目测试;跨契约工单必须运行所有受影响项目的契约测试。未执行部分写入工单。
|
||||
|
||||
## Sense/Bell 固定工具链与只读参考源
|
||||
|
||||
根 `goadmin-baseline.json` 是机器可读基线。开始 Sense/Bell 骨架或升级工作前,先核对:
|
||||
|
||||
```powershell
|
||||
Get-Content .\goadmin-baseline.json
|
||||
|
||||
git -C D:\github\goadmin\go-admin rev-parse HEAD
|
||||
git -C D:\github\goadmin\go-admin-ui rev-parse HEAD
|
||||
git -C D:\github\goadmin\go-admin-doc rev-parse HEAD
|
||||
|
||||
go version
|
||||
node --version
|
||||
pnpm --version
|
||||
```
|
||||
|
||||
`D:\github\goadmin` 下三个仓库只允许读取和核对,不允许在其中开发或提交 yovision 产品代码。本机路径不是跨机器事实;缺少该路径时,应从清单中的上游 URL 检出相同 commit 到仓库外目录。
|
||||
|
||||
截至 2026-08-12,系统 Go 1.23.0 仍不满足基线;开发机已在仓库外安装并校验隔离 Go 1.26.5,并通过 Corepack 使用 pnpm 9.15.1。Sense、Bell 的后端测试/构建与前端 lint/build 已在该隔离工具链完成。隔离目录是开发机临时事实,不得写入部署配置;其他机器应从官方来源安装相同精确版本。
|
||||
|
||||
`go-admin-doc` 只用于理解上游框架;项目自己的架构、开发规则和版本事实仍以 yovision Wiki、Agent 规则与 `goadmin-baseline.json` 为准。
|
||||
|
||||
<!-- sense-runtime:start -->
|
||||
## Sense 本地构建与验证
|
||||
|
||||
新 Sense 尚未初始化。骨架工单必须先验证冻结 go-admin/go-admin-ui commit 与许可证,再记录从上游继承的构建、迁移、启动和测试命令;不得把 `explore` 的自研命令写成新基线事实。
|
||||
<!-- sense-runtime:end -->
|
||||
|
||||
|
||||
<!-- bell-runtime:start -->
|
||||
## Bell 本地构建与验证
|
||||
|
||||
新 Bell 尚未初始化。骨架工单必须从冻结 GoAdmin 源码建立并验证独立构建、数据库、认证和前端流程;旧 Bell 命令只在 `explore` 对应提交中适用。
|
||||
<!-- bell-runtime:end -->
|
||||
|
||||
<!-- sense-windows-package:start -->
|
||||
## Sense Windows 打包状态
|
||||
|
||||
旧 Sense Windows 包脚本只属于 `explore` 快照。新的打包命令必须在 GoAdmin 派生骨架和业务迁移完成后由独立工单重新建立、验证和记录。
|
||||
<!-- sense-windows-package:end -->
|
||||
|
||||
|
||||
## GoAdmin 派生前置检查
|
||||
|
||||
每个 Sense/Bell 骨架或基础能力工单在修改前必须:
|
||||
|
||||
1. 核对 `goadmin-baseline.json` 中三个完整 commit。
|
||||
2. 核对本机只读副本或重新检出的上游仓库 HEAD。
|
||||
3. 阅读任务相关的 go-admin-doc 主题/文件,并把参考项记录到工单。
|
||||
4. 记录计划继承的 go-admin/go-admin-ui 路径、计划隐藏/禁用的模块和许可证处理。
|
||||
5. 验证最终产品树确实包含上游派生结构;只使用 Go、Vue、Element Plus 或相似视觉不算通过。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Troubleshooting
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Troubleshooting
|
||||
wiki_revision: 632e07b6cd22f6e3ae07c23ed58439ea2beecff2
|
||||
synchronized_at: 2026-08-11T10:30:46Z
|
||||
wiki_revision: 2be5d803b954df64e31085bcf097ee27f8611f9d
|
||||
synchronized_at: 2026-08-14T01:17:28Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 故障排查
|
||||
@@ -39,3 +39,24 @@ synchronized_at: 2026-08-11T10:30:46Z
|
||||
- 契约破坏性变化没有新版本、迁移和回退方案;
|
||||
- 涉及删除、不可逆迁移、发布或生产变更但没有明确授权;
|
||||
- 工作区出现来源不明且与当前任务重叠的修改。
|
||||
|
||||
<!-- sense-mvp:start -->
|
||||
## 旧 Sense MVP 排错资料
|
||||
|
||||
旧 Sense 的数据库、ONVIF/RTSP、MediaMTX、实时监看、会话和区域排错只适用于 `explore` 快照。新 `main` / `dev` 当前没有可运行 Sense;在新的 GoAdmin 派生骨架和业务迁移工单完成前,不使用旧命令或旧状态判断新实现。
|
||||
<!-- sense-mvp:end -->
|
||||
|
||||
<!-- sense-windows-package:start -->
|
||||
## 旧 Sense Windows 包状态
|
||||
|
||||
旧 Windows 打包与启动脚本仅保存在 `explore`,不属于新开发基线。新的 GoAdmin 派生实现必须由独立工单重新建立打包、配置和排错说明。
|
||||
<!-- sense-windows-package:end -->
|
||||
|
||||
|
||||
|
||||
## 重建阶段常见问题
|
||||
|
||||
| 现象 | 处理 |
|
||||
|---|---|
|
||||
| 在 `dev` 找不到 Sense/Bell 可运行代码 | 这是重建空基线的预期状态;旧实现位于 `explore`,新代码必须由 GoAdmin 源码派生工单建立。 |
|
||||
| 新骨架只有相似页面、没有 GoAdmin 启动链或权限模块 | 不符合二次开发门禁;停止验收,对照 `goadmin-baseline.json`、上游源码和 go-admin-doc 重新实施。 |
|
||||
|
||||
+26
-2
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Delivery-Documentation-Guide
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Delivery-Documentation-Guide.-
|
||||
wiki_revision: e0b43bfada515ff95831debc4378de41a8ad969a
|
||||
synchronized_at: 2026-08-11T10:31:06Z
|
||||
wiki_revision: d4f2bfcef46493361ba019839b36957ed9947003
|
||||
synchronized_at: 2026-08-14T01:18:58Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 交付文档指南
|
||||
@@ -78,3 +78,27 @@ synchronized_at: 2026-08-11T10:31:06Z
|
||||
## 不在本页解决的内容
|
||||
|
||||
开发任务怎样建单、实施和归档见[开发工作流](Development-Workflow.-);代码结构和维护入口见[架构与代码地图](Architecture-and-Code-Map.-)。本页不规定市场宣传、合同、法务或商务承诺。
|
||||
|
||||
<!-- sense-mvp:start -->
|
||||
## Sense MVP 岗位操作路径
|
||||
|
||||
Sense 面向网管、实施人员和非技术现场人员,菜单按日常任务组织:工作台 → 设备管理 → 视频接入 → 视频服务 → 实时监看 → 区域与警戒线。普通操作优先展示中文状态与下一步,不要求用户理解 ONVIF、RTSP 或 MediaMTX 内部模型。
|
||||
|
||||
- 系统管理员:完成一次性安全初始化,维护 Sense 用户与角色,查看操作记录。首次管理员和新用户密码仅要求至少 6 个字符,不限制字符种类并允许包含用户名;仍应由管理员选择难猜且不复用的密码。
|
||||
- 实施/运维:添加设备、只写更新凭据、在获准网卡发现或手工接入、检查主辅码流、处理媒体进程与对账。
|
||||
- 站点管理员:查看/维护站点设备,实时监看,绘制和发布区域版本;无权创建系统管理员。
|
||||
- 只读用户:查看设备、媒体状态、实时画面和区域,不能修改。
|
||||
|
||||
交付验收必须使用仓库外测试账户和获准实验室设备;文档、截图、工单和日志不得包含摄像机密码、Cookie、令牌、含凭据 URI 或客户真实画面。当前自动化已覆盖内部状态和安全边界;真实 PostgreSQL、MediaMTX、摄像机兼容与目标浏览器画面仍需实施人员验证。
|
||||
<!-- sense-mvp:end -->
|
||||
|
||||
<!-- sense-windows-package:start -->
|
||||
## Sense Windows 运行包交付
|
||||
|
||||
交付对象为实施和运维人员。`sense-windows-amd64.zip` 包含后端程序、已构建前端、空值示例配置、上游许可证、启动脚本与包内说明;不包含 PostgreSQL、MediaMTX、Windows 服务、生产数据或秘密。
|
||||
|
||||
- 临时查看必须显式运行 `start-sense.bat demo`,其内存数据在进程结束后丢失,不能当作生产部署。
|
||||
- 生产配置可由运维写入解压目录的 `config\sense.env`,或通过 Windows 进程环境安全注入;进程环境优先。先运行 `start-sense.bat check` 检查必填项,再运行 `start-sense.bat`。
|
||||
- 交付时记录 ZIP SHA-256,并至少验证 `/healthz` 与首页;真实 PostgreSQL、MediaMTX、摄像机和目标浏览器仍需在获准环境验收。
|
||||
- 包内 `README-WINDOWS.md` 是现场操作入口;真实 `config\sense.env` 只留在具体部署目录,不得提交 Git 或重新打入交付 ZIP,交付 ZIP 只保留 `sense.env.example`。
|
||||
<!-- sense-windows-package:end -->
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-28-三项目当前MVP交互原型
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-28-%E4%B8%89%E9%A1%B9%E7%9B%AE%E5%BD%93%E5%89%8DMVP%E4%BA%A4%E4%BA%92%E5%8E%9F%E5%9E%8B.-
|
||||
wiki_revision: 47ef943ea870c7ff814dff5b1dd641052954d828
|
||||
synchronized_at: 2026-08-14T02:05:27Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 28 三项目当前 MVP 交互原型
|
||||
|
||||
- 类型:协同原型任务
|
||||
- 状态:已完成
|
||||
- 日期:2026-08-14
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/28
|
||||
|
||||
## 背景与目标
|
||||
|
||||
为 Sense、Brain、Bell 首个 MVP 建立可浏览的交互原型,用于确认信息架构、页面入口和关键流程,不替代真实产品实现。
|
||||
|
||||
## 最终方案
|
||||
|
||||
生成三项目原型入口及共享设计资产;后续 #30/#31 对齐 go-admin-ui 与 Element Plus,#32/#33 分别从 Sense 网管/非技术人员和 Bell 普通用户视角收敛业务语言与流程。
|
||||
|
||||
## 修改文件
|
||||
|
||||
`prototypes/index.html`、三项目原型页面和共享样式、脚本与设计资源(历史快照位于 `explore`)。
|
||||
|
||||
## 验收结果
|
||||
|
||||
原型入口、4 个 HTML 页面和共享资源形成完整导航与关键流程。
|
||||
|
||||
## 测试
|
||||
|
||||
Harness、仓库测试、静态资源、JavaScript、键盘交互、减少动画、桌面和 375px 移动布局检查通过。
|
||||
|
||||
## 相关提交
|
||||
|
||||
实现提交 `533225d`,原型 PR #29。
|
||||
|
||||
## 未验证部分
|
||||
|
||||
**未验证部分**:未验证真实视频、API、数据库、模型效果或跨项目契约;这些属于产品实现工单。
|
||||
|
||||
## 用户验收
|
||||
|
||||
用户于 2026-08-14 明确验收通过。原型保留于 `explore` 作为需求与交互参考,不恢复为 `dev` 中已经实现的产品代码。
|
||||
@@ -0,0 +1,69 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-3-GoAdmin默认模块精简与UI组件复用
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-3-GoAdmin%E9%BB%98%E8%AE%A4%E6%A8%A1%E5%9D%97%E7%B2%BE%E7%AE%80%E4%B8%8EUI%E7%BB%84%E4%BB%B6%E5%A4%8D%E7%94%A8.-
|
||||
wiki_revision: 184eb46c074967590361b45e0eeb001eafbc84f2
|
||||
synchronized_at: 2026-08-12T08:38:41Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 3 GoAdmin 默认模块精简与 UI 组件复用
|
||||
|
||||
- 类型:需求
|
||||
- 所属 Epic:无
|
||||
- 所属 MVP / 版本:流程初始化
|
||||
- 状态:已完成
|
||||
- 日期:2026-08-12
|
||||
- 验收日期:2026-08-12
|
||||
- 验收结果:用户明确验收通过
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/3
|
||||
- Pull Request:https://git.ilapage.cn/ila/yovision/pulls/4
|
||||
- Wiki 页面:Task-3-GoAdmin默认模块精简与UI组件复用
|
||||
- Wiki revision:见本地镜像头
|
||||
|
||||
## 背景与目标
|
||||
|
||||
为 Sense、Bell 使用 go-admin/go-admin-ui 建立统一的最小化和组件复用规则,避免默认菜单、路由、权限与接口造成产品噪音,同时避免重复组件和视觉交互割裂。
|
||||
|
||||
## 最终方案
|
||||
|
||||
1. Sense、Bell 均遵循“最小可见、最小启用”;快速交付优先使用配置、菜单权限和路由开关隐藏无关能力。
|
||||
2. 涉及权限或安全时同时关闭前端入口和后端访问;永久删除默认模块必须单独建单并核对依赖、数据库、权限和升级影响。
|
||||
3. 新页面优先复用现有布局、表格、表单、弹窗、上传、权限控制和请求封装。
|
||||
4. 新组件只在现有组件无法满足明确业务需求时建立,并保持颜色、字号、间距、反馈、命名和目录结构一致。
|
||||
5. 局部页面不得擅自改变公共组件全局行为;全局或跨产品影响升级为独立/协同工单。
|
||||
6. UI 验收检查无多余入口、无失效入口、无重复实现且视觉交互一致。
|
||||
|
||||
与建单方案无差异。
|
||||
|
||||
## 修改文件
|
||||
|
||||
- `AGENTS.md`:增加 GoAdmin 精简、复用和变更边界。
|
||||
- `Sense/AGENTS.md`、`Bell/AGENTS.md`:增加产品侧可执行约束。
|
||||
- `dev_scripts/check_harness.py`:增加规则门禁。
|
||||
- `tests/test_harness_docs.py`:增加回归测试。
|
||||
- `docs/01-workflow.md`:同步 Wiki 长期流程文档。
|
||||
|
||||
## 验收结果
|
||||
|
||||
| 验收标准 | 结果 |
|
||||
|---|---|
|
||||
| 根规则明确隐藏、禁用、删除边界与组件复用原则 | 通过 |
|
||||
| Sense、Bell 子项目规则包含可执行约束 | 通过 |
|
||||
| 全局公共组件改动和永久删除需单独建单 | 通过 |
|
||||
| Harness 能检测关键规则丢失 | 通过 |
|
||||
| Wiki 与镜像一致 | 通过 |
|
||||
| 用户明确验收 | 通过(2026-08-12) |
|
||||
|
||||
## 测试
|
||||
|
||||
- `python dev_scripts/check_harness.py --strict`:通过。
|
||||
- `python -m unittest discover -s tests -v`:28 项全部通过。
|
||||
- `python -m compileall -q dev_scripts tests`:通过。
|
||||
- `python dev_scripts/sync_wiki_docs.py --check`:通过。
|
||||
- `git diff --check`:通过。
|
||||
- **未验证部分**:规则实施时 Sense、Bell 尚无实际业务页面,未执行浏览器视觉验收或真实默认模块隐藏;后续页面工单按本规则验收。
|
||||
|
||||
## 相关提交
|
||||
|
||||
- `6ff12e6` docs: define go-admin UI reuse rules (#3)
|
||||
- `2285fb0` Merge pull request #4: GoAdmin UI 精简与复用规则
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-30-Sense原型对齐GoAdmin与Element-Plus
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-30-Sense%E5%8E%9F%E5%9E%8B%E5%AF%B9%E9%BD%90GoAdmin%E4%B8%8EElement-Plus.-
|
||||
wiki_revision: aa30868a002d3f3840d663f849d3ce4cc1963ae0
|
||||
synchronized_at: 2026-08-14T02:05:31Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 30 Sense 原型对齐 GoAdmin 与 Element Plus
|
||||
|
||||
- 类型:Sense 原型任务
|
||||
- 状态:已完成
|
||||
- 日期:2026-08-14
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/30
|
||||
|
||||
## 背景与目标
|
||||
|
||||
将 Sense 原型的通用外壳和组件对齐冻结 go-admin-ui / Element Plus,使后续源码二次开发可直接映射布局、导航、标签页、搜索、权限按钮、表格、表单、分页和弹窗。
|
||||
|
||||
## 最终方案
|
||||
|
||||
保留 Sense 业务信息架构;通用 UI 复用 go-admin-ui 风格,只为视频监看和区域绘制保留业务专用组件。
|
||||
|
||||
## 修改文件
|
||||
|
||||
`prototypes/sense.html` 及原型共享外壳、样式和交互资源(历史快照位于 `explore`)。
|
||||
|
||||
## 验收结果
|
||||
|
||||
Sense 原型应用外壳、通用组件和响应式行为与冻结 go-admin-ui 基准对齐。
|
||||
|
||||
## 测试
|
||||
|
||||
Sense、Brain、Bell 原型回归、桌面与移动布局、交互和静态检查通过。
|
||||
|
||||
## 相关提交
|
||||
|
||||
实现提交 `1da77fe`,原型 PR #29。
|
||||
|
||||
## 未验证部分
|
||||
|
||||
**未验证部分**:未验证真实 GoAdmin 产品代码集成;新 Sense 仍须在 #61 中从冻结源码派生。
|
||||
|
||||
## 用户验收
|
||||
|
||||
用户于 2026-08-14 明确验收通过。原型保留于 `explore` 作为需求与交互参考,不恢复为 `dev` 中已经实现的产品代码。
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-32-Sense网管与非技术人员原型
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-32-Sense%E7%BD%91%E7%AE%A1%E4%B8%8E%E9%9D%9E%E6%8A%80%E6%9C%AF%E4%BA%BA%E5%91%98%E5%8E%9F%E5%9E%8B.-
|
||||
wiki_revision: 4c91b1e89414ae4d2790df6254e8b927b5a062e1
|
||||
synchronized_at: 2026-08-14T02:05:35Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 32 Sense 网管与非技术人员原型
|
||||
|
||||
- 类型:Sense 原型任务
|
||||
- 状态:已完成
|
||||
- 日期:2026-08-14
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/32
|
||||
|
||||
## 背景与目标
|
||||
|
||||
从网管和非技术设备管理员视角调整 Sense 模块名称、设备接入流程、故障反馈和日常操作,避免暴露不必要的底层技术概念。
|
||||
|
||||
## 最终方案
|
||||
|
||||
一级导航收敛为设备概况、摄像头管理、实时画面和警戒区域;接入采用发现/手工添加、账号、测试、预览、完成的引导流程;故障提示说明原因、影响和恢复动作。
|
||||
|
||||
## 修改文件
|
||||
|
||||
`prototypes/sense.html` 及相关原型交互、样式资源(历史快照位于 `explore`)。
|
||||
|
||||
## 验收结果
|
||||
|
||||
模块名称、接入流程与错误恢复均从目标用户视角完成收敛。
|
||||
|
||||
## 测试
|
||||
|
||||
关键流程、表单反馈、响应式布局和三项目原型回归通过。
|
||||
|
||||
## 相关提交
|
||||
|
||||
实现提交 `25c2534`,原型 PR #29。
|
||||
|
||||
## 未验证部分
|
||||
|
||||
**未验证部分**:未验证真实摄像机和产品 API;新 Sense 须在 #61 的 GoAdmin 源码基线上重新落地。
|
||||
|
||||
## 用户验收
|
||||
|
||||
用户于 2026-08-14 明确验收通过。原型保留于 `explore` 作为需求与交互参考,不恢复为 `dev` 中已经实现的产品代码。
|
||||
@@ -0,0 +1,69 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-5-冻结Sense与Bell-GoAdmin技术基线
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-5-%E5%86%BB%E7%BB%93Sense%E4%B8%8EBell-GoAdmin%E6%8A%80%E6%9C%AF%E5%9F%BA%E7%BA%BF.-
|
||||
wiki_revision: a5fc02e96d04200e5c51f9313634b50c4e5218c6
|
||||
synchronized_at: 2026-08-12T08:38:44Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 5 冻结 Sense 与 Bell GoAdmin 技术基线
|
||||
|
||||
- 类型:需求
|
||||
- 所属 Epic:无
|
||||
- 所属 MVP / 版本:技术基线
|
||||
- 状态:已完成
|
||||
- 日期:2026-08-12
|
||||
- 验收日期:2026-08-12
|
||||
- 验收结果:用户明确验收通过
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/5
|
||||
- Pull Request:https://git.ilapage.cn/ila/yovision/pulls/6
|
||||
- Wiki 页面:Task-5-冻结Sense与Bell-GoAdmin技术基线
|
||||
- Wiki revision:见本地镜像头
|
||||
|
||||
## 背景与目标
|
||||
|
||||
把 Sense、Bell 共用的 GoAdmin 上游、工具链和本机只读参考源从口头约定固化为可复现、可检查的正式基线,避免两个产品从不同提交或运行时起步。
|
||||
|
||||
## 最终方案
|
||||
|
||||
1. 根级 `goadmin-baseline.json` 是机器可读事实来源,远端 URL 与完整 commit 是可复现标识。
|
||||
2. go-admin 固定为 `f06540883b41d03782bb6b2c4150f298f328c6b6`,要求 Go 1.26.5。
|
||||
3. go-admin-ui 固定为 `67d393d713877572fab0b897296a4c1d525fc81d`,要求 Node.js 22.22.1、pnpm 9.15.1。
|
||||
4. go-admin-doc 固定为 `424855aacf6905f3fde860c3331385cb25529a0d`,仅用于理解框架用法。
|
||||
5. `D:\github\goadmin` 下三个仓库只读参考;产品代码必须分别进入 Sense、Bell。
|
||||
6. 派生上游代码保留 MIT 许可证与来源;任一上游或工具链升级必须建立 Sense/Bell 协同工单。
|
||||
7. Sense、Bell 从同一基线独立构建、发布,不形成隐藏共享实现。
|
||||
|
||||
与建单方案无差异。#5 原为叠加 PR;用户验收后先合并 #3/PR #4,再把 PR #6 调整到 `main` 并合并。
|
||||
|
||||
## 修改文件
|
||||
|
||||
- `goadmin-baseline.json`:记录上游和工具链精确版本、来源与许可证。
|
||||
- `AGENTS.md`、`Sense/AGENTS.md`、`Bell/AGENTS.md`:固化只读参考、派生、升级与构建规则。
|
||||
- `dev_scripts/check_harness.py`、`tests/test_harness_docs.py`:增加机器可读基线门禁和回归测试。
|
||||
- `docs/00-project-profile.md`、`docs/04-local-development-and-verification.md`:同步 Wiki 长期文档。
|
||||
|
||||
## 验收结果
|
||||
|
||||
| 验收标准 | 结果 |
|
||||
|---|---|
|
||||
| 清单记录精确上游 commit、远端、分支、许可证和运行时版本 | 通过 |
|
||||
| 根及 Sense/Bell 规则明确只读参考、禁止原地开发和升级流程 | 通过 |
|
||||
| Wiki 说明固定版本、复现方式和环境要求 | 通过 |
|
||||
| commit 与 package version/tag 边界明确 | 通过 |
|
||||
| Harness 与测试覆盖关键字段 | 通过 |
|
||||
| 用户明确验收 | 通过(2026-08-12) |
|
||||
|
||||
## 测试
|
||||
|
||||
- `python dev_scripts/check_harness.py --strict`:通过。
|
||||
- `python -m unittest discover -s tests -v`:31 项全部通过。
|
||||
- `python -m compileall -q dev_scripts tests`:通过。
|
||||
- `python dev_scripts/sync_wiki_docs.py --check`:通过。
|
||||
- `git diff --check`:通过。
|
||||
- **未验证部分**:#5 本身未初始化产品代码,因此未执行产品构建。验收后已在仓库外隔离准备并校验 Go 1.26.5;Node.js 22.22.1 可用,pnpm 9.15.1 可通过 Corepack 精确调用,供 #21 使用。
|
||||
|
||||
## 相关提交
|
||||
|
||||
- `7dac434` docs: freeze GoAdmin technical baseline (#5)
|
||||
- `8486d90` Merge pull request #6: 冻结 GoAdmin 技术基线
|
||||
@@ -0,0 +1,91 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-58-建立explore-main-dev分支治理并重置GoAdmin开发基线
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-58-%E5%BB%BA%E7%AB%8Bexplore-main-dev%E5%88%86%E6%94%AF%E6%B2%BB%E7%90%86%E5%B9%B6%E9%87%8D%E7%BD%AEGoAdmin%E5%BC%80%E5%8F%91%E5%9F%BA%E7%BA%BF.-
|
||||
wiki_revision: f4579e552dec5ed7e3dbff934697be97e4285b86
|
||||
synchronized_at: 2026-08-14T01:55:39Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 58 建立 explore/main/dev 分支治理并重置 GoAdmin 开发基线
|
||||
|
||||
- 类型:根级协调任务
|
||||
- 状态:已完成
|
||||
- 日期:2026-08-14
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/58
|
||||
|
||||
## 背景与目标
|
||||
|
||||
用户确认保留当前实现作为历史快照,并将后续开发切换到 `dev`,只有用户审核后才能进入 `main`。同时纠正此前“仅参考 GoAdmin、自研基础框架”的偏差:新的 Sense、Bell 必须真正从冻结 go-admin 与 go-admin-ui 源码派生,开发时必须参考冻结 go-admin-doc。
|
||||
|
||||
## 最终方案
|
||||
|
||||
1. 创建 `explore` 聚合快照,合入 Sense 最新链、Bell MVP、Bell 任务归档和已确认原型;原来源分支全部保留。
|
||||
2. 共享 Wiki 镜像冲突时,快照树保留 Sense 最新版本;Bell 独立任务归档保留在 `docs/task/`,Bell 共享文档仍可从来源提交完整读取。
|
||||
3. 审计发现执行前 `main` 已经是最小治理基线,没有 Sense/Bell 产品实现,因此不制造删除或清空提交;只补充分支门禁、GoAdmin 源码派生门禁和更新后的 Wiki 镜像。
|
||||
4. `dev` 从最终 `main` 创建,后续任务分支从 `dev` 派生,通过 PR 合回 `dev`;只有用户明确审核通过才能执行 `dev → main`。
|
||||
5. “基于 GoAdmin”定义为从 `goadmin-baseline.json` 指定的完整 go-admin/go-admin-ui commit 导入或派生源码,保留许可证、启动链和通用框架能力。仅使用 Go、Vue、Element Plus、类似目录或视觉不算满足。
|
||||
6. 每个 Sense/Bell 框架相关工单实施前必须读取相关 go-admin-doc,并记录文档主题/文件、继承路径、裁剪/隐藏模块和来源证据。
|
||||
7. 当前旧产品实现不合入新 `main` / `dev`;迁移只从 `explore` 提取已确认业务需求和必要业务适配器。
|
||||
|
||||
## 分支与回退证据
|
||||
|
||||
- 重建前 main:`be2c1e3c81131b51ee9fba67394ae9928e41104c`
|
||||
- explore 聚合提交:`9020b32`
|
||||
- main 规则提交:`0ce30e5`
|
||||
- main/dev 最终共同基线:`1e724e973189f0fa9bfba8109791658bacd08640`
|
||||
- Sense 来源:`a9f8a55`
|
||||
- Bell 来源:`9ee06e3`
|
||||
- Bell 文档来源:`3c245e1`
|
||||
- 原型来源:`8d1875c`
|
||||
|
||||
所有来源分支均保留。需要回退分支治理时可把 main 恢复到重建前 SHA;需要读取旧产品时使用 `explore` 或对应来源分支。
|
||||
|
||||
## 测试
|
||||
|
||||
- `python dev_scripts/check_harness.py --strict`:通过。
|
||||
- `python -m unittest discover -s tests -v`:31 项通过。
|
||||
- `git diff --check`:通过。
|
||||
- Gitea Wiki 的 Project-Profile、Development-Workflow、Architecture-and-Code-Map、Local-Development-and-Verification 已先更新并读取确认。
|
||||
|
||||
## Gitea 管理配置
|
||||
|
||||
通过当前实例 Swagger 描述核对 API 后,使用仓库外安全凭据完成并读取确认:`dev` 已设为默认分支;`main` 与 `dev` 均禁止直接推送和强推;`main` 要求 1 个审批,`dev` 要求 0 个审批以允许 Agent 在测试后合并任务 PR。管理员覆盖保持可用,实际执行仍受“只有用户明确审核后才能进入 main”的仓库规则约束。
|
||||
|
||||
## 遗留边界
|
||||
|
||||
本任务不导入新的 GoAdmin 产品源码,也不迁移旧业务功能。Sense/Bell 的真正源码派生和逐模块迁移应另建工单并在 `dev` 上实施。
|
||||
|
||||
## 修改文件
|
||||
|
||||
- 根级与子项目规则:`AGENTS.md`、`Sense/AGENTS.md`、`Bell/AGENTS.md`
|
||||
- 分支说明:`README.md`
|
||||
- Wiki 镜像:项目档案、开发工作流、架构与代码地图、本地开发与验证
|
||||
- 任务归档镜像与映射:`docs/task/58-*.md`、`wiki-docs.json`
|
||||
- explore 快照说明:`EXPLORE-SNAPSHOT.md`
|
||||
|
||||
## 验收结果
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|---|---|
|
||||
| explore 聚合旧 Sense、Bell、原型和任务证据 | 通过 |
|
||||
| 原来源分支保持存在 | 通过 |
|
||||
| main 保持最小治理与重建基线 | 通过 |
|
||||
| dev 从最终 main 创建 | 通过;两者起点均为 `1e724e9` |
|
||||
| GoAdmin/go-admin-ui 源码派生与 go-admin-doc 必查规则 | 通过 |
|
||||
| 用户未跟踪文件未提交 | 通过 |
|
||||
| Gitea main/dev 保护与默认分支 | 通过;已读取服务器配置确认 |
|
||||
|
||||
## 相关提交
|
||||
|
||||
- `9020b32`:记录 explore 快照来源
|
||||
- `0ce30e5`:建立审核分支流和 GoAdmin 源码派生门禁
|
||||
- `1e724e9`:登记并镜像工单 #58 归档
|
||||
|
||||
## 未验证部分
|
||||
|
||||
- **未验证部分**:无。产品源码导入和业务迁移明确属于后续工单,不是本任务未验证项。
|
||||
|
||||
## 用户验收
|
||||
|
||||
- 2026-08-14:用户明确确认当前未验收工单通过验收。
|
||||
- 本工单完成后,`dev` 成为后续集成开发分支;#61、#62、#10 的分支门禁解除。
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"frozen_at": "2026-08-12",
|
||||
"authority": "upstream_url_and_commit",
|
||||
"consumers": [
|
||||
"Sense",
|
||||
"Bell"
|
||||
],
|
||||
"runtimes": {
|
||||
"go": "1.26.5",
|
||||
"node": "22.22.1",
|
||||
"package_manager": "pnpm",
|
||||
"pnpm": "9.15.1"
|
||||
},
|
||||
"sources": {
|
||||
"go-admin": {
|
||||
"role": "backend_baseline",
|
||||
"local_reference_path": "D:\\github\\goadmin\\go-admin",
|
||||
"upstream_url": "https://github.com/go-admin-team/go-admin.git",
|
||||
"branch": "master",
|
||||
"commit": "f06540883b41d03782bb6b2c4150f298f328c6b6",
|
||||
"describe": "v2.2.0-25-gf065408",
|
||||
"exact_tag": null,
|
||||
"license": "MIT",
|
||||
"license_file": "LICENSE.md"
|
||||
},
|
||||
"go-admin-ui": {
|
||||
"role": "frontend_baseline",
|
||||
"local_reference_path": "D:\\github\\goadmin\\go-admin-ui",
|
||||
"upstream_url": "https://github.com/go-admin-team/go-admin-ui.git",
|
||||
"branch": "master",
|
||||
"commit": "67d393d713877572fab0b897296a4c1d525fc81d",
|
||||
"describe": "v2.0.9-170-g67d393d",
|
||||
"exact_tag": null,
|
||||
"package_version": "2.0.9",
|
||||
"package_version_is_authoritative": false,
|
||||
"license": "MIT",
|
||||
"license_file": "LICENSE"
|
||||
},
|
||||
"go-admin-doc": {
|
||||
"role": "reference_only_documentation",
|
||||
"local_reference_path": "D:\\github\\goadmin\\go-admin-doc",
|
||||
"upstream_url": "https://github.com/go-admin-team/go-admin-doc.git",
|
||||
"branch": "master",
|
||||
"commit": "424855aacf6905f3fde860c3331385cb25529a0d",
|
||||
"describe": "424855a",
|
||||
"exact_tag": null,
|
||||
"package_version": "0.0.1",
|
||||
"package_version_is_authoritative": false,
|
||||
"license": "MIT",
|
||||
"license_file": "LICENSE"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"local_repositories_are_read_only": true,
|
||||
"product_code_must_not_be_developed_in_reference_repositories": true,
|
||||
"preserve_upstream_license_and_copyright": true,
|
||||
"upgrades_require_coordination_issue": true,
|
||||
"sense_and_bell_release_independently": true
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
# YoVision 当前 MVP 交互原型
|
||||
|
||||
本目录是工单 #28 生成的版本绑定设计资产,用于确认 MVP #8 的信息架构、页面状态和交互流程,不代表产品功能已经实现。
|
||||
|
||||
## 打开方式
|
||||
|
||||
可直接双击 `index.html`,或在仓库根目录启动本地静态服务:
|
||||
|
||||
```powershell
|
||||
python -m http.server 4173 --directory prototypes
|
||||
```
|
||||
|
||||
然后访问 `http://127.0.0.1:4173/`。
|
||||
|
||||
## 原型范围
|
||||
|
||||
- `sense/index.html`:设备接入、ONVIF/RTSP、MediaMTX、实时监看和区域配置;直接映射 go-admin-ui 与 Element Plus 的通用管理端组件。
|
||||
- `brain/index.html`:合成输入到项目内匿名事件的内部流水线;Brain 无客户管理 UI。
|
||||
- `bell/index.html`:合成事件、不可变 Event、规则/Alert、ack/close 和审计时间线;直接映射 go-admin-ui 与 Element Plus 的通用管理端组件。
|
||||
- `assets/`:三套原型复用的基础样式和交互脚本;Sense/Bell 共同复用 go-admin-ui 原型外壳适配器。
|
||||
- `design-system/yovision/MASTER.md`:由 UI/UX Pro Max 生成的设计系统基线。
|
||||
|
||||
## 交互提示
|
||||
|
||||
- 左侧导航可切换页面;移动端使用顶部菜单按钮。
|
||||
- Sense 可筛选设备、添加待激活设备并查看区域配置。
|
||||
- Brain 可模拟处理下一帧并生成项目内事件。
|
||||
- Bell 可注入合成事件,并演示 Alert 的 ack 和 close。
|
||||
- 所有弹窗支持 `Esc` 关闭,交互元素支持键盘焦点。
|
||||
|
||||
## Sense / Bell 的 go-admin-ui 对齐基线
|
||||
|
||||
- 只读参考仓库:`D:\github\goadmin\go-admin-ui`。
|
||||
- 参考 commit:`67d393d713877572fab0b897296a4c1d525fc81d`。
|
||||
- 应用外壳映射:`Sidebar`、`Navbar`、`TagsView`、`AppMain` 和 `BasicLayout` / 页面容器。
|
||||
- 通用组件映射:Element Plus 的 Button、Form、Input/Select、Table、Pagination、Dialog 和 Tag,以及 go-admin-ui 的搜索区与 `v-permisaction` 权限按钮模式。
|
||||
- 共用原型适配器:`assets/go-admin-ui.css` 与 `assets/go-admin-shell.js`;两套产品不复制应用外壳和通用组件原型代码。
|
||||
- Sense 专用组件:视频舞台 `SenseVideoStage` 与区域画布 `SenseZoneCanvas`。
|
||||
- Bell 专用组件:值班处置工作台 `AlertWorkbench` 与审计时间线 `AlertAuditTimeline`。
|
||||
- 新增专用组件前必须先确认 go-admin-ui / Element Plus 中没有等价组件。
|
||||
- 原型仍是无构建依赖的 HTML/CSS/JavaScript,并未嵌入或复制 go-admin-ui / Element Plus 源码;这些名称用于约束正式 Vue 实现的组件落点。
|
||||
|
||||
## 事实来源边界
|
||||
|
||||
- 产品范围以 Gitea Wiki 的 Product Requirements、Product Roadmap 和对应工单为准。
|
||||
- 本原型不定义 API、数据库或跨项目契约。
|
||||
- 旧仓库原型仅用于只读参考;本目录没有复制旧原型代码。
|
||||
- 页面数据均为虚构演示数据,不得放入真实账号、摄像头凭据、客户数据或生产地址。
|
||||
Vendored
-203
@@ -1,203 +0,0 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const qs = (selector, root = document) => root.querySelector(selector);
|
||||
const qsa = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const toastRegion = qs("[data-toast-region]");
|
||||
let lastFocused = null;
|
||||
|
||||
function toast(message) {
|
||||
if (!toastRegion) return;
|
||||
const item = document.createElement("div");
|
||||
item.className = "toast";
|
||||
item.setAttribute("role", "status");
|
||||
item.textContent = message;
|
||||
toastRegion.appendChild(item);
|
||||
window.setTimeout(() => item.remove(), 3600);
|
||||
}
|
||||
|
||||
function setView(name) {
|
||||
qsa("[data-view]").forEach((view) => {
|
||||
const active = view.dataset.view === name;
|
||||
view.classList.toggle("active", active);
|
||||
view.hidden = !active;
|
||||
});
|
||||
qsa("[data-view-target]").forEach((button) => {
|
||||
const active = button.dataset.viewTarget === name;
|
||||
button.classList.toggle("active", active);
|
||||
if (active) button.setAttribute("aria-current", "page");
|
||||
else button.removeAttribute("aria-current");
|
||||
});
|
||||
const selected = qs(`[data-view-target="${name}"]`);
|
||||
const title = selected?.dataset.title || selected?.textContent.trim();
|
||||
const pageName = qs("[data-page-name]");
|
||||
if (pageName && title) pageName.textContent = title;
|
||||
qs("#main-content")?.focus({ preventScroll: true });
|
||||
qs("[data-sidebar]")?.classList.remove("open");
|
||||
qs("[data-mobile-menu]")?.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
qsa("[data-view-target]").forEach((button) => {
|
||||
button.addEventListener("click", () => setView(button.dataset.viewTarget));
|
||||
});
|
||||
|
||||
qs("[data-mobile-menu]")?.addEventListener("click", () => {
|
||||
const sidebar = qs("[data-sidebar]");
|
||||
const open = sidebar.classList.toggle("open");
|
||||
qs("[data-mobile-menu]").setAttribute("aria-expanded", String(open));
|
||||
});
|
||||
|
||||
function openModal(id, trigger) {
|
||||
const backdrop = qs(`#${id}`);
|
||||
if (!backdrop) return;
|
||||
lastFocused = trigger || document.activeElement;
|
||||
backdrop.classList.add("open");
|
||||
backdrop.setAttribute("aria-hidden", "false");
|
||||
document.body.style.overflow = "hidden";
|
||||
qs("input, select, textarea, button", qs(".modal", backdrop))?.focus();
|
||||
}
|
||||
|
||||
function closeModal(backdrop) {
|
||||
if (!backdrop) return;
|
||||
backdrop.classList.remove("open");
|
||||
backdrop.setAttribute("aria-hidden", "true");
|
||||
document.body.style.overflow = "";
|
||||
lastFocused?.focus();
|
||||
}
|
||||
|
||||
qsa("[data-open-modal]").forEach((trigger) => {
|
||||
trigger.addEventListener("click", () => openModal(trigger.dataset.openModal, trigger));
|
||||
});
|
||||
qsa("[data-close-modal]").forEach((trigger) => {
|
||||
trigger.addEventListener("click", () => closeModal(trigger.closest(".modal-backdrop")));
|
||||
});
|
||||
qsa(".modal-backdrop").forEach((backdrop) => {
|
||||
backdrop.addEventListener("mousedown", (event) => {
|
||||
if (event.target === backdrop) closeModal(backdrop);
|
||||
});
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") closeModal(qs(".modal-backdrop.open"));
|
||||
});
|
||||
|
||||
qsa("[role='tab'][data-tab-target]").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const group = tab.closest("[data-tabs]");
|
||||
qsa("[role='tab']", group).forEach((item) => {
|
||||
item.setAttribute("aria-selected", String(item === tab));
|
||||
item.tabIndex = item === tab ? 0 : -1;
|
||||
});
|
||||
qsa(".tab-panel", group.parentElement).forEach((panel) => {
|
||||
const active = panel.id === tab.dataset.tabTarget;
|
||||
panel.classList.toggle("active", active);
|
||||
panel.hidden = !active;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
qsa("[data-filter-input]").forEach((input) => {
|
||||
input.addEventListener("input", () => {
|
||||
const table = qs(input.dataset.filterInput);
|
||||
const query = input.value.trim().toLocaleLowerCase("zh-CN");
|
||||
qsa("tbody tr", table).forEach((row) => {
|
||||
row.hidden = query && !row.textContent.toLocaleLowerCase("zh-CN").includes(query);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
qsa("[data-toast]").forEach((button) => {
|
||||
button.addEventListener("click", () => toast(button.dataset.toast));
|
||||
});
|
||||
|
||||
const addDevice = qs("[data-action='add-device']");
|
||||
addDevice?.addEventListener("click", () => {
|
||||
const name = qs("#device-name")?.value.trim() || "新摄像头";
|
||||
const address = qs("#device-address")?.value.trim() || "192.168.10.88";
|
||||
const tbody = qs("#device-table tbody");
|
||||
if (tbody) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `<td><span class="table-title"></span><span class="table-subtitle">待探测 · video</span></td><td></td><td><span class="status status-warning">待激活</span></td><td>尚未选择</td><td><button class="btn btn-quiet" data-toast="已打开设备探测流程">查看</button></td>`;
|
||||
qs(".table-title", row).textContent = name;
|
||||
row.children[1].textContent = address;
|
||||
qs("button", row).addEventListener("click", () => toast("已打开设备探测流程"));
|
||||
tbody.prepend(row);
|
||||
}
|
||||
closeModal(addDevice.closest(".modal-backdrop"));
|
||||
toast(`${name} 已保存为待激活设备`);
|
||||
});
|
||||
|
||||
let frameCount = 12842;
|
||||
qs("[data-action='run-frame']")?.addEventListener("click", (event) => {
|
||||
event.currentTarget.disabled = true;
|
||||
event.currentTarget.textContent = "处理中…";
|
||||
window.setTimeout(() => {
|
||||
frameCount += 1;
|
||||
const counter = qs("[data-frame-count]");
|
||||
if (counter) counter.textContent = frameCount.toLocaleString("zh-CN");
|
||||
const events = qs("[data-brain-events]");
|
||||
if (events) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "activity-item";
|
||||
item.innerHTML = `<div class="activity-icon"><svg aria-hidden="true"><use href="#icon-activity"></use></svg></div><div><div class="activity-title">方向越线候选</div><div class="activity-meta">synthetic-camera-01 · 置信度 0.91 · 项目内事件</div></div><div class="activity-time">刚刚</div>`;
|
||||
events.prepend(item);
|
||||
}
|
||||
event.currentTarget.disabled = false;
|
||||
event.currentTarget.textContent = "处理下一帧";
|
||||
toast("已完成一帧推理并生成项目内候选事件");
|
||||
}, 520);
|
||||
});
|
||||
|
||||
qs("[data-action='inject-event']")?.addEventListener("click", (event) => {
|
||||
const button = event.currentTarget;
|
||||
const idleText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = "注入中…";
|
||||
window.setTimeout(() => {
|
||||
const count = qs("[data-alert-count]");
|
||||
if (count) count.textContent = String(Number(count.textContent) + 1);
|
||||
button.disabled = false;
|
||||
button.textContent = idleText;
|
||||
closeModal(button.closest(".modal-backdrop"));
|
||||
toast("合成事件已校验并生成新的 Alert");
|
||||
}, 520);
|
||||
});
|
||||
|
||||
const alertState = qs("[data-alert-state]");
|
||||
const ackButton = qs("[data-action='ack-alert']");
|
||||
const closeButton = qs("[data-action='close-alert']");
|
||||
ackButton?.addEventListener("click", () => {
|
||||
alertState.textContent = "已确认";
|
||||
alertState.className = "status status-warning el-tag";
|
||||
ackButton.disabled = true;
|
||||
closeButton.disabled = false;
|
||||
const timeline = qs("[data-alert-timeline]");
|
||||
timeline?.insertAdjacentHTML("beforeend", `<div class="timeline-item"><div class="timeline-title">王值班员确认预警</div><div class="timeline-meta">刚刚 · 首个成功 ack · 已写入审计</div></div>`);
|
||||
toast("预警已由王值班员确认");
|
||||
});
|
||||
closeButton?.addEventListener("click", () => {
|
||||
alertState.textContent = "已关闭";
|
||||
alertState.className = "status status-success el-tag";
|
||||
closeButton.disabled = true;
|
||||
qs("[data-alert-timeline]")?.insertAdjacentHTML("beforeend", `<div class="timeline-item"><div class="timeline-title">处置完成并关闭</div><div class="timeline-meta">刚刚 · 原始 Event 保持不变</div></div>`);
|
||||
toast("Alert 已关闭,原始 Event 未修改");
|
||||
});
|
||||
|
||||
qsa("[data-select-alert]").forEach((row) => {
|
||||
const select = () => {
|
||||
qsa("[data-select-alert]").forEach((item) => item.classList.toggle("selected", item === row));
|
||||
toast(`已选择:${row.dataset.selectAlert}`);
|
||||
};
|
||||
row.addEventListener("click", select);
|
||||
if (row.tagName !== "BUTTON") {
|
||||
row.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
select();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const initial = qs("[data-view-target].active")?.dataset.viewTarget || qs("[data-view]")?.dataset.view;
|
||||
if (initial) setView(initial);
|
||||
})();
|
||||
Vendored
-41
@@ -1,41 +0,0 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const shell = document.querySelector("[data-go-admin-shell]");
|
||||
const collapse = document.querySelector("[data-collapse-sidebar]");
|
||||
|
||||
function syncTags(viewName) {
|
||||
document.querySelectorAll(".tags-view-item").forEach((tag) => {
|
||||
const active = tag.dataset.viewTarget === viewName;
|
||||
tag.classList.toggle("active", active);
|
||||
if (active) tag.setAttribute("aria-current", "page");
|
||||
else tag.removeAttribute("aria-current");
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-view-target]").forEach((trigger) => {
|
||||
trigger.addEventListener("click", () => syncTags(trigger.dataset.viewTarget));
|
||||
});
|
||||
|
||||
if (collapse && shell) {
|
||||
collapse.addEventListener("click", () => {
|
||||
const compact = shell.classList.toggle("compact");
|
||||
collapse.setAttribute("aria-expanded", String(!compact));
|
||||
collapse.setAttribute("aria-label", compact ? "展开主导航" : "折叠主导航");
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-go-admin-query]").forEach((form) => {
|
||||
const input = document.querySelector(form.dataset.queryInput || "");
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
input?.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
form.addEventListener("reset", () => {
|
||||
window.setTimeout(() => input?.dispatchEvent(new Event("input", { bubbles: true })), 0);
|
||||
});
|
||||
});
|
||||
|
||||
const initialView = document.querySelector("[data-view-target].active")?.dataset.viewTarget;
|
||||
if (initialView) syncTags(initialView);
|
||||
})();
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Shared prototype adapter for go-admin-ui commit 67d393d713877572fab0b897296a4c1d525fc81d.
|
||||
* This is an offline visual/interaction mapping, not copied framework CSS and not an Element Plus runtime.
|
||||
*/
|
||||
.go-admin-prototype {
|
||||
--ga-primary: #1677ff;
|
||||
--ga-primary-hover: #409eff;
|
||||
--ga-sidebar: #001529;
|
||||
--ga-sidebar-hover: #000c17;
|
||||
--ga-app-bg: #f0f2f5;
|
||||
--ga-border: #e4e7ed;
|
||||
--ga-text: #303133;
|
||||
--ga-muted: #606266;
|
||||
--ga-sidebar-width: 210px;
|
||||
color: var(--ga-text);
|
||||
background: var(--ga-app-bg);
|
||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.go-admin-prototype .go-admin-shell { grid-template-columns: var(--ga-sidebar-width) minmax(0, 1fr); }
|
||||
.go-admin-prototype .sidebar-container { width: var(--ga-sidebar-width); padding: 0; background: var(--ga-sidebar); box-shadow: 2px 0 6px rgba(0, 21, 41, .35); transition: width .28s ease, transform .28s ease; }
|
||||
.go-admin-prototype .sidebar-logo { height: 64px; min-height: 64px; padding: 0 16px; gap: 12px; color: #fff; text-decoration: none; background: #001529; }
|
||||
.go-admin-prototype .brand-mark { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 5px; background: var(--ga-primary); }
|
||||
.go-admin-prototype .brand-copy { min-width: 0; }
|
||||
.go-admin-prototype .brand-name { white-space: nowrap; }
|
||||
.go-admin-prototype .brand-subtitle { color: rgba(255,255,255,.56); white-space: nowrap; }
|
||||
.go-admin-prototype .el-menu { gap: 0; padding: 8px 0; }
|
||||
.go-admin-prototype .menu-group,
|
||||
.go-admin-prototype .el-menu-item { min-height: 50px; height: 50px; padding: 0 18px; border-radius: 0; gap: 14px; color: rgba(255,255,255,.82); }
|
||||
.go-admin-prototype .menu-group { display: flex; align-items: center; font-size: 14px; font-weight: 500; }
|
||||
.go-admin-prototype .menu-group .menu-arrow { width: 14px; margin-left: auto; transform: rotate(90deg); }
|
||||
.go-admin-prototype .el-menu-item:hover { color: #fff; background: var(--ga-sidebar-hover); }
|
||||
.go-admin-prototype .el-menu-item.active { color: var(--ga-primary); background: var(--ga-sidebar-hover); box-shadow: inset 3px 0 0 var(--ga-primary); }
|
||||
.go-admin-prototype .side-note { margin: 12px; padding: 11px 12px; border-radius: 4px; color: rgba(255,255,255,.72); }
|
||||
|
||||
.go-admin-prototype .main-container { min-width: 0; background: var(--ga-app-bg); }
|
||||
.go-admin-prototype .fixed-header { position: sticky; top: 0; z-index: 30; }
|
||||
.go-admin-prototype .navbar { position: relative; min-height: 50px; height: 50px; padding: 0 16px 0 0; border-bottom: 1px solid #eef0f6; background: #fff; box-shadow: 0 1px 8px rgba(79,70,229,.06); backdrop-filter: none; }
|
||||
.go-admin-prototype .navbar::after { content: ""; position: absolute; right: 0; bottom: 0; left: 0; height: 2px; background: linear-gradient(90deg, #1677ff, #40a9ff); opacity: .7; }
|
||||
.go-admin-prototype .navbar-action,
|
||||
.go-admin-prototype .avatar-button { min-width: 46px; height: 48px; padding: 0 12px; border: 0; display: inline-flex; align-items: center; justify-content: center; gap: 7px; background: transparent; color: #64748b; }
|
||||
.go-admin-prototype .navbar-action:hover,
|
||||
.go-admin-prototype .avatar-button:hover { color: var(--ga-primary); background: rgba(22,119,255,.06); }
|
||||
.go-admin-prototype .navbar-action svg { width: 19px; height: 19px; }
|
||||
.go-admin-prototype .mobile-menu { display: none; }
|
||||
.go-admin-prototype .breadcrumb { display: flex; align-items: center; gap: 9px; color: #97a8be; font-size: 13px; white-space: nowrap; }
|
||||
.go-admin-prototype .breadcrumb a { color: #606266; text-decoration: none; }
|
||||
.go-admin-prototype .breadcrumb strong { color: #303133; font-weight: 500; }
|
||||
.go-admin-prototype .avatar-button { min-width: auto; }
|
||||
.go-admin-prototype .avatar { width: 32px; height: 32px; display: grid; place-items: center; border: 2px solid rgba(22,119,255,.2); border-radius: 50%; color: var(--ga-primary); }
|
||||
.go-admin-prototype .prototype-pill { min-height: 25px; border-radius: 4px; }
|
||||
|
||||
.go-admin-prototype .tags-view-container { height: 40px; padding: 8px 8px 0; display: flex; align-items: flex-end; gap: 3px; overflow-x: auto; border-bottom: 1px solid #e8eaec; background: #fff; box-shadow: 0 1px 4px rgba(0,21,41,.08); }
|
||||
.go-admin-prototype .tags-view-item { height: 32px; padding: 0 10px; border: 1px solid #e0e0e6; border-radius: 3px 3px 0 0; flex: 0 0 auto; background: #f8f9fa; color: #606266; font-size: 12px; }
|
||||
.go-admin-prototype .tags-view-item:hover { color: var(--ga-primary); background: #e6f4ff; }
|
||||
.go-admin-prototype .tags-view-item.active { color: var(--ga-primary); border-color: #91caff; border-bottom-color: #fff; background: #fff; font-weight: 500; }
|
||||
.go-admin-prototype .tags-view-item span { margin-left: 4px; color: #909399; }
|
||||
|
||||
.go-admin-prototype .app-main { width: 100%; min-height: calc(100vh - 90px); padding: 16px; background: var(--ga-app-bg); }
|
||||
.go-admin-prototype .compact-header { margin-bottom: 14px; }
|
||||
.go-admin-prototype .compact-header h1 { font-size: 20px; font-weight: 500; letter-spacing: 0; }
|
||||
.go-admin-prototype .compact-header p { font-size: 13px; }
|
||||
.go-admin-prototype .el-card { border-color: var(--ga-border); border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,.03); }
|
||||
.go-admin-prototype .card-header { min-height: 48px; padding: 10px 14px; }
|
||||
.go-admin-prototype .card-body { padding: 14px; }
|
||||
.go-admin-prototype .metric { padding: 14px; }
|
||||
|
||||
.go-admin-prototype .el-button { min-height: 32px; height: 32px; padding: 5px 12px; border-color: #dcdfe6; border-radius: 4px; background: #fff; color: #606266; font-size: 12px; font-weight: 400; }
|
||||
.go-admin-prototype .el-button svg { width: 15px; height: 15px; }
|
||||
.go-admin-prototype .el-button:hover { color: var(--ga-primary); border-color: #a0cfff; background: #ecf5ff; }
|
||||
.go-admin-prototype .el-button-primary { color: #fff; border-color: var(--ga-primary); background: var(--ga-primary); }
|
||||
.go-admin-prototype .el-button-primary:hover { color: #fff; border-color: var(--ga-primary-hover); background: var(--ga-primary-hover); }
|
||||
.go-admin-prototype .el-button-link { min-height: 30px; height: 30px; padding: 3px 4px; border: 0; color: var(--ga-primary); background: transparent; }
|
||||
.go-admin-prototype .el-button:disabled { color: #a8abb2; border-color: #e4e7ed; background: #f5f7fa; }
|
||||
|
||||
.go-admin-prototype .el-form { color: var(--ga-text); }
|
||||
.go-admin-prototype .el-form-inline { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 18px; padding-bottom: 14px; border-bottom: 1px solid #ebeef5; }
|
||||
.go-admin-prototype .form-item { min-width: 180px; display: grid; grid-template-columns: auto minmax(150px, 220px); align-items: center; gap: 8px; }
|
||||
.go-admin-prototype .form-item label { color: #606266; font-size: 13px; white-space: nowrap; }
|
||||
.go-admin-prototype .form-actions { display: flex; gap: 8px; }
|
||||
.go-admin-prototype .el-input,
|
||||
.go-admin-prototype .el-select { min-height: 32px; height: 32px; padding: 4px 10px; border-color: #dcdfe6; border-radius: 4px; color: #606266; font-size: 13px; }
|
||||
.go-admin-prototype .el-input:focus,
|
||||
.go-admin-prototype .el-select:focus { border-color: var(--ga-primary); outline: 2px solid rgba(22,119,255,.16); }
|
||||
.go-admin-prototype .vertical-form { display: grid; gap: 16px; }
|
||||
.go-admin-prototype .vertical-form .form-item { grid-template-columns: 92px minmax(0,1fr); }
|
||||
.go-admin-prototype .vertical-form .help { margin: -8px 0 0 100px; }
|
||||
.go-admin-prototype .form-footer { display: flex; justify-content: flex-end; }
|
||||
|
||||
.go-admin-prototype .operation-bar { min-height: 48px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.go-admin-prototype .operation-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.go-admin-prototype .permission-hint { display: inline-flex; align-items: center; gap: 5px; color: #909399; font-size: 12px; }
|
||||
.go-admin-prototype .permission-hint svg { width: 14px; height: 14px; }
|
||||
.go-admin-prototype .el-table { min-width: 760px; border: 1px solid #ebeef5; }
|
||||
.go-admin-prototype .el-table th,
|
||||
.go-admin-prototype .el-table td { padding: 9px 10px; border-right: 1px solid #ebeef5; border-bottom-color: #ebeef5; }
|
||||
.go-admin-prototype .el-table th { background: #f5f7fa; color: #909399; font-weight: 500; }
|
||||
.go-admin-prototype .el-table tbody tr:nth-child(even) { background: #fafafa; }
|
||||
.go-admin-prototype .el-table tbody tr:hover { background: #f5f7fa; }
|
||||
.go-admin-prototype .checkbox-faux { width: 14px; height: 14px; display: inline-block; border: 1px solid #dcdfe6; border-radius: 2px; background: #fff; }
|
||||
.go-admin-prototype .action-divider { height: 14px; margin: 0 3px; display: inline-block; border-left: 1px solid #dcdfe6; vertical-align: middle; }
|
||||
.go-admin-prototype .el-tag { min-height: 24px; padding: 2px 8px; border: 1px solid currentColor; border-radius: 4px; font-weight: 400; }
|
||||
.go-admin-prototype .el-tag::before { display: none; }
|
||||
|
||||
.go-admin-prototype .pagination-container { min-height: 48px; display: flex; align-items: center; justify-content: flex-end; gap: 6px; color: #606266; font-size: 12px; }
|
||||
.go-admin-prototype .pagination-container button { min-width: 30px; height: 28px; border: 0; border-radius: 3px; background: #f4f4f5; color: #606266; }
|
||||
.go-admin-prototype .pagination-container button.active { color: #fff; background: var(--ga-primary); }
|
||||
.go-admin-prototype .pagination-container button:disabled { color: #c0c4cc; cursor: not-allowed; }
|
||||
.go-admin-prototype .pagination-size { height: 28px; border: 1px solid #dcdfe6; border-radius: 4px; color: #606266; }
|
||||
.go-admin-prototype .pagination-total { margin-right: 4px; }
|
||||
|
||||
.go-admin-prototype .description-list { display: grid; }
|
||||
.go-admin-prototype .description-row { min-height: 43px; padding: 8px 0; display: grid; grid-template-columns: minmax(90px,.7fr) minmax(0,1.5fr) auto; align-items: center; gap: 10px; border-bottom: 1px solid #ebeef5; }
|
||||
.go-admin-prototype .description-row:last-child { border-bottom: 0; }
|
||||
.go-admin-prototype .description-row > span:first-child { color: #909399; font-size: 12px; }
|
||||
.go-admin-prototype .description-row strong { font-size: 13px; font-weight: 500; }
|
||||
|
||||
.go-admin-prototype .sense-video-stage,
|
||||
.go-admin-prototype .sense-zone-canvas { border-radius: 4px; }
|
||||
.go-admin-prototype .el-overlay { background: rgba(0,0,0,.5); }
|
||||
.go-admin-prototype .el-dialog { width: min(600px,100%); border-radius: 4px; }
|
||||
.go-admin-prototype .el-dialog-header,
|
||||
.go-admin-prototype .el-dialog-footer { padding: 16px 20px; }
|
||||
.go-admin-prototype .el-dialog-body { padding: 20px; }
|
||||
.go-admin-prototype .dialog-close { min-height: 32px; height: 32px; color: #909399; }
|
||||
|
||||
.go-admin-prototype .go-admin-shell.compact { grid-template-columns: 54px minmax(0,1fr); }
|
||||
.go-admin-prototype .go-admin-shell.compact .sidebar-container { width: 54px; }
|
||||
.go-admin-prototype .go-admin-shell.compact .brand-copy,
|
||||
.go-admin-prototype .go-admin-shell.compact .el-menu-item span,
|
||||
.go-admin-prototype .go-admin-shell.compact .menu-group span,
|
||||
.go-admin-prototype .go-admin-shell.compact .menu-arrow,
|
||||
.go-admin-prototype .go-admin-shell.compact .side-note { display: none; }
|
||||
.go-admin-prototype .go-admin-shell.compact .sidebar-logo,
|
||||
.go-admin-prototype .go-admin-shell.compact .menu-group,
|
||||
.go-admin-prototype .go-admin-shell.compact .el-menu-item { padding-right: 0; padding-left: 0; justify-content: center; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.go-admin-prototype .go-admin-shell,
|
||||
.go-admin-prototype .go-admin-shell.compact { display: block; }
|
||||
.go-admin-prototype .sidebar-container,
|
||||
.go-admin-prototype .go-admin-shell.compact .sidebar-container { position: fixed; top: 0; left: 0; z-index: 40; width: min(300px,88vw); height: 100vh; transform: translateX(-102%); }
|
||||
.go-admin-prototype .sidebar-container.open,
|
||||
.go-admin-prototype .go-admin-shell.compact .sidebar-container.open { transform: translateX(0); }
|
||||
.go-admin-prototype .go-admin-shell.compact .brand-copy,
|
||||
.go-admin-prototype .go-admin-shell.compact .el-menu-item span,
|
||||
.go-admin-prototype .go-admin-shell.compact .menu-group span,
|
||||
.go-admin-prototype .go-admin-shell.compact .menu-arrow,
|
||||
.go-admin-prototype .go-admin-shell.compact .side-note { display: initial; }
|
||||
.go-admin-prototype .go-admin-shell.compact .sidebar-logo,
|
||||
.go-admin-prototype .go-admin-shell.compact .menu-group,
|
||||
.go-admin-prototype .go-admin-shell.compact .el-menu-item { padding: 0 18px; justify-content: flex-start; }
|
||||
.go-admin-prototype .desktop-collapse { display: none; }
|
||||
.go-admin-prototype .mobile-menu { display: inline-flex; }
|
||||
.go-admin-prototype .breadcrumb { max-width: 46vw; overflow: hidden; }
|
||||
.go-admin-prototype .breadcrumb a,
|
||||
.go-admin-prototype .breadcrumb span { display: none; }
|
||||
.go-admin-prototype .app-main { padding: 12px; }
|
||||
.go-admin-prototype .tags-view-container { height: 48px; padding-top: 8px; }
|
||||
.go-admin-prototype .tags-view-item { height: 40px; }
|
||||
.go-admin-prototype .el-button { min-height: 44px; height: 44px; }
|
||||
.go-admin-prototype .el-input,
|
||||
.go-admin-prototype .el-select { min-height: 44px; height: 44px; font-size: 16px; }
|
||||
.go-admin-prototype .el-form-inline { align-items: stretch; }
|
||||
.go-admin-prototype .el-form-inline .form-item { width: 100%; grid-template-columns: 82px minmax(0,1fr); }
|
||||
.go-admin-prototype .el-form-inline .form-actions { margin-left: 90px; }
|
||||
.go-admin-prototype .operation-bar { align-items: flex-start; flex-direction: column; padding: 9px 0; }
|
||||
.go-admin-prototype .pagination-container button,
|
||||
.go-admin-prototype .pagination-size { min-width: 40px; height: 40px; }
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.go-admin-prototype .desktop-only,
|
||||
.go-admin-prototype .prototype-pill { display: none; }
|
||||
.go-admin-prototype .topbar-actions { gap: 0; }
|
||||
.go-admin-prototype .navbar-action,
|
||||
.go-admin-prototype .avatar-button { min-width: 42px; padding: 0 8px; }
|
||||
.go-admin-prototype .vertical-form .form-item { grid-template-columns: 1fr; gap: 5px; }
|
||||
.go-admin-prototype .vertical-form .help { margin-left: 0; }
|
||||
.go-admin-prototype .pagination-container { justify-content: flex-start; overflow-x: auto; }
|
||||
}
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f5f7fb;
|
||||
--surface: #ffffff;
|
||||
--surface-muted: #f8fafc;
|
||||
--text: #172033;
|
||||
--text-muted: #5d687c;
|
||||
--border: #dce2ec;
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--primary-soft: #e8f0ff;
|
||||
--success: #16794a;
|
||||
--success-soft: #e9f7ef;
|
||||
--warning: #a15c00;
|
||||
--warning-soft: #fff5df;
|
||||
--danger: #c62828;
|
||||
--danger-soft: #fff0f0;
|
||||
--neutral-soft: #eef2f7;
|
||||
--sidebar: #111b2e;
|
||||
--sidebar-muted: #91a0b8;
|
||||
--focus: #0b6bcb;
|
||||
--radius-sm: 6px;
|
||||
--radius: 10px;
|
||||
--radius-lg: 16px;
|
||||
--shadow-sm: 0 1px 2px rgba(18, 32, 56, 0.06);
|
||||
--shadow-md: 0 8px 24px rgba(18, 32, 56, 0.09);
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--sidebar-width: 240px;
|
||||
font-family: Inter, "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { min-width: 320px; background: var(--bg); }
|
||||
body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.5; }
|
||||
button, input, select, textarea { font: inherit; }
|
||||
button, a, [role="button"] { touch-action: manipulation; }
|
||||
button { cursor: pointer; }
|
||||
a { color: inherit; }
|
||||
svg { width: 20px; height: 20px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
:focus-visible { outline: 3px solid color-mix(in srgb, var(--focus) 42%, transparent); outline-offset: 2px; }
|
||||
#main-content:focus { outline: none; }
|
||||
.skip-link { position: fixed; left: 12px; top: -80px; z-index: 1000; background: var(--surface); padding: 10px 14px; border-radius: var(--radius-sm); box-shadow: var(--shadow-md); }
|
||||
.skip-link:focus { top: 12px; }
|
||||
.sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||
|
||||
.app-shell { min-height: 100vh; display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); }
|
||||
.sidebar { position: sticky; top: 0; height: 100vh; overflow-y: auto; padding: 18px 14px; background: var(--sidebar); color: #fff; z-index: 40; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 8px 18px; }
|
||||
.brand-mark { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 9px; background: linear-gradient(135deg, #3b82f6, #1d4ed8); color: #fff; font-weight: 800; letter-spacing: -0.04em; }
|
||||
.brand-name { font-size: 16px; font-weight: 700; letter-spacing: 0.01em; }
|
||||
.brand-subtitle { display: block; color: var(--sidebar-muted); font-size: 11px; font-weight: 500; }
|
||||
.side-label { margin: 12px 10px 6px; color: var(--sidebar-muted); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
.side-nav { display: grid; gap: 4px; }
|
||||
.side-nav button, .side-nav a { width: 100%; min-height: 44px; border: 0; border-radius: 8px; padding: 10px 12px; display: flex; align-items: center; gap: 10px; background: transparent; color: #c8d1e0; text-align: left; text-decoration: none; transition: background 180ms ease, color 180ms ease; }
|
||||
.side-nav button:hover, .side-nav a:hover { background: rgba(255,255,255,.07); color: #fff; }
|
||||
.side-nav button.active, .side-nav a.active { background: #243653; color: #fff; box-shadow: inset 3px 0 0 #60a5fa; }
|
||||
.side-note { margin: 18px 4px 0; padding: 12px; border: 1px solid rgba(255,255,255,.12); border-radius: 10px; background: rgba(255,255,255,.04); color: #c8d1e0; font-size: 12px; }
|
||||
.side-note strong { color: #fff; display: block; margin-bottom: 4px; }
|
||||
|
||||
.main-column { min-width: 0; }
|
||||
.topbar { position: sticky; top: 0; z-index: 30; min-height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 24px; border-bottom: 1px solid var(--border); background: rgba(255,255,255,.94); backdrop-filter: blur(10px); }
|
||||
.topbar-left, .topbar-actions, .inline { display: flex; align-items: center; gap: 10px; }
|
||||
.btn.mobile-menu { display: none; }
|
||||
.page-eyebrow { color: var(--text-muted); font-size: 12px; }
|
||||
.page-name { margin: 0; font-size: 18px; line-height: 1.25; }
|
||||
.prototype-pill { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 4px 9px; border: 1px solid #b8cdf8; border-radius: 999px; background: var(--primary-soft); color: #194fae; font-size: 12px; font-weight: 650; white-space: nowrap; }
|
||||
.prototype-pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--primary); }
|
||||
.content { width: min(1540px, 100%); margin: 0 auto; padding: 24px; }
|
||||
.view { display: none; animation: fade-in 180ms ease-out; }
|
||||
.view.active { display: block; }
|
||||
@keyframes fade-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 20px; }
|
||||
.page-header h1 { margin: 0 0 4px; font-size: clamp(22px, 2vw, 30px); line-height: 1.25; letter-spacing: -0.02em; }
|
||||
.page-header p { margin: 0; color: var(--text-muted); max-width: 760px; }
|
||||
.page-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||
.btn { min-height: 40px; padding: 8px 13px; border: 1px solid var(--border); border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; background: var(--surface); color: var(--text); font-weight: 650; text-decoration: none; transition: border-color 180ms ease, background 180ms ease, color 180ms ease, box-shadow 180ms ease; }
|
||||
.btn:hover { border-color: #a9b5c8; background: #f8fafc; }
|
||||
.btn-primary { border-color: var(--primary); background: var(--primary); color: #fff; }
|
||||
.btn-primary:hover { border-color: var(--primary-hover); background: var(--primary-hover); }
|
||||
.btn-danger { border-color: #e7b8b8; background: var(--danger-soft); color: var(--danger); }
|
||||
.btn-quiet { border-color: transparent; background: transparent; color: var(--text-muted); }
|
||||
.btn-icon { width: 42px; padding: 8px; }
|
||||
.btn:disabled { cursor: not-allowed; opacity: .48; }
|
||||
|
||||
.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||
.metric { min-width: 0; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-sm); }
|
||||
.metric-label { color: var(--text-muted); font-size: 12px; font-weight: 650; }
|
||||
.metric-value { margin: 5px 0 3px; font-size: 26px; font-weight: 760; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
|
||||
.metric-foot { display: flex; align-items: center; gap: 6px; color: var(--text-muted); font-size: 12px; }
|
||||
.metric-foot strong { color: var(--success); }
|
||||
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-2 { grid-template-columns: minmax(0, 1.6fr) minmax(280px, .9fr); }
|
||||
.grid-even { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.card { min-width: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-sm); }
|
||||
.card-header { min-height: 54px; padding: 13px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--border); }
|
||||
.card-header h2, .card-header h3 { margin: 0; font-size: 15px; }
|
||||
.card-body { padding: 16px; }
|
||||
.card-subtitle { margin: 2px 0 0; color: var(--text-muted); font-size: 12px; }
|
||||
.stack { display: grid; gap: 12px; align-content: start; }
|
||||
|
||||
.status { min-height: 25px; padding: 3px 8px; display: inline-flex; align-items: center; gap: 6px; border-radius: 999px; font-size: 12px; font-weight: 680; white-space: nowrap; }
|
||||
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
||||
.status-success { background: var(--success-soft); color: var(--success); }
|
||||
.status-warning { background: var(--warning-soft); color: var(--warning); }
|
||||
.status-danger { background: var(--danger-soft); color: var(--danger); }
|
||||
.status-neutral { background: var(--neutral-soft); color: #4c5a70; }
|
||||
|
||||
.toolbar { display: flex; flex-wrap: wrap; align-items: end; gap: 10px; margin-bottom: 12px; }
|
||||
.field { display: grid; gap: 5px; min-width: 170px; }
|
||||
.field.grow { flex: 1 1 240px; }
|
||||
.field label { font-size: 12px; font-weight: 680; color: #465268; }
|
||||
.input, select.input, textarea.input { width: 100%; min-height: 42px; border: 1px solid #cfd7e4; border-radius: 8px; padding: 8px 11px; background: #fff; color: var(--text); }
|
||||
.input:focus { border-color: var(--focus); outline: 3px solid color-mix(in srgb, var(--focus) 18%, transparent); outline-offset: 0; }
|
||||
.help { color: var(--text-muted); font-size: 12px; }
|
||||
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 680px; }
|
||||
th, td { padding: 11px 12px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: middle; }
|
||||
th { background: var(--surface-muted); color: #465268; font-size: 12px; font-weight: 720; }
|
||||
td { font-variant-numeric: tabular-nums; }
|
||||
tbody tr { transition: background 160ms ease; }
|
||||
tbody tr:hover { background: #f8fbff; }
|
||||
.table-title { font-weight: 680; }
|
||||
.table-subtitle { display: block; margin-top: 2px; color: var(--text-muted); font-size: 12px; }
|
||||
.empty-row { text-align: center; color: var(--text-muted); padding: 30px; }
|
||||
|
||||
.video-stage { position: relative; min-height: 360px; overflow: hidden; border-radius: 9px; background: radial-gradient(circle at 45% 38%, #31445d, #172337 55%, #0d1524); color: #dfe8f4; }
|
||||
.video-stage::before { content: ""; position: absolute; inset: 18% 12%; border: 1px solid rgba(255,255,255,.12); background: linear-gradient(145deg, transparent 45%, rgba(255,255,255,.04)); }
|
||||
.camera-caption { position: absolute; left: 14px; top: 14px; display: flex; gap: 8px; align-items: center; padding: 7px 10px; border-radius: 7px; background: rgba(7,13,24,.72); font-size: 12px; }
|
||||
.video-time { position: absolute; right: 14px; bottom: 12px; font: 12px ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||
.zone-shape { position: absolute; inset: 24% 22% 18% 18%; border: 2px solid #60a5fa; background: rgba(37,99,235,.16); clip-path: polygon(8% 14%, 88% 0, 100% 72%, 42% 100%, 0 78%); }
|
||||
.zone-label { position: absolute; left: 23%; top: 30%; padding: 4px 7px; border-radius: 5px; background: #1d4ed8; color: #fff; font-size: 11px; font-weight: 700; }
|
||||
|
||||
.activity-list { display: grid; gap: 10px; }
|
||||
.activity-item { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: start; padding: 10px 0; border-bottom: 1px solid var(--border); }
|
||||
.activity-item:last-child { border-bottom: 0; }
|
||||
.activity-icon { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 8px; background: var(--primary-soft); color: var(--primary); }
|
||||
.activity-title { font-weight: 680; }
|
||||
.activity-meta { color: var(--text-muted); font-size: 12px; }
|
||||
.activity-time { color: var(--text-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.pipeline { display: grid; grid-template-columns: repeat(6, minmax(112px, 1fr)); gap: 8px; overflow-x: auto; padding-bottom: 4px; }
|
||||
.pipeline-step { position: relative; min-height: 116px; padding: 13px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); }
|
||||
.pipeline-step:not(:last-child)::after { content: ""; position: absolute; right: -9px; top: 50%; width: 9px; border-top: 2px solid #9fb2ce; }
|
||||
.pipeline-index { width: 25px; height: 25px; display: grid; place-items: center; border-radius: 50%; background: var(--primary-soft); color: var(--primary); font-weight: 750; font-size: 12px; }
|
||||
.pipeline-step h3 { margin: 9px 0 3px; font-size: 13px; }
|
||||
.pipeline-step p { margin: 0; color: var(--text-muted); font-size: 12px; }
|
||||
.pipeline-step.running { border-color: #8bb2ff; box-shadow: inset 0 0 0 1px #8bb2ff; }
|
||||
|
||||
.alert-row { display: grid; grid-template-columns: 5px minmax(0, 1fr) auto; gap: 12px; padding: 13px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); cursor: pointer; }
|
||||
.alert-row:hover, .alert-row.selected { border-color: #9db7e8; background: #f8fbff; }
|
||||
.severity-bar { border-radius: 999px; background: var(--danger); }
|
||||
.severity-bar.medium { background: var(--warning); }
|
||||
.alert-title { font-weight: 720; }
|
||||
.alert-meta { display: flex; flex-wrap: wrap; gap: 8px 14px; margin-top: 4px; color: var(--text-muted); font-size: 12px; }
|
||||
.timeline { position: relative; display: grid; gap: 0; }
|
||||
.timeline-item { position: relative; padding: 0 0 18px 26px; }
|
||||
.timeline-item::before { content: ""; position: absolute; left: 6px; top: 7px; width: 9px; height: 9px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 0 4px var(--primary-soft); }
|
||||
.timeline-item:not(:last-child)::after { content: ""; position: absolute; left: 10px; top: 19px; bottom: 2px; border-left: 1px solid #bbcae0; }
|
||||
.timeline-title { font-weight: 680; }
|
||||
.timeline-meta { color: var(--text-muted); font-size: 12px; }
|
||||
|
||||
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 14px; overflow-x: auto; }
|
||||
.tabs button { min-height: 42px; padding: 8px 12px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--text-muted); font-weight: 650; white-space: nowrap; }
|
||||
.tabs button[aria-selected="true"] { border-bottom-color: var(--primary); color: var(--primary); }
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
.modal-backdrop { position: fixed; inset: 0; z-index: 100; display: none; align-items: center; justify-content: center; padding: 18px; background: rgba(7, 13, 24, .55); }
|
||||
.modal-backdrop.open { display: flex; }
|
||||
.modal { width: min(520px, 100%); max-height: min(760px, calc(100vh - 36px)); overflow-y: auto; border-radius: var(--radius-lg); background: var(--surface); box-shadow: 0 24px 70px rgba(0,0,0,.28); animation: modal-in 180ms ease-out; }
|
||||
@keyframes modal-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.modal-header, .modal-footer { padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.modal-header { border-bottom: 1px solid var(--border); }
|
||||
.modal-header h2 { margin: 0; font-size: 17px; }
|
||||
.modal-body { padding: 18px; }
|
||||
.modal-footer { border-top: 1px solid var(--border); justify-content: flex-end; }
|
||||
|
||||
.toast-region { position: fixed; right: 18px; bottom: 18px; z-index: 200; display: grid; gap: 8px; width: min(360px, calc(100vw - 36px)); }
|
||||
.toast { padding: 12px 14px; border: 1px solid #b9c8dc; border-radius: 9px; background: #172033; color: #fff; box-shadow: var(--shadow-md); animation: toast-in 180ms ease-out; }
|
||||
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.prototype-home { min-height: 100vh; background: radial-gradient(circle at 80% 0%, #dbeafe, transparent 30%), var(--bg); }
|
||||
.home-nav { min-height: 68px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px clamp(18px, 5vw, 72px); border-bottom: 1px solid var(--border); background: rgba(255,255,255,.88); }
|
||||
.home-main { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: clamp(42px, 8vw, 92px) 0; }
|
||||
.hero { max-width: 850px; }
|
||||
.hero h1 { margin: 14px 0 16px; font-size: clamp(34px, 6vw, 64px); line-height: 1.05; letter-spacing: -0.045em; }
|
||||
.hero p { max-width: 720px; margin: 0; color: var(--text-muted); font-size: clamp(16px, 2vw, 20px); }
|
||||
.product-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 44px; }
|
||||
.product-card { min-height: 300px; padding: 22px; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-lg); background: rgba(255,255,255,.94); box-shadow: var(--shadow-sm); text-decoration: none; transition: border-color 180ms ease, box-shadow 180ms ease; }
|
||||
.product-card:hover { border-color: #9db7e8; box-shadow: var(--shadow-md); }
|
||||
.product-icon { width: 48px; height: 48px; display: grid; place-items: center; border-radius: 12px; background: var(--primary-soft); color: var(--primary); }
|
||||
.product-icon svg { width: 26px; height: 26px; }
|
||||
.product-card h2 { margin: 22px 0 8px; font-size: 23px; }
|
||||
.product-card p { margin: 0; color: var(--text-muted); }
|
||||
.product-card ul { padding-left: 18px; color: #465268; }
|
||||
.card-link { margin-top: auto; display: flex; align-items: center; justify-content: space-between; color: var(--primary); font-weight: 720; }
|
||||
.boundary-note { margin-top: 20px; padding: 14px 16px; border-left: 4px solid var(--warning); border-radius: 6px; background: var(--warning-soft); color: #714300; }
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.grid-2, .grid-even { grid-template-columns: 1fr; }
|
||||
.product-grid { grid-template-columns: 1fr; }
|
||||
.product-card { min-height: auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.app-shell { display: block; }
|
||||
.sidebar { position: fixed; left: 0; top: 0; width: min(300px, 88vw); transform: translateX(-102%); transition: transform 200ms ease; box-shadow: var(--shadow-md); }
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.btn.mobile-menu { display: inline-flex; }
|
||||
.topbar { padding: 9px 14px; }
|
||||
.topbar-actions .prototype-pill { display: none; }
|
||||
.content { padding: 18px 14px 28px; }
|
||||
.page-header { display: grid; }
|
||||
.page-actions { justify-content: flex-start; }
|
||||
.metric-grid { grid-template-columns: 1fr 1fr; }
|
||||
.metric-value { font-size: 22px; }
|
||||
.video-stage { min-height: 260px; }
|
||||
.pipeline { grid-template-columns: repeat(2, minmax(0, 1fr)); overflow: visible; }
|
||||
.pipeline-step:not(:last-child)::after { display: none; }
|
||||
.home-nav { padding: 10px 16px; }
|
||||
.home-main { width: min(100% - 28px, 1180px); padding-top: 44px; }
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.metric-grid { grid-template-columns: 1fr; }
|
||||
.topbar-actions .btn { display: none; }
|
||||
.page-actions .btn { flex: 1 1 auto; }
|
||||
.activity-item { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.activity-time { grid-column: 2; }
|
||||
.pipeline { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
.bell-go-admin .sidebar { background: linear-gradient(180deg, #7c2d12 0%, #9a3412 44%, #431407 100%); }
|
||||
.bell-go-admin .brand-mark { background: #ffedd5; color: #9a3412; }
|
||||
.bell-go-admin .el-menu-item.active { background: rgba(255, 255, 255, 0.18); }
|
||||
.bell-go-admin .nav-count { margin-left: auto; min-width: 20px; padding: 1px 6px; border-radius: 10px; background: rgba(255,255,255,.2); font-size: 12px; text-align: center; }
|
||||
.bell-go-admin .compact-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.bell-workbench { grid-template-columns: minmax(290px, .8fr) minmax(440px, 1.2fr); align-items: start; }
|
||||
.alert-workbench .card-body { padding: 0; }
|
||||
.alert-list { display: grid; }
|
||||
.alert-row { display: grid; grid-template-columns: 4px minmax(0, 1fr) auto; gap: 14px; width: 100%; min-height: 82px; padding: 15px 18px; border: 0; border-bottom: 1px solid var(--border); background: #fff; color: var(--text); font: inherit; text-align: left; cursor: pointer; }
|
||||
.alert-row:hover { background: #fff7ed; }
|
||||
.alert-row:focus-visible { outline: 2px solid #c2410c; outline-offset: -3px; }
|
||||
.alert-row.selected { background: #fff7ed; box-shadow: inset 3px 0 #c2410c; }
|
||||
.severity-bar { width: 4px; height: 48px; border-radius: 4px; background: var(--danger); }
|
||||
.severity-bar.medium { background: var(--warning); }
|
||||
.alert-title { display: block; margin-bottom: 7px; }
|
||||
.alert-meta { display: flex; flex-wrap: wrap; gap: 6px 12px; color: var(--text-muted); font-size: 13px; }
|
||||
.activity-time { color: var(--text-muted); font-size: 13px; }
|
||||
.situation-summary { display: grid; gap: 12px; }
|
||||
.situation-summary > div { display: grid; grid-template-columns: 92px 1fr; gap: 14px; }
|
||||
.situation-summary span { color: var(--text-muted); }
|
||||
.evidence-preview { display: flex; align-items: flex-end; justify-content: space-between; min-height: 170px; margin: 18px 0; padding: 18px; border-radius: 8px; background: linear-gradient(140deg, #172033, #506078); color: #fff; }
|
||||
.evidence-preview span { color: #e2e8f0; }
|
||||
.quick-actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.primary-task { margin-top: 18px; padding: 15px; border-left: 4px solid #c2410c; border-radius: 4px; background: #fff7ed; }
|
||||
.primary-task p { margin: 0 0 12px; line-height: 1.6; }
|
||||
.technical-details { margin-top: 18px; border: 1px solid var(--border); border-radius: 6px; background: #f8fafc; }
|
||||
.technical-details summary { padding: 11px 14px; cursor: pointer; color: #475569; font-weight: 600; }
|
||||
.technical-details summary:focus-visible { outline: 2px solid #c2410c; outline-offset: 2px; }
|
||||
.technical-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; padding: 0 14px 14px; }
|
||||
.technical-grid > div { display: grid; gap: 4px; }
|
||||
.technical-grid span { color: var(--text-muted); font-size: 13px; }
|
||||
.technical-grid strong { overflow-wrap: anywhere; }
|
||||
.working-list { display: grid; gap: 10px; }
|
||||
.working-item { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 15px; border: 1px solid var(--border); border-radius: 8px; }
|
||||
.working-item p { margin: 5px 0 0; color: var(--text-muted); }
|
||||
.result-options { display: grid; gap: 10px; margin: 0; padding: 0; border: 0; }
|
||||
.result-options legend { margin-bottom: 4px; font-weight: 700; }
|
||||
.result-card { display: flex; gap: 12px; min-height: 64px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; }
|
||||
.result-card:has(input:checked) { border-color: #c2410c; background: #fff7ed; box-shadow: 0 0 0 1px #c2410c; }
|
||||
.result-card input { margin-top: 4px; }
|
||||
.result-card span { display: grid; gap: 3px; }
|
||||
.result-card small { color: var(--text-muted); }
|
||||
.field-error { color: #b91c1c; font-size: 13px; }
|
||||
.dialog-subtitle { margin: 4px 0 0; color: var(--text-muted); font-size: 13px; }
|
||||
.admin-records { margin-bottom: 14px; }
|
||||
@media (max-width: 1100px) { .bell-workbench { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 767px) {
|
||||
.bell-go-admin .compact-metrics { grid-template-columns: 1fr; }
|
||||
.situation-summary > div { grid-template-columns: 1fr; gap: 3px; }
|
||||
.technical-grid { grid-template-columns: 1fr; }
|
||||
.working-item { align-items: stretch; flex-direction: column; }
|
||||
.working-item .btn { width: 100%; }
|
||||
.evidence-preview { align-items: flex-start; flex-direction: column; }
|
||||
}
|
||||
Vendored
-76
@@ -1,76 +0,0 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const qs = (selector, root = document) => root.querySelector(selector);
|
||||
const qsa = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const toastRegion = qs("[data-toast-region]");
|
||||
const toast = (message) => {
|
||||
if (!toastRegion) return;
|
||||
const item = document.createElement("div");
|
||||
item.className = "toast";
|
||||
item.setAttribute("role", "status");
|
||||
item.textContent = message;
|
||||
toastRegion.appendChild(item);
|
||||
window.setTimeout(() => item.remove(), 3600);
|
||||
};
|
||||
const number = (selector) => Number(qs(selector)?.textContent || 0);
|
||||
|
||||
qsa("[data-select-alert]").forEach((row) => row.addEventListener("click", () => {
|
||||
qs("[data-detail-title]").textContent = row.dataset.selectAlert;
|
||||
qs("[data-detail-what]").textContent = row.dataset.selectAlert;
|
||||
qs("[data-detail-location]").textContent = row.dataset.location;
|
||||
qs("[data-detail-meta]").textContent = `${row.dataset.location} · ${row.dataset.time}`;
|
||||
qs("[data-detail-suggestion]").textContent = row.dataset.suggestion;
|
||||
const state = qs("[data-alert-state]");
|
||||
state.textContent = row.dataset.risk;
|
||||
state.className = `status ${row.dataset.risk === "高风险" ? "status-danger" : "status-warning"} el-tag`;
|
||||
}));
|
||||
|
||||
let started = false;
|
||||
const startButtons = qsa("[data-action='start-alert'], [data-action='table-start-alert']");
|
||||
const startAlert = () => {
|
||||
if (started) {
|
||||
toast("该预警已由王值班员开始处理");
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
startButtons.forEach((button) => {
|
||||
button.disabled = true;
|
||||
button.textContent = "已确认";
|
||||
});
|
||||
const dashboardState = qs("[data-alert-state]");
|
||||
dashboardState.textContent = "处理中";
|
||||
dashboardState.className = "status status-warning el-tag";
|
||||
const tableState = qs("[data-table-alert-state]");
|
||||
tableState.textContent = "处理中";
|
||||
tableState.className = "status status-warning el-tag";
|
||||
qs("[data-table-owner]").textContent = "王值班员";
|
||||
qs("[data-finish-from-table]").disabled = false;
|
||||
qs("[data-pending-count]").textContent = String(Math.max(0, number("[data-pending-count]") - 1));
|
||||
qs("[data-working-count]").textContent = String(number("[data-working-count]") + 1);
|
||||
toast("预警已确认并由王值班员开始处理");
|
||||
};
|
||||
startButtons.forEach((button) => button.addEventListener("click", startAlert));
|
||||
|
||||
qsa("input[name='result']").forEach((input) => input.addEventListener("change", () => {
|
||||
qs("[data-result-error]").hidden = true;
|
||||
}));
|
||||
|
||||
qs("[data-action='finish-alert']")?.addEventListener("click", () => {
|
||||
const selected = qs("input[name='result']:checked");
|
||||
const error = qs("[data-result-error]");
|
||||
if (!selected) {
|
||||
error.hidden = false;
|
||||
qs("input[name='result']")?.focus();
|
||||
return;
|
||||
}
|
||||
const tableState = qs("[data-table-alert-state]");
|
||||
tableState.textContent = "已关闭";
|
||||
tableState.className = "status status-success el-tag";
|
||||
qs("[data-finish-from-table]").disabled = true;
|
||||
qs("[data-completed-count]").textContent = String(number("[data-completed-count]") + 1);
|
||||
qs("[data-working-count]").textContent = String(Math.max(0, number("[data-working-count]") - 1));
|
||||
qs("[data-close-modal]", qs("#finish-alert"))?.click();
|
||||
toast(`预警已关闭,处理结果:${selected.value}`);
|
||||
});
|
||||
})();
|
||||
@@ -1,83 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="YoVision Bell 普通预警系统后台 MVP 交互原型">
|
||||
<title>YoVision Bell · 预警管理后台</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="../assets/styles.css">
|
||||
<link rel="stylesheet" href="../assets/go-admin-ui.css">
|
||||
<link rel="stylesheet" href="./bell.css">
|
||||
</head>
|
||||
<body class="go-admin-prototype bell-go-admin">
|
||||
<svg class="sr-only" aria-hidden="true">
|
||||
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
|
||||
<symbol id="icon-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></symbol>
|
||||
<symbol id="icon-bell" viewBox="0 0 24 24"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"/></symbol>
|
||||
<symbol id="icon-file" viewBox="0 0 24 24"><path d="M6 2h8l4 4v16H6zM14 2v5h5M9 13h6m-6 4h6"/></symbol>
|
||||
<symbol id="icon-rule" viewBox="0 0 24 24"><path d="M4 6h10m4 0h2M4 12h3m4 0h9M4 18h7m4 0h5"/><circle cx="16" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="13" cy="18" r="2"/></symbol>
|
||||
<symbol id="icon-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></symbol>
|
||||
<symbol id="icon-refresh" viewBox="0 0 24 24"><path d="M20 6v5h-5M4 18v-5h5M18 9a7 7 0 0 0-12-2L4 11m16 2-2 4a7 7 0 0 1-12-2"/></symbol>
|
||||
<symbol id="icon-fullscreen" viewBox="0 0 24 24"><path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/></symbol>
|
||||
<symbol id="icon-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></symbol>
|
||||
<symbol id="icon-arrow" viewBox="0 0 24 24"><path d="M9 18l6-6-6-6"/></symbol>
|
||||
</svg>
|
||||
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
<div class="app-shell go-admin-shell" data-go-admin-shell>
|
||||
<aside class="sidebar sidebar-container" data-sidebar aria-label="Bell 主导航">
|
||||
<a class="brand sidebar-logo" href="../index.html"><span class="brand-mark">B</span><span class="brand-copy"><span class="brand-name">YoVision Bell</span><span class="brand-subtitle">预警管理系统</span></span></a>
|
||||
<nav class="side-nav el-menu" aria-label="预警系统菜单">
|
||||
<div class="menu-group"><svg aria-hidden="true"><use href="#icon-bell"></use></svg><span>预警中心</span><svg class="menu-arrow" aria-hidden="true"><use href="#icon-arrow"></use></svg></div>
|
||||
<button class="active el-menu-item" data-view-target="dashboard" data-title="工作台"><svg aria-hidden="true"><use href="#icon-grid"></use></svg><span>工作台</span></button>
|
||||
<button class="el-menu-item" data-view-target="alerts" data-title="预警管理"><svg aria-hidden="true"><use href="#icon-bell"></use></svg><span>预警管理</span><span class="nav-count">3</span></button>
|
||||
<button class="el-menu-item" data-view-target="events" data-title="事件查询"><svg aria-hidden="true"><use href="#icon-file"></use></svg><span>事件查询</span></button>
|
||||
<button class="el-menu-item" data-view-target="rules" data-title="规则配置"><svg aria-hidden="true"><use href="#icon-rule"></use></svg><span>规则配置</span></button>
|
||||
</nav>
|
||||
<div class="side-note"><strong>常用入口</strong><span>工作台查看最新情况;预警管理完成确认与关闭;事件和规则用于查询与配置。</span></div>
|
||||
</aside>
|
||||
|
||||
<div class="main-column main-container">
|
||||
<div class="fixed-header">
|
||||
<header class="topbar navbar"><div class="topbar-left"><button class="navbar-action mobile-menu" type="button" data-mobile-menu aria-label="打开主导航" aria-expanded="false"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button><button class="navbar-action desktop-collapse" type="button" data-collapse-sidebar aria-label="折叠主导航" aria-expanded="true"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button><nav class="breadcrumb" aria-label="面包屑"><a href="../index.html">YoVision</a><span>/</span><span>Bell</span><span>/</span><strong data-page-name>工作台</strong></nav></div><div class="topbar-actions right-menu"><button class="navbar-action" type="button" data-toast="可按地点、预警或事件搜索" aria-label="全局搜索"><svg aria-hidden="true"><use href="#icon-search"></use></svg></button><button class="navbar-action desktop-only" type="button" data-toast="原型不切换浏览器全屏" aria-label="全屏"><svg aria-hidden="true"><use href="#icon-fullscreen"></use></svg></button><span class="prototype-pill">交互原型 · 非生产数据</span><button class="avatar-button" type="button" data-toast="当前用户:王值班员" aria-label="打开用户菜单"><span class="avatar"><svg aria-hidden="true"><use href="#icon-user"></use></svg></span><span class="desktop-only">王值班员</span></button></div></header>
|
||||
<nav class="tags-view-container" aria-label="已访问页面"><button class="tags-view-item active" data-view-target="dashboard" data-title="工作台" aria-current="page">工作台</button><button class="tags-view-item" data-view-target="alerts" data-title="预警管理">预警管理 <span aria-hidden="true">×</span></button><button class="tags-view-item" data-view-target="events" data-title="事件查询">事件查询 <span aria-hidden="true">×</span></button><button class="tags-view-item" data-view-target="rules" data-title="规则配置">规则配置 <span aria-hidden="true">×</span></button></nav>
|
||||
</div>
|
||||
|
||||
<main id="main-content" class="content app-main" tabindex="-1">
|
||||
<section class="view active" data-view="dashboard">
|
||||
<div class="page-header compact-header"><div><h1>工作台</h1><p>集中查看今日预警、处理进度和最新高风险情况。</p></div><div class="page-actions"><button class="btn el-button" data-view-target="alerts">进入预警管理</button></div></div>
|
||||
<div class="metric-grid"><article class="metric el-card"><div class="metric-label">待确认预警</div><div class="metric-value" data-pending-count>3</div><div class="metric-foot"><span class="status status-danger el-tag">1 条高风险</span></div></article><article class="metric el-card"><div class="metric-label">处理中</div><div class="metric-value" data-working-count>2</div><div class="metric-foot"><span>均已明确处理人</span></div></article><article class="metric el-card"><div class="metric-label">今日已关闭</div><div class="metric-value" data-completed-count>18</div><div class="metric-foot"><strong>平均用时约 4 分钟</strong></div></article><article class="metric el-card"><div class="metric-label">今日事件</div><div class="metric-value">29</div><div class="metric-foot"><span>已生成 23 条预警</span></div></article></div>
|
||||
<div class="grid grid-2 bell-workbench">
|
||||
<article class="card el-card alert-workbench"><header class="card-header"><div><h2>最新预警</h2><p class="card-subtitle">按风险和时间排序</p></div><button class="btn btn-quiet el-button-link" data-view-target="alerts">查看全部</button></header><div class="card-body alert-list" aria-label="最新预警列表"><button class="alert-row selected" type="button" data-select-alert="危险区域有人进入" data-location="北围墙 01" data-time="今天 10:24" data-risk="高风险" data-suggestion="先查看现场;如无法确认,请联系校内保安巡查。"><span class="severity-bar"></span><span><strong class="alert-title">危险区域有人进入</strong><span class="alert-meta"><span>北围墙 01</span><span>待确认</span><span>刚刚</span></span></span><time class="activity-time">10:24</time></button><button class="alert-row" type="button" data-select-alert="有人反向越过警戒线" data-location="宿舍东门 02" data-time="今天 10:19" data-risk="中风险" data-suggestion="查看现场是否有人逆向进入;确认安全后记录处理结果。"><span class="severity-bar medium"></span><span><strong class="alert-title">有人反向越过警戒线</strong><span class="alert-meta"><span>宿舍东门 02</span><span>处理中</span><span>5 分钟前</span></span></span><time class="activity-time">10:19</time></button><button class="alert-row" type="button" data-select-alert="危险区域有人长时间停留" data-location="食堂后门 01" data-time="今天 10:11" data-risk="中风险" data-suggestion="查看现场并确认停留原因;无法判断时请安排人员巡查。"><span class="severity-bar medium"></span><span><strong class="alert-title">危险区域有人长时间停留</strong><span class="alert-meta"><span>食堂后门 01</span><span>待确认</span><span>13 分钟前</span></span></span><time class="activity-time">10:11</time></button></div></article>
|
||||
<article class="card el-card alert-detail"><header class="card-header"><div><h2 data-detail-title>危险区域有人进入</h2><p class="card-subtitle" data-detail-meta>北围墙 01 · 今天 10:24</p></div><span class="status status-danger el-tag" data-alert-state>高风险</span></header><div class="card-body"><div class="situation-summary"><div><span>预警内容</span><strong data-detail-what>危险区域有人进入</strong></div><div><span>发生地点</span><strong data-detail-location>北围墙 01</strong></div><div><span>处理建议</span><strong data-detail-suggestion>先查看现场;如无法确认,请联系校内保安巡查。</strong></div></div><div class="evidence-preview" role="img" aria-label="北围墙预警现场的模拟画面"><span>北围墙 01 · 10:23:55</span><strong>现场画面</strong></div><div class="quick-actions"><button class="btn el-button" data-toast="已打开现场画面(原型演示)">查看现场</button><button class="btn el-button el-button-primary" data-action="start-alert">确认并开始处理</button></div><details class="technical-details"><summary>技术详情</summary><div class="technical-grid"><div><span>预警编号</span><strong>BEL-A-20260812-0031</strong></div><div><span>关联事件</span><strong>2 条不可变事件</strong></div><div><span>规则版本</span><strong>校园高危区域 · v4</strong></div><div><span>状态映射</span><strong>Alert new → ack → close</strong></div></div></details></div></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="alerts" hidden>
|
||||
<div class="page-header compact-header"><div><h1>预警管理</h1><p>统一查询和处理待确认、处理中及已关闭的预警。</p></div></div>
|
||||
<article class="card el-card box-card"><div class="card-body"><form class="search-form el-form el-form-inline" data-go-admin-query data-query-input="#alert-filter"><div class="form-item"><label for="alert-filter">预警关键字</label><input id="alert-filter" class="input el-input" data-filter-input="#alert-table" placeholder="预警内容、地点或处理人"></div><div class="form-item"><label for="alert-status">预警状态</label><select id="alert-status" class="input el-select"><option>全部状态</option><option>待确认</option><option>处理中</option><option>已关闭</option></select></div><div class="form-item"><label for="alert-level">风险等级</label><select id="alert-level" class="input el-select"><option>全部等级</option><option>高风险</option><option>中风险</option></select></div><div class="form-actions"><button class="btn el-button el-button-primary" type="submit"><svg aria-hidden="true"><use href="#icon-search"></use></svg>搜索</button><button class="btn el-button" type="reset"><svg aria-hidden="true"><use href="#icon-refresh"></use></svg>重置</button></div></form><div class="operation-bar"><div class="operation-actions"><button class="btn el-button" disabled title="请先选择预警">批量导出</button></div><span class="permission-hint">确认、关闭等操作会记录处理人和时间</span></div>
|
||||
<div class="table-wrap"><table id="alert-table" class="el-table"><thead><tr><th>预警内容</th><th>地点</th><th>风险</th><th>状态</th><th>处理人</th><th>发生时间</th><th>操作</th></tr></thead><tbody data-alert-body><tr data-current-alert><td><span class="table-title">危险区域有人进入</span></td><td>北围墙 01</td><td><span class="status status-danger el-tag">高风险</span></td><td><span class="status status-danger el-tag" data-table-alert-state>待确认</span></td><td data-table-owner>—</td><td>10:24</td><td><button class="btn btn-quiet el-button-link" data-action="table-start-alert">确认</button><button class="btn btn-quiet el-button-link" data-open-modal="finish-alert" data-finish-from-table disabled>关闭</button></td></tr><tr><td><span class="table-title">有人反向越过警戒线</span></td><td>宿舍东门 02</td><td><span class="status status-warning el-tag">中风险</span></td><td><span class="status status-warning el-tag">处理中</span></td><td>李值班员</td><td>10:19</td><td><button class="btn btn-quiet el-button-link" data-toast="该预警已由李值班员处理">查看</button></td></tr><tr><td><span class="table-title">危险区域有人长时间停留</span></td><td>食堂后门 01</td><td><span class="status status-warning el-tag">中风险</span></td><td><span class="status status-success el-tag">已关闭</span></td><td>保安室</td><td>10:11</td><td><button class="btn btn-quiet el-button-link" data-toast="已打开预警详情和处理记录">查看</button></td></tr></tbody></table></div><nav class="pagination-container el-pagination" aria-label="预警列表分页"><span class="pagination-total">共 23 条</span><button type="button" disabled aria-label="上一页">‹</button><button type="button" class="active" aria-current="page">1</button><button type="button" aria-label="下一页">›</button><select class="pagination-size" aria-label="每页条数"><option>10 条/页</option><option>20 条/页</option></select></nav>
|
||||
</div></article>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="events" hidden>
|
||||
<div class="page-header compact-header"><div><h1>事件查询</h1><p>查询系统接收的原始事件及其关联预警;原始内容接收后不可修改。</p></div></div>
|
||||
<article class="card el-card box-card"><div class="card-body"><form class="search-form el-form el-form-inline" data-go-admin-query data-query-input="#event-filter"><div class="form-item"><label for="event-filter">事件关键字</label><input id="event-filter" class="input el-input" data-filter-input="#event-table" placeholder="事件类型、来源或地点"></div><div class="form-item"><label for="event-link">关联状态</label><select id="event-link" class="input el-select"><option>全部</option><option>已生成预警</option><option>未生成预警</option></select></div><div class="form-actions"><button class="btn el-button el-button-primary" type="submit"><svg aria-hidden="true"><use href="#icon-search"></use></svg>搜索</button><button class="btn el-button" type="reset"><svg aria-hidden="true"><use href="#icon-refresh"></use></svg>重置</button></div></form><div class="table-wrap"><table id="event-table" class="el-table"><thead><tr><th>事件类型</th><th>来源</th><th>地点</th><th>接收时间</th><th>关联预警</th><th>操作</th></tr></thead><tbody><tr><td><span class="table-title">危险区域进入</span></td><td>视频分析服务</td><td>北围墙 01</td><td>10:23:55</td><td>BEL-A-0031</td><td><button class="btn btn-quiet el-button-link" data-toast="已打开事件与预警关联详情">查看</button></td></tr><tr><td><span class="table-title">方向越线</span></td><td>视频分析服务</td><td>宿舍东门 02</td><td>10:19:12</td><td>BEL-A-0029</td><td><button class="btn btn-quiet el-button-link" data-toast="已打开事件与预警关联详情">查看</button></td></tr><tr><td><span class="table-title">设备离线</span></td><td>第三方设备平台</td><td>体育馆北门</td><td>10:02:31</td><td>未生成</td><td><button class="btn btn-quiet el-button-link" data-toast="已打开原始事件详情">查看</button></td></tr></tbody></table></div><details class="technical-details"><summary>技术接收信息</summary><div class="technical-grid"><div><span>幂等依据</span><strong>生产者身份 + 来源事件编号</strong></div><div><span>重复接收</span><strong>6 条重复收据,未重复创建预警</strong></div></div></details><nav class="pagination-container el-pagination" aria-label="事件列表分页"><span class="pagination-total">共 29 条</span><button type="button" disabled aria-label="上一页">‹</button><button type="button" class="active" aria-current="page">1</button><button type="button" aria-label="下一页">›</button></nav></div></article>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="rules" hidden>
|
||||
<div class="page-header compact-header"><div><h1>规则配置</h1><p>配置哪些事件需要生成预警及其风险等级;已发布版本不会被直接修改。</p></div><div class="page-actions"><button class="btn el-button el-button-primary" data-toast="已创建规则草稿,不影响当前生效版本" title="权限:bell:rule:add">新增规则</button></div></div>
|
||||
<article class="card el-card box-card"><div class="card-body"><form class="search-form el-form el-form-inline"><div class="form-item"><label for="rule-filter">规则名称</label><input id="rule-filter" class="input el-input" placeholder="输入规则名称"></div><div class="form-item"><label for="rule-status">状态</label><select id="rule-status" class="input el-select"><option>全部状态</option><option>已启用</option><option>已停用</option></select></div><div class="form-actions"><button class="btn el-button el-button-primary" type="button" data-toast="已筛选规则">搜索</button><button class="btn el-button" type="reset">重置</button></div></form><div class="table-wrap"><table class="el-table"><thead><tr><th>规则名称</th><th>事件类型</th><th>风险等级</th><th>当前版本</th><th>状态</th><th>更新时间</th><th>操作</th></tr></thead><tbody><tr><td><span class="table-title">校园高危区域</span></td><td>危险区域进入</td><td><span class="status status-danger el-tag">高风险</span></td><td>v4</td><td><span class="status status-success el-tag">已启用</span></td><td>今天 09:20</td><td><button class="btn btn-quiet el-button-link" data-toast="已打开规则草稿编辑页" title="权限:bell:rule:edit">编辑</button></td></tr><tr><td><span class="table-title">反向越过警戒线</span></td><td>方向越线</td><td><span class="status status-warning el-tag">中风险</span></td><td>v2</td><td><span class="status status-success el-tag">已启用</span></td><td>昨天 16:08</td><td><button class="btn btn-quiet el-button-link" data-toast="已打开规则草稿编辑页" title="权限:bell:rule:edit">编辑</button></td></tr></tbody></table></div><div class="boundary-note">规则编辑和发布受角色权限控制;自动升级、通知和排班将在后续版本提供。</div></div></article>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop el-overlay" id="finish-alert" aria-hidden="true"><section class="modal el-dialog" role="dialog" aria-modal="true" aria-labelledby="finish-title"><header class="modal-header el-dialog-header"><div><h2 id="finish-title">关闭预警</h2><p class="dialog-subtitle">北围墙 01 · 危险区域有人进入</p></div><button class="btn btn-icon btn-quiet dialog-close" data-close-modal aria-label="关闭预警窗口">×</button></header><div class="modal-body el-dialog-body"><form class="el-form vertical-form"><fieldset class="result-options"><legend>处理结果 <span aria-hidden="true">*</span></legend><label class="result-card"><input type="radio" name="result" value="确认有危险"><span><strong>确认有危险</strong><small>现场确有危险,需要保留为真实预警</small></span></label><label class="result-card"><input type="radio" name="result" value="误报"><span><strong>误报</strong><small>识别有误,现场没有相关情况</small></span></label><label class="result-card"><input type="radio" name="result" value="现场正常"><span><strong>现场正常</strong><small>情况真实发生,但已确认没有危险</small></span></label><label class="result-card"><input type="radio" name="result" value="无法确认"><span><strong>无法确认</strong><small>信息不足,需要后续复核</small></span></label><span class="field-error" data-result-error role="alert" hidden>请选择处理结果后再关闭预警。</span></fieldset><div class="form-item"><label for="result-note">处理说明(选填)</label><textarea id="result-note" class="input el-input" rows="3" placeholder="例如:已联系保安到现场确认"></textarea></div></form></div><footer class="modal-footer el-dialog-footer"><button class="btn el-button" data-close-modal>取消</button><button class="btn el-button el-button-primary" type="button" data-action="finish-alert">确认结果并关闭</button></footer></section></div>
|
||||
<div class="toast-region" data-toast-region aria-live="polite"></div>
|
||||
<script src="../assets/app.js"></script>
|
||||
<script src="../assets/go-admin-shell.js"></script>
|
||||
<script src="./bell.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,56 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="YoVision Brain 当前 MVP 内部流程原型">
|
||||
<title>YoVision Brain · 当前 MVP 内部流程原型</title>
|
||||
<link rel="stylesheet" href="../assets/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg class="sr-only" aria-hidden="true">
|
||||
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
|
||||
<symbol id="icon-cpu" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 1v3m6-3v3M9 20v3m6-3v3M1 9h3m-3 6h3m16-6h3m-3 6h3M9 9h6v6H9z"/></symbol>
|
||||
<symbol id="icon-input" viewBox="0 0 24 24"><path d="M4 12h13m-5-5l5 5-5 5M20 5v14"/></symbol>
|
||||
<symbol id="icon-activity" viewBox="0 0 24 24"><path d="M3 12h4l2-7 4 14 2-7h6"/></symbol>
|
||||
<symbol id="icon-file" viewBox="0 0 24 24"><path d="M6 2h8l4 4v16H6zM14 2v5h5M9 13h6m-6 4h6"/></symbol>
|
||||
</svg>
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" data-sidebar aria-label="Brain 内部导航">
|
||||
<a class="brand" href="../index.html" style="text-decoration:none"><span class="brand-mark">B</span><span><span class="brand-name">YoVision Brain</span><span class="brand-subtitle">匿名推理交付单元</span></span></a>
|
||||
<div class="side-label">内部验收</div>
|
||||
<nav class="side-nav">
|
||||
<button class="active" data-view-target="pipeline" data-title="推理流水线"><svg aria-hidden="true"><use href="#icon-cpu"></use></svg>推理流水线</button>
|
||||
<button data-view-target="inputs" data-title="输入夹具"><svg aria-hidden="true"><use href="#icon-input"></use></svg>输入夹具</button>
|
||||
<button data-view-target="events" data-title="项目内事件"><svg aria-hidden="true"><use href="#icon-activity"></use></svg>项目内事件</button>
|
||||
</nav>
|
||||
<div class="side-note"><strong>不是客户管理后台</strong>Brain 无交互式账户,不持有 Alert、ack 或通知状态。本页仅用于开发、演示与验收内部流水线。</div>
|
||||
</aside>
|
||||
<div class="main-column">
|
||||
<header class="topbar"><div class="topbar-left"><button class="btn btn-icon mobile-menu" data-mobile-menu aria-label="打开内部导航" aria-expanded="false"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button><div><div class="page-eyebrow">synthetic-lab · CPU 降级模式</div><h2 class="page-name" data-page-name>推理流水线</h2></div></div><div class="topbar-actions"><span class="prototype-pill">内部流程原型</span><a class="btn" href="../index.html">返回原型入口</a></div></header>
|
||||
<main id="main-content" class="content" tabindex="-1">
|
||||
<section class="view active" data-view="pipeline">
|
||||
<div class="page-header"><div><h1>单路匿名推理流水线</h1><p>使用合成视频和项目内配置独立验收,不依赖 Sense/Bell 在线,也不输出正式跨项目契约。</p></div><div class="page-actions"><button class="btn" data-toast="流水线已暂停;现有项目内事件不受影响">暂停</button><button class="btn btn-primary" data-action="run-frame">处理下一帧</button></div></div>
|
||||
<div class="metric-grid"><article class="metric"><div class="metric-label">已处理帧</div><div class="metric-value" data-frame-count>12,842</div><div class="metric-foot"><strong>24.8 FPS</strong><span>· 单路</span></div></article><article class="metric"><div class="metric-label">端到端延迟</div><div class="metric-value">82 ms</div><div class="metric-foot"><strong>实验室值</strong><span>· 非容量承诺</span></div></article><article class="metric"><div class="metric-label">当前目标</div><div class="metric-value">3</div><div class="metric-foot"><span>匿名 track ID</span></div></article><article class="metric"><div class="metric-label">候选事件</div><div class="metric-value">7</div><div class="metric-foot"><span class="status status-neutral">项目内</span></div></article></div>
|
||||
<article class="card"><header class="card-header"><div><h2>执行链</h2><p class="card-subtitle">每层均可替换并独立测试</p></div><span class="status status-success">运行中</span></header><div class="card-body"><div class="pipeline" aria-label="推理流水线六个步骤">
|
||||
<div class="pipeline-step running"><span class="pipeline-index">1</span><h3>输入适配</h3><p>synthetic-campus.mp4<br>1920×1080 · 25 FPS</p></div>
|
||||
<div class="pipeline-step running"><span class="pipeline-index">2</span><h3>视频解码</h3><p>software decode<br>队列 2 帧</p></div>
|
||||
<div class="pipeline-step running"><span class="pipeline-index">3</span><h3>匿名检测</h3><p>person · 3 boxes<br>模型 demo-v1</p></div>
|
||||
<div class="pipeline-step running"><span class="pipeline-index">4</span><h3>单路跟踪</h3><p>3 active tracks<br>无身份信息</p></div>
|
||||
<div class="pipeline-step running"><span class="pipeline-index">5</span><h3>区域判定</h3><p>危险区域 + 方向线<br>规则 v3</p></div>
|
||||
<div class="pipeline-step"><span class="pipeline-index">6</span><h3>项目内事件</h3><p>本地 JSON<br>未绑定 Bell 契约</p></div>
|
||||
</div></div></article>
|
||||
<div class="grid grid-2" style="margin-top:16px"><article class="card"><header class="card-header"><div><h2>模拟画面</h2><p class="card-subtitle">不包含真实未成年人影像</p></div></header><div class="card-body"><div class="video-stage" role="img" aria-label="合成校园画面,显示匿名跟踪框和方向警戒线"><div class="camera-caption"><span class="status status-success">合成输入</span>synthetic-camera-01</div><div class="zone-shape"></div><span class="zone-label">track-0042 · person</span><div style="position:absolute;left:62%;top:34%;width:72px;height:132px;border:2px solid #f59e0b"></div><span class="video-time">frame 12,842</span></div></div></article><article class="card"><header class="card-header"><div><h2>最近候选事实</h2><p class="card-subtitle">正式事件结构等待协同工单</p></div></header><div class="card-body activity-list" data-brain-events><div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-activity"></use></svg></div><div><div class="activity-title">危险区域进入候选</div><div class="activity-meta">track-0042 · 置信度 0.88 · rule-v3</div></div><div class="activity-time">10:20:16</div></div><div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-activity"></use></svg></div><div><div class="activity-title">方向越线候选</div><div class="activity-meta">track-0038 · east-to-west · rule-v3</div></div><div class="activity-time">10:19:42</div></div></div></article></div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="inputs" hidden><div class="page-header"><div><h1>输入夹具</h1><p>本地视频、合成帧和项目内区域配置,确保 Brain 可脱离 Sense 独立测试。</p></div><div class="page-actions"><button class="btn btn-primary" data-toast="已校验夹具:视频、Profile 与区域坐标匹配">校验全部夹具</button></div></div><article class="card"><div class="table-wrap"><table><thead><tr><th>夹具</th><th>类型</th><th>Profile</th><th>区域配置</th><th>状态</th></tr></thead><tbody><tr><td><span class="table-title">synthetic-campus.mp4</span><span class="table-subtitle">无真实人物数据</span></td><td>合成视频</td><td>1920×1080 / 25 FPS</td><td>zone-v3.json</td><td><span class="status status-success">可用</span></td></tr><tr><td><span class="table-title">cross-line-east.jsonl</span><span class="table-subtitle">可重复确定性输入</span></td><td>合成帧序列</td><td>1280×720 / 15 FPS</td><td>line-v2.json</td><td><span class="status status-success">可用</span></td></tr><tr><td><span class="table-title">local-sample.mp4</span><span class="table-subtitle">仓库外路径引用</span></td><td>本地视频</td><td>尚未探测</td><td>无</td><td><span class="status status-warning">待配置</span></td></tr></tbody></table></div></article></section>
|
||||
|
||||
<section class="view" data-view="events" hidden><div class="page-header"><div><h1>项目内匿名事件</h1><p>仅用于 Brain 独立纵切验收。它不是 `contracts/` 中的标准事件,也不会直接投递 Bell。</p></div><div class="page-actions"><button class="btn" data-toast="已导出 7 条脱敏项目内事件">导出测试结果</button></div></div><div class="boundary-note" style="margin:0 0 16px"><strong>契约门禁:</strong>禁止在本目录复制一份临时 Bell 事件协议。正式 producer/source event ID、设备逻辑引用和证据字段由后续协同工单冻结。</div><article class="card"><div class="table-wrap"><table><thead><tr><th>本地事件 ID</th><th>规则</th><th>匿名观测</th><th>规则/模型版本</th><th>结果</th></tr></thead><tbody><tr><td>local-0007</td><td>危险区域进入</td><td>track-0042</td><td>rule-v3 / demo-v1</td><td><span class="status status-success">已生成</span></td></tr><tr><td>local-0006</td><td>方向越线</td><td>track-0038</td><td>rule-v3 / demo-v1</td><td><span class="status status-success">已生成</span></td></tr><tr><td>local-0005</td><td>危险区域停留</td><td>track-0031</td><td>rule-v2 / demo-v1</td><td><span class="status status-neutral">已抑制</span></td></tr></tbody></table></div></article></section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast-region" data-toast-region aria-live="polite"></div>
|
||||
<script src="../assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,227 +0,0 @@
|
||||
# Design System Master File
|
||||
|
||||
> **LOGIC:** When building a specific page, first check `design-system/pages/[page-name].md`.
|
||||
> If that file exists, its rules **override** this Master file.
|
||||
> If not, strictly follow the rules below.
|
||||
|
||||
---
|
||||
|
||||
**Project:** YoVision
|
||||
**Generated:** 2026-08-12 10:10:03
|
||||
**Category:** Analytics Dashboard
|
||||
**Design Dials:** Variance 4/10 (Balanced / Modern) | Motion 3/10 (Subtle) | Density 8/10 (Dense / Dashboard)
|
||||
|
||||
---
|
||||
|
||||
## Global Rules
|
||||
|
||||
### Color Palette
|
||||
|
||||
| Role | Hex | CSS Variable |
|
||||
|------|-----|--------------|
|
||||
| Primary | `#DC2626` | `--color-primary` |
|
||||
| On Primary | `#FFFFFF` | `--color-on-primary` |
|
||||
| Secondary | `#EF4444` | `--color-secondary` |
|
||||
| Accent/CTA | `#2563EB` | `--color-accent` |
|
||||
| Background | `#FFF1F2` | `--color-background` |
|
||||
| Foreground | `#0F172A` | `--color-foreground` |
|
||||
| Muted | `#FCF1F1` | `--color-muted` |
|
||||
| Border | `#FAE4E4` | `--color-border` |
|
||||
| Destructive | `#DC2626` | `--color-destructive` |
|
||||
| Ring | `#DC2626` | `--color-ring` |
|
||||
|
||||
**Color Notes:** Alert red + safety blue
|
||||
|
||||
### Typography
|
||||
|
||||
- **Heading Font:** Inter
|
||||
- **Body Font:** Inter
|
||||
- **Mood:** minimal, clean, swiss, functional, neutral, professional
|
||||
- **Google Fonts:** [Inter + Inter](https://fonts.google.com/share?selection.family=Inter:wght@300;400;500;600;700)
|
||||
|
||||
**CSS Import:**
|
||||
```css
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
```
|
||||
|
||||
### Spacing Variables
|
||||
|
||||
*Density: 8/10 — Dense / Dashboard*
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| `--space-xs` | `2px` / `0.125rem` | Tight gaps |
|
||||
| `--space-sm` | `4px` / `0.25rem` | Icon gaps, inline spacing |
|
||||
| `--space-md` | `8px` / `0.5rem` | Standard padding |
|
||||
| `--space-lg` | `12px` / `0.75rem` | Section padding |
|
||||
| `--space-xl` | `16px` / `1rem` | Large gaps |
|
||||
| `--space-2xl` | `24px` / `1.5rem` | Section margins |
|
||||
| `--space-3xl` | `32px` / `2rem` | Hero padding |
|
||||
|
||||
### Shadow Depths
|
||||
|
||||
| Level | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift |
|
||||
| `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons |
|
||||
| `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns |
|
||||
| `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards |
|
||||
|
||||
---
|
||||
|
||||
## Component Specs
|
||||
|
||||
### Buttons
|
||||
|
||||
```css
|
||||
/* Primary Button */
|
||||
.btn-primary {
|
||||
background: #2563EB;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Secondary Button */
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #DC2626;
|
||||
border: 2px solid #DC2626;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
```
|
||||
|
||||
### Cards
|
||||
|
||||
```css
|
||||
.card {
|
||||
background: #FFF1F2;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
```
|
||||
|
||||
### Inputs
|
||||
|
||||
```css
|
||||
.input {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 200ms ease;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #DC2626;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px #DC262620;
|
||||
}
|
||||
```
|
||||
|
||||
### Modals
|
||||
|
||||
```css
|
||||
.modal-overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-xl);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style Guidelines
|
||||
|
||||
**Style:** Data-Dense Dashboard
|
||||
|
||||
**Keywords:** Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility
|
||||
|
||||
**Best For:** Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing
|
||||
|
||||
**Key Effects:** Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners
|
||||
|
||||
### Page Pattern
|
||||
|
||||
**Pattern Name:** Real-Time / Operations Landing
|
||||
|
||||
- **Conversion Strategy:** For ops/security/iot products. Demo or sandbox link. Trust signals.
|
||||
- **CTA Placement:** Primary CTA in nav + After metrics
|
||||
- **Section Order:** 1. Hero (product + live preview or status), 2. Key metrics/indicators, 3. How it works, 4. CTA (Start trial / Contact)
|
||||
|
||||
---
|
||||
|
||||
## Motion
|
||||
|
||||
**Page Transition** (Subtle) — Trigger: route change | Duration: 200-300ms | Easing: `power1.inOut`
|
||||
|
||||
```js
|
||||
gsap.to(main, { opacity: 0, duration: 0.2, onComplete: () => { navigate(); gsap.fromTo(main, { opacity: 0 }, { opacity: 1, duration: 0.2 }); } });
|
||||
```
|
||||
|
||||
**Framework notes:** Pair with the router's transition hooks (Next.js App Router transitions, React Router's useNavigate, Vue Router's beforeEach/afterEach)
|
||||
|
||||
- ✅ Preload the destination route's critical assets before the exit tween finishes
|
||||
- ❌ Don't block navigation on animation; cap exit duration at ~250ms so the app never feels unresponsive
|
||||
- ⚡ Exit animation should always resolve faster than entrance (asymmetric timing) so back/forward feels snappy
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (Do NOT Use)
|
||||
|
||||
- ❌ Ornate design
|
||||
- ❌ No filtering
|
||||
|
||||
### Additional Forbidden Patterns
|
||||
|
||||
- ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons)
|
||||
- ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer
|
||||
- ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout
|
||||
- ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio
|
||||
- ❌ **Instant state changes** — Always use transitions (150-300ms)
|
||||
- ❌ **Invisible focus states** — Focus states must be visible for a11y
|
||||
|
||||
---
|
||||
|
||||
## Pre-Delivery Checklist
|
||||
|
||||
Before delivering any UI code, verify:
|
||||
|
||||
- [ ] No emojis used as icons (use SVG instead)
|
||||
- [ ] All icons from consistent icon set (Heroicons/Lucide)
|
||||
- [ ] `cursor-pointer` on all clickable elements
|
||||
- [ ] Hover states with smooth transitions (150-300ms)
|
||||
- [ ] Light mode: text contrast 4.5:1 minimum
|
||||
- [ ] Focus states visible for keyboard navigation
|
||||
- [ ] `prefers-reduced-motion` respected
|
||||
- [ ] Responsive: 375px, 768px, 1024px, 1440px
|
||||
- [ ] No content hidden behind fixed navbars
|
||||
- [ ] No horizontal scroll on mobile
|
||||
@@ -1,58 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="YoVision Sense、Brain、Bell 当前 MVP 交互原型入口">
|
||||
<title>YoVision · 当前 MVP 交互原型</title>
|
||||
<link rel="stylesheet" href="assets/styles.css">
|
||||
</head>
|
||||
<body class="prototype-home">
|
||||
<svg class="sr-only" aria-hidden="true">
|
||||
<symbol id="icon-camera" viewBox="0 0 24 24"><path d="M15 10l4.5-2.5v9L15 14z"/><rect x="3" y="6" width="12" height="12" rx="2"/></symbol>
|
||||
<symbol id="icon-cpu" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 1v3m6-3v3M9 20v3m6-3v3M1 9h3m-3 6h3m16-6h3m-3 6h3M9 9h6v6H9z"/></symbol>
|
||||
<symbol id="icon-bell" viewBox="0 0 24 24"><path d="M18 8a6 6 0 00-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"/></symbol>
|
||||
<symbol id="icon-arrow" viewBox="0 0 24 24"><path d="M5 12h14m-5-5l5 5-5 5"/></symbol>
|
||||
</svg>
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
<header class="home-nav">
|
||||
<div class="brand" aria-label="YoVision">
|
||||
<span class="brand-mark">YV</span>
|
||||
<span><span class="brand-name" style="color:var(--text)">YoVision</span><span class="brand-subtitle" style="color:var(--text-muted)">School Safety Suite</span></span>
|
||||
</div>
|
||||
<span class="prototype-pill">MVP #8 · 交互原型</span>
|
||||
</header>
|
||||
<main id="main-content" class="home-main" tabindex="-1">
|
||||
<section class="hero">
|
||||
<span class="status status-neutral">三项目 · 两个独立产品</span>
|
||||
<h1>先独立跑通,<br>再可靠协作。</h1>
|
||||
<p>这组原型对应当前 YoVision MVP:Sense 负责设备与视频感知管理,Brain 负责无界面匿名推理,Bell 负责独立预警处置。页面中的数据均为演示数据。</p>
|
||||
</section>
|
||||
|
||||
<section class="product-grid" aria-label="选择产品原型">
|
||||
<a class="product-card" href="sense/index.html">
|
||||
<span class="product-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></span>
|
||||
<h2>Sense</h2>
|
||||
<p>智能 NVR 与视频感知管理面</p>
|
||||
<ul><li>设备与 ONVIF/RTSP</li><li>MediaMTX 与实时监看</li><li>区域和方向警戒线</li></ul>
|
||||
<span class="card-link">打开 Sense 原型 <svg aria-hidden="true"><use href="#icon-arrow"></use></svg></span>
|
||||
</a>
|
||||
<a class="product-card" href="brain/index.html">
|
||||
<span class="product-icon"><svg aria-hidden="true"><use href="#icon-cpu"></use></svg></span>
|
||||
<h2>Brain</h2>
|
||||
<p>无客户界面的匿名推理交付单元</p>
|
||||
<ul><li>合成/本地输入</li><li>解码、检测与跟踪</li><li>区域/越线项目内事件</li></ul>
|
||||
<span class="card-link">打开内部流程原型 <svg aria-hidden="true"><use href="#icon-arrow"></use></svg></span>
|
||||
</a>
|
||||
<a class="product-card" href="bell/index.html">
|
||||
<span class="product-icon"><svg aria-hidden="true"><use href="#icon-bell"></use></svg></span>
|
||||
<h2>Bell</h2>
|
||||
<p>独立事件预警与处置产品</p>
|
||||
<ul><li>合成事件与不可变 Event</li><li>规则匹配与 Alert</li><li>并发 ack、close 与审计</li></ul>
|
||||
<span class="card-link">打开 Bell 原型 <svg aria-hidden="true"><use href="#icon-arrow"></use></svg></span>
|
||||
</a>
|
||||
</section>
|
||||
<aside class="boundary-note"><strong>边界说明:</strong>三个原型不代表共享账号、数据库或会话。Brain 页面仅供开发与验收理解内部流水线,不是第三个销售管理后台。</aside>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,147 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="YoVision Sense 面向网管与非技术设备管理员的 MVP 交互原型">
|
||||
<title>YoVision Sense · 摄像头管理</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="../assets/styles.css">
|
||||
<link rel="stylesheet" href="../assets/go-admin-ui.css">
|
||||
<link rel="stylesheet" href="./sense.css">
|
||||
</head>
|
||||
<body class="go-admin-prototype sense-go-admin">
|
||||
<svg class="sr-only" aria-hidden="true">
|
||||
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
|
||||
<symbol id="icon-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></symbol>
|
||||
<symbol id="icon-camera" viewBox="0 0 24 24"><path d="M15 10l4.5-2.5v9L15 14z"/><rect x="3" y="6" width="12" height="12" rx="2"/></symbol>
|
||||
<symbol id="icon-monitor" viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="14" rx="2"/><path d="M8 22h8m-4-4v4"/></symbol>
|
||||
<symbol id="icon-zone" viewBox="0 0 24 24"><path d="M5 4l14 2 2 12-11 3-7-8z"/><circle cx="5" cy="4" r="1"/><circle cx="19" cy="6" r="1"/><circle cx="21" cy="18" r="1"/><circle cx="10" cy="21" r="1"/><circle cx="3" cy="13" r="1"/></symbol>
|
||||
<symbol id="icon-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
||||
<symbol id="icon-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></symbol>
|
||||
<symbol id="icon-refresh" viewBox="0 0 24 24"><path d="M20 6v5h-5M4 18v-5h5M18 9a7 7 0 0 0-12-2L4 11m16 2-2 4a7 7 0 0 1-12-2"/></symbol>
|
||||
<symbol id="icon-fullscreen" viewBox="0 0 24 24"><path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/></symbol>
|
||||
<symbol id="icon-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></symbol>
|
||||
<symbol id="icon-arrow" viewBox="0 0 24 24"><path d="M9 18l6-6-6-6"/></symbol>
|
||||
</svg>
|
||||
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
<div class="app-shell go-admin-shell" data-go-admin-shell>
|
||||
<aside class="sidebar sidebar-container" data-sidebar aria-label="Sense 主导航">
|
||||
<a class="brand sidebar-logo" href="../index.html"><span class="brand-mark">S</span><span class="brand-copy"><span class="brand-name">YoVision Sense</span><span class="brand-subtitle">摄像头与警戒区域管理</span></span></a>
|
||||
<nav class="side-nav el-menu" aria-label="摄像头管理菜单">
|
||||
<div class="menu-group"><svg aria-hidden="true"><use href="#icon-camera"></use></svg><span>日常管理</span><svg class="menu-arrow" aria-hidden="true"><use href="#icon-arrow"></use></svg></div>
|
||||
<button class="active el-menu-item" data-view-target="overview" data-title="设备概况"><svg aria-hidden="true"><use href="#icon-grid"></use></svg><span>设备概况</span></button>
|
||||
<button class="el-menu-item" data-view-target="devices" data-title="摄像头管理"><svg aria-hidden="true"><use href="#icon-camera"></use></svg><span>摄像头管理</span></button>
|
||||
<button class="el-menu-item" data-view-target="live" data-title="实时画面"><svg aria-hidden="true"><use href="#icon-monitor"></use></svg><span>实时画面</span></button>
|
||||
<button class="el-menu-item" data-view-target="zones" data-title="警戒区域"><svg aria-hidden="true"><use href="#icon-zone"></use></svg><span>警戒区域</span></button>
|
||||
</nav>
|
||||
<div class="side-note"><strong>日常使用提示</strong><span>先处理概况页中的异常项;技术参数只在“高级信息”中展示。</span></div>
|
||||
</aside>
|
||||
|
||||
<div class="main-column main-container">
|
||||
<div class="fixed-header">
|
||||
<header class="topbar navbar">
|
||||
<div class="topbar-left">
|
||||
<button class="navbar-action mobile-menu" type="button" data-mobile-menu aria-label="打开主导航" aria-expanded="false"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button>
|
||||
<button class="navbar-action desktop-collapse" type="button" data-collapse-sidebar aria-label="折叠主导航" aria-expanded="true"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button>
|
||||
<nav class="breadcrumb" aria-label="面包屑"><a href="../index.html">YoVision</a><span>/</span><span>Sense</span><span>/</span><strong data-page-name>设备概况</strong></nav>
|
||||
</div>
|
||||
<div class="topbar-actions right-menu">
|
||||
<button class="navbar-action" type="button" data-toast="可按摄像头名称或位置搜索" aria-label="全局搜索"><svg aria-hidden="true"><use href="#icon-search"></use></svg></button>
|
||||
<button class="navbar-action desktop-only" type="button" data-toast="原型不切换浏览器全屏" aria-label="全屏"><svg aria-hidden="true"><use href="#icon-fullscreen"></use></svg></button>
|
||||
<span class="prototype-pill">交互原型 · 非生产数据</span>
|
||||
<button class="avatar-button" type="button" data-toast="当前身份:设备管理员" aria-label="打开用户菜单"><span class="avatar"><svg aria-hidden="true"><use href="#icon-user"></use></svg></span><span class="desktop-only">设备管理员</span></button>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="tags-view-container" aria-label="已访问页面">
|
||||
<button class="tags-view-item active" data-view-target="overview" data-title="设备概况" aria-current="page">设备概况</button>
|
||||
<button class="tags-view-item" data-view-target="devices" data-title="摄像头管理">摄像头管理 <span aria-hidden="true">×</span></button>
|
||||
<button class="tags-view-item" data-view-target="live" data-title="实时画面">实时画面 <span aria-hidden="true">×</span></button>
|
||||
<button class="tags-view-item" data-view-target="zones" data-title="警戒区域">警戒区域 <span aria-hidden="true">×</span></button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<main id="main-content" class="content app-main" tabindex="-1">
|
||||
<section class="view active" data-view="overview">
|
||||
<div class="page-header compact-header"><div><h1>设备概况</h1><p>先看需要处理的事项,其余摄像头和服务均正常运行。</p></div><div class="page-actions"><button class="btn el-button el-button-primary" data-open-modal="add-camera"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>添加摄像头</button></div></div>
|
||||
<div class="metric-grid">
|
||||
<article class="metric el-card"><div class="metric-label">摄像头</div><div class="metric-value">6</div><div class="metric-foot"><strong>5 台正常</strong><span>· 1 台待处理</span></div></article>
|
||||
<article class="metric el-card"><div class="metric-label">实时画面</div><div class="metric-value">5</div><div class="metric-foot"><strong>全部可查看</strong></div></article>
|
||||
<article class="metric el-card"><div class="metric-label">已启用警戒区域</div><div class="metric-value">7</div><div class="metric-foot"><span>另有 1 个未发布草稿</span></div></article>
|
||||
<article class="metric el-card"><div class="metric-label">需要处理</div><div class="metric-value">1</div><div class="metric-foot"><span class="status status-danger el-tag">账号验证失败</span></div></article>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<article class="card el-card attention-card"><header class="card-header"><div><h2>需要你处理</h2><p class="card-subtitle">每个问题都提供原因和下一步操作</p></div><span class="status status-danger el-tag">1 项</span></header><div class="card-body">
|
||||
<div class="recovery-item"><div><strong>宿舍东门 02 暂时无法连接</strong><p>原因:摄像头拒绝了当前账号。影响:该位置暂时没有实时画面。</p></div><button class="btn el-button el-button-primary" data-open-modal="recover-camera">重新输入账号密码</button></div>
|
||||
</div></article>
|
||||
<article class="card el-card"><header class="card-header"><div><h2>系统运行情况</h2><p class="card-subtitle">日常使用只需关注是否正常</p></div><span class="status status-success el-tag">正常</span></header><div class="card-body stack">
|
||||
<div class="description-row"><span>摄像头服务</span><strong>运行正常</strong><span class="status status-success el-tag">正常</span></div>
|
||||
<div class="description-row"><span>配置保存</span><strong>已同步,12 秒前检查</strong><span class="status status-success el-tag">正常</span></div>
|
||||
<details class="advanced-panel"><summary>查看高级信息</summary><div class="advanced-content"><div><span>媒体服务</span><strong>MediaMTX · 5/5 paths converged</strong></div><div><span>智能分析连接</span><strong>Brain adapter · 未配置(不影响独立运行)</strong></div><div><span>预警平台连接</span><strong>Bell connector · 未安装(不影响独立运行)</strong></div></div></details>
|
||||
</div></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="devices" hidden>
|
||||
<div class="page-header compact-header"><div><h1>摄像头管理</h1><p>添加、检查和维护摄像头;系统会自动选择推荐的画面清晰度。</p></div><div class="page-actions"><button class="btn el-button el-button-primary" data-open-modal="add-camera"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>添加摄像头</button></div></div>
|
||||
<article class="card el-card box-card"><div class="card-body">
|
||||
<form class="search-form el-form el-form-inline" data-go-admin-query data-query-input="#device-filter">
|
||||
<div class="form-item"><label for="device-filter">摄像头</label><input id="device-filter" class="input el-input" data-filter-input="#device-table" placeholder="输入名称、位置或状态"></div>
|
||||
<div class="form-item"><label for="device-status">运行状态</label><select id="device-status" class="input el-select"><option>全部状态</option><option>正常</option><option>需要处理</option><option>等待设置</option></select></div>
|
||||
<div class="form-actions"><button class="btn el-button el-button-primary" type="submit"><svg aria-hidden="true"><use href="#icon-search"></use></svg>搜索</button><button class="btn el-button" type="reset"><svg aria-hidden="true"><use href="#icon-refresh"></use></svg>重置</button></div>
|
||||
</form>
|
||||
<div class="operation-bar"><div class="operation-actions"><button class="btn el-button" data-open-modal="add-camera">自动发现摄像头</button></div><span class="permission-hint">推荐先使用自动发现;找不到时再手工添加</span></div>
|
||||
<div class="table-wrap"><table id="device-table" class="el-table"><thead><tr><th>摄像头</th><th>位置</th><th>运行状态</th><th>画面清晰度</th><th>操作</th></tr></thead><tbody>
|
||||
<tr><td><span class="table-title">北围墙 01</span><span class="table-subtitle">已稳定运行 4 天</span></td><td>北围墙</td><td><span class="status status-success el-tag">正常</span></td><td>高清(推荐)</td><td><button class="btn btn-quiet el-button-link" data-view-target="live">查看画面</button></td></tr>
|
||||
<tr><td><span class="table-title">宿舍东门 02</span><span class="table-subtitle">账号验证失败</span></td><td>宿舍东门</td><td><span class="status status-danger el-tag">需要处理</span></td><td>标清</td><td><button class="btn btn-quiet el-button-link" data-open-modal="recover-camera">重新输入账号密码</button></td></tr>
|
||||
<tr><td><span class="table-title">食堂后门 01</span><span class="table-subtitle">已发现,尚未完成设置</span></td><td>食堂后门</td><td><span class="status status-warning el-tag">等待设置</span></td><td>系统将自动推荐</td><td><button class="btn btn-quiet el-button-link" data-open-modal="add-camera">继续设置</button></td></tr>
|
||||
</tbody></table></div>
|
||||
<details class="advanced-panel table-advanced"><summary>网管高级信息</summary><div class="advanced-content advanced-grid"><div><span>北围墙 01</span><strong>192.168.10.21 · ONVIF · MainStream 1080p</strong></div><div><span>宿舍东门 02</span><strong>192.168.10.32 · ONVIF · SubStream 720p</strong></div><div><span>食堂后门 01</span><strong>192.168.10.45 · 手工添加 · Profile 待探测</strong></div></div></details>
|
||||
<nav class="pagination-container el-pagination" aria-label="摄像头列表分页"><span class="pagination-total">共 3 条</span><button type="button" disabled aria-label="上一页">‹</button><button type="button" class="active" aria-current="page">1</button><button type="button" disabled aria-label="下一页">›</button><select class="pagination-size" aria-label="每页条数"><option>10 条/页</option><option>20 条/页</option></select></nav>
|
||||
</div></article>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="live" hidden>
|
||||
<div class="page-header compact-header"><div><h1>实时画面</h1><p>选择摄像头即可查看;连接中断时画面会保留并给出恢复操作。</p></div><div class="page-actions"><select class="input el-select" aria-label="选择摄像头"><option>北围墙 01</option><option>宿舍东门 02(需要处理)</option></select><button class="btn el-button" data-toast="正在重新连接,当前画面会继续保留"><svg aria-hidden="true"><use href="#icon-refresh"></use></svg>重新连接</button></div></div>
|
||||
<div class="grid grid-2"><article class="card el-card"><header class="card-header"><h2>北围墙 01</h2><span class="status status-success el-tag">画面正常</span></header><div class="card-body"><div class="video-stage sense-video-stage" role="img" aria-label="北围墙摄像头的模拟实时画面,画面中显示危险区域"><div class="camera-caption">北围墙 01 · 高清</div><div class="zone-shape"></div><span class="zone-label">危险区域 A</span><span class="video-time">2026-08-12 10:18:24</span></div></div></article><article class="card el-card"><header class="card-header"><h2>画面状态</h2></header><div class="card-body description-list"><div class="description-row"><span>当前质量</span><strong>高清 · 画面流畅</strong></div><div class="description-row"><span>连接状态</span><strong>稳定运行 4 小时 18 分</strong></div><details class="advanced-panel"><summary>查看高级信息</summary><div class="advanced-content"><div><span>协议与服务</span><strong>WebRTC / MediaMTX</strong></div><div><span>源画面</span><strong>MainStream · 1920×1080</strong></div><div><span>实时指标</span><strong>1.8 Mbps · 延迟 340 ms</strong></div></div></details></div></article></div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="zones" hidden>
|
||||
<div class="page-header compact-header"><div><h1>警戒区域</h1><p>在画面上标出需要重点关注的区域,保存后再发布生效。</p></div><div class="page-actions"><button class="btn el-button" data-toast="警戒方向已反转,尚未保存">反转警戒方向</button><button class="btn el-button el-button-primary" data-toast="草稿已保存,发布前不会影响当前规则">保存草稿</button></div></div>
|
||||
<div class="grid grid-2"><article class="card el-card"><header class="card-header"><div><h2>在画面上绘制</h2><p class="card-subtitle">拖动控制点调整范围</p></div></header><div class="card-body"><div class="video-stage sense-zone-canvas" role="img" aria-label="区域编辑画布,包含五边形危险区域"><div class="camera-caption">北围墙 01 · 高清画面</div><div class="zone-shape"></div><span class="zone-label">危险区域 A · 未发布</span></div></div></article><article class="card el-card"><header class="card-header"><h2>区域设置</h2><span class="status status-warning el-tag">未发布</span></header><div class="card-body"><form class="el-form vertical-form"><div class="form-item"><label for="zone-name">区域名称</label><input id="zone-name" class="input el-input" value="危险区域 A"></div><div class="form-item"><label for="zone-rule">发现以下情况时提醒</label><select id="zone-rule" class="input el-select"><option>有人进入危险区域</option><option>有人反向越过警戒线</option><option>区域内人员聚集</option></select></div><div class="form-item"><label for="zone-camera">使用摄像头</label><input id="zone-camera" class="input el-input" value="北围墙 01 · 高清" readonly></div><div class="boundary-note">如果更换画面清晰度,系统会提示重新校准,避免警戒范围偏移。</div><details class="advanced-panel"><summary>网管高级信息</summary><div class="advanced-content"><div><span>绑定画面</span><strong>MainStream · 1920×1080</strong></div><div><span>保存方式</span><strong>归一化坐标 · 草稿 v8</strong></div></div></details><div class="form-footer"><button type="button" class="btn el-button el-button-primary" data-toast="警戒区域已发布并生效;上一版本仍可追溯">发布并生效</button></div></form></div></article></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop el-overlay" id="add-camera" aria-hidden="true">
|
||||
<section class="modal el-dialog wizard-dialog" role="dialog" aria-modal="true" aria-labelledby="add-camera-title">
|
||||
<header class="modal-header el-dialog-header"><div><h2 id="add-camera-title">添加摄像头</h2><p class="dialog-subtitle" data-wizard-step-label>第 1 步,共 6 步</p></div><button class="btn btn-icon btn-quiet dialog-close" data-close-modal aria-label="关闭添加摄像头窗口">×</button></header>
|
||||
<div class="modal-body el-dialog-body">
|
||||
<ol class="wizard-progress" aria-label="添加摄像头进度"><li class="active">查找方式</li><li>选择设备</li><li>账号</li><li>测试</li><li>预览</li><li>完成</li></ol>
|
||||
<div class="wizard-panel active" data-wizard-panel="0"><h3>怎样查找摄像头?</h3><p>推荐自动查找同一网络中的摄像头;找不到时再手工填写地址。</p><label class="choice-card selected"><input type="radio" name="discover-method" value="auto" checked><span><strong>自动查找(推荐)</strong><small>无需知道网络地址,约需 10 秒</small></span></label><label class="choice-card"><input type="radio" name="discover-method" value="manual"><span><strong>手工填写地址</strong><small>适合已知道摄像头地址的网管</small></span></label></div>
|
||||
<div class="wizard-panel" data-wizard-panel="1" hidden><div data-auto-choice><h3>选择要添加的摄像头</h3><p>已找到 2 台尚未添加的设备。</p><label class="choice-card selected"><input type="radio" name="camera-choice" checked><span><strong>教学楼西门 · 192.168.10.88</strong><small>海康摄像头 · 信号正常</small></span></label><label class="choice-card"><input type="radio" name="camera-choice"><span><strong>体育馆北门 · 192.168.10.91</strong><small>大华摄像头 · 信号正常</small></span></label></div><div data-manual-choice hidden><h3>填写摄像头地址</h3><p>请输入网管提供的局域网地址,系统会自动检查可用的接入方式。</p><div class="form-item"><label for="manual-address">摄像头地址</label><input id="manual-address" class="input el-input" inputmode="url" placeholder="例如 192.168.10.88"><span class="help">无需填写协议、端口或画面配置,系统会自动探测。</span></div></div></div>
|
||||
<div class="wizard-panel" data-wizard-panel="2" hidden><h3>输入摄像头账号</h3><p>账号只用于连接该摄像头,保存后不会显示密码。</p><form class="el-form vertical-form"><div class="form-item"><label for="wizard-name">摄像头名称</label><input id="wizard-name" class="input el-input" value="教学楼西门 01"></div><div class="form-item"><label for="wizard-user">用户名</label><input id="wizard-user" class="input el-input" value="operator" autocomplete="off"></div><div class="form-item"><label for="wizard-password">密码</label><input id="wizard-password" class="input el-input" type="password" autocomplete="new-password"><span class="help">原型不会保存输入内容。</span></div></form></div>
|
||||
<div class="wizard-panel" data-wizard-panel="3" hidden><h3>测试连接</h3><p>系统会检查账号、网络和画面,并自动选择推荐清晰度。</p><div class="test-status" data-connection-status role="status"><strong>尚未测试</strong><span>点击“测试连接”继续。</span></div><div class="inline-actions"><button class="btn el-button el-button-primary" type="button" data-test-connection>测试连接</button><button class="btn el-button" type="button" data-simulate-failure>演示失败与恢复</button></div></div>
|
||||
<div class="wizard-panel" data-wizard-panel="4" hidden><h3>确认画面</h3><p>画面清楚且位置正确即可继续。</p><div class="video-stage wizard-preview" role="img" aria-label="教学楼西门摄像头预览画面"><div class="camera-caption">教学楼西门 01 · 高清(推荐)</div><span class="video-time">实时预览</span></div><details class="advanced-panel"><summary>网管高级信息</summary><div class="advanced-content"><div><span>接入方式</span><strong>ONVIF</strong></div><div><span>画面配置</span><strong>MainStream · 1920×1080</strong></div></div></details></div>
|
||||
<div class="wizard-panel" data-wizard-panel="5" hidden><div class="completion-state"><span class="completion-mark" aria-hidden="true">✓</span><h3>摄像头已准备好</h3><p>教学楼西门 01 将以高清画面运行。完成后可立即查看实时画面或配置警戒区域。</p></div></div>
|
||||
</div>
|
||||
<footer class="modal-footer el-dialog-footer"><button class="btn el-button" type="button" data-wizard-back disabled>上一步</button><button class="btn el-button el-button-primary" type="button" data-wizard-next>下一步</button></footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop el-overlay" id="recover-camera" aria-hidden="true">
|
||||
<section class="modal el-dialog" role="dialog" aria-modal="true" aria-labelledby="recover-title">
|
||||
<header class="modal-header el-dialog-header"><h2 id="recover-title">恢复宿舍东门 02</h2><button class="btn btn-icon btn-quiet dialog-close" data-close-modal aria-label="关闭恢复窗口">×</button></header>
|
||||
<div class="modal-body el-dialog-body"><div class="recovery-summary"><strong>摄像头拒绝了当前账号</strong><p>该位置暂时没有实时画面。请重新输入摄像头账号密码,然后测试连接。</p></div><form class="el-form vertical-form"><div class="form-item"><label for="recover-user">用户名</label><input id="recover-user" class="input el-input" value="operator"></div><div class="form-item"><label for="recover-password">新密码</label><input id="recover-password" class="input el-input" type="password" autocomplete="new-password" placeholder="输入摄像头当前密码"><span class="field-error" data-recovery-error role="alert" hidden>密码不能为空,请输入后再测试。</span></div></form><div class="test-status" data-recovery-status role="status"><strong>等待测试</strong><span>保存前必须确认能够连接。</span></div><details class="advanced-panel"><summary>网管高级信息</summary><div class="advanced-content"><div><span>设备地址</span><strong>192.168.10.32</strong></div><div><span>错误来源</span><strong>ONVIF authentication rejected</strong></div></div></details></div>
|
||||
<footer class="modal-footer el-dialog-footer"><button class="btn el-button" data-close-modal>取消</button><button class="btn el-button" type="button" data-recovery-test>测试连接</button><button class="btn el-button el-button-primary" type="button" data-recovery-save disabled>保存并恢复</button></footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="toast-region" data-toast-region aria-live="polite"></div>
|
||||
<script src="../assets/app.js"></script>
|
||||
<script src="../assets/go-admin-shell.js"></script>
|
||||
<script src="./sense.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,51 +0,0 @@
|
||||
.sense-go-admin .attention-card { border-top: 3px solid var(--danger); }
|
||||
.sense-go-admin .recovery-item { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||
.sense-go-admin .recovery-item p,
|
||||
.sense-go-admin .wizard-panel > p,
|
||||
.sense-go-admin .recovery-summary p { margin: 6px 0 0; color: var(--text-muted); line-height: 1.6; }
|
||||
.advanced-panel { margin-top: 14px; border: 1px solid var(--border); border-radius: 6px; background: #f8fafc; }
|
||||
.advanced-panel summary { padding: 11px 14px; cursor: pointer; color: #475569; font-weight: 600; }
|
||||
.advanced-panel summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||||
.advanced-content { display: grid; gap: 10px; padding: 0 14px 14px; }
|
||||
.advanced-content > div { display: flex; justify-content: space-between; gap: 16px; color: var(--text-muted); }
|
||||
.advanced-content strong { color: var(--text); text-align: right; overflow-wrap: anywhere; }
|
||||
.table-advanced { margin-bottom: 12px; }
|
||||
.advanced-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.advanced-grid > div { display: grid; }
|
||||
.advanced-grid strong { text-align: left; }
|
||||
.wizard-dialog { width: min(720px, calc(100vw - 32px)); }
|
||||
.dialog-subtitle { margin: 4px 0 0; color: var(--text-muted); font-size: 13px; }
|
||||
.wizard-progress { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px; padding: 0; margin: 0 0 24px; list-style: none; counter-reset: wizard; }
|
||||
.wizard-progress li { position: relative; padding-top: 22px; color: var(--text-muted); font-size: 12px; text-align: center; }
|
||||
.wizard-progress li::before { counter-increment: wizard; content: counter(wizard); position: absolute; top: 0; left: 50%; width: 18px; height: 18px; transform: translateX(-50%); border: 1px solid var(--border); border-radius: 50%; background: #fff; line-height: 18px; }
|
||||
.wizard-progress li::after { content: ""; position: absolute; top: 9px; left: calc(50% + 11px); width: calc(100% - 22px); height: 1px; background: var(--border); }
|
||||
.wizard-progress li:last-child::after { display: none; }
|
||||
.wizard-progress li.active { color: var(--primary); font-weight: 700; }
|
||||
.wizard-progress li.active::before { border-color: var(--primary); background: var(--primary); color: #fff; }
|
||||
.wizard-panel h3 { margin: 0 0 4px; font-size: 18px; }
|
||||
.choice-card { display: flex; align-items: flex-start; gap: 12px; min-height: 66px; margin-top: 12px; padding: 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; }
|
||||
.choice-card:has(input:checked) { border-color: var(--primary); background: #eff6ff; box-shadow: 0 0 0 1px var(--primary); }
|
||||
.choice-card input { margin-top: 4px; }
|
||||
.choice-card span { display: grid; gap: 4px; }
|
||||
.choice-card small { color: var(--text-muted); }
|
||||
.test-status { display: grid; gap: 4px; margin: 18px 0; padding: 14px; border-left: 4px solid #94a3b8; border-radius: 4px; background: #f8fafc; }
|
||||
.test-status.testing { border-color: var(--primary); background: #eff6ff; }
|
||||
.test-status.success { border-color: var(--success); background: #f0fdf4; }
|
||||
.test-status.error { border-color: var(--danger); background: #fef2f2; }
|
||||
.test-status span { color: var(--text-muted); }
|
||||
.inline-actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.wizard-preview { min-height: 260px; background: linear-gradient(140deg, #27364a, #526984); }
|
||||
.completion-state { padding: 28px 12px; text-align: center; }
|
||||
.completion-mark { display: inline-grid; width: 54px; height: 54px; place-items: center; border-radius: 50%; background: #16a34a; color: #fff; font-size: 30px; }
|
||||
.completion-state h3 { margin-top: 16px; }
|
||||
.recovery-summary { padding: 14px; border-left: 4px solid var(--danger); border-radius: 4px; background: #fef2f2; }
|
||||
.field-error { margin-top: 6px; color: #b91c1c; font-size: 13px; }
|
||||
@media (max-width: 767px) {
|
||||
.sense-go-admin .recovery-item { align-items: stretch; flex-direction: column; }
|
||||
.advanced-content > div { display: grid; }
|
||||
.advanced-content strong { text-align: left; }
|
||||
.advanced-grid { grid-template-columns: 1fr; }
|
||||
.wizard-progress li { font-size: 0; }
|
||||
.wizard-progress li::before { font-size: 12px; }
|
||||
.wizard-dialog .modal-body { padding-inline: 16px; }
|
||||
}
|
||||
Vendored
-122
@@ -1,122 +0,0 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const qs = (selector, root = document) => root.querySelector(selector);
|
||||
const qsa = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const toastRegion = qs("[data-toast-region]");
|
||||
const announce = (message) => {
|
||||
if (!toastRegion) return;
|
||||
const item = document.createElement("div");
|
||||
item.className = "toast";
|
||||
item.setAttribute("role", "status");
|
||||
item.textContent = message;
|
||||
toastRegion.appendChild(item);
|
||||
window.setTimeout(() => item.remove(), 3600);
|
||||
};
|
||||
|
||||
qsa(".choice-card input").forEach((input) => {
|
||||
input.addEventListener("change", () => {
|
||||
qsa(`input[name="${input.name}"]`).forEach((peer) => peer.closest(".choice-card")?.classList.toggle("selected", peer.checked));
|
||||
if (input.name === "discover-method") {
|
||||
const manual = input.value === "manual";
|
||||
qs("[data-auto-choice]").hidden = manual;
|
||||
qs("[data-manual-choice]").hidden = !manual;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const wizard = qs("#add-camera");
|
||||
if (wizard) {
|
||||
let step = 0;
|
||||
let connectionPassed = false;
|
||||
const panels = qsa("[data-wizard-panel]", wizard);
|
||||
const progress = qsa(".wizard-progress li", wizard);
|
||||
const back = qs("[data-wizard-back]", wizard);
|
||||
const next = qs("[data-wizard-next]", wizard);
|
||||
const stepLabel = qs("[data-wizard-step-label]", wizard);
|
||||
const connectionStatus = qs("[data-connection-status]", wizard);
|
||||
|
||||
const render = () => {
|
||||
panels.forEach((panel, index) => {
|
||||
panel.hidden = index !== step;
|
||||
panel.classList.toggle("active", index === step);
|
||||
});
|
||||
progress.forEach((item, index) => item.classList.toggle("active", index <= step));
|
||||
stepLabel.textContent = `第 ${step + 1} 步,共 ${panels.length} 步`;
|
||||
back.disabled = step === 0;
|
||||
next.textContent = step === panels.length - 1 ? "完成" : "下一步";
|
||||
next.disabled = step === 3 && !connectionPassed;
|
||||
qs("input, button, h3[tabindex]", panels[step])?.focus({ preventScroll: true });
|
||||
};
|
||||
|
||||
qsa("[data-open-modal='add-camera']").forEach((trigger) => trigger.addEventListener("click", () => {
|
||||
step = trigger.textContent.includes("继续") ? 2 : 0;
|
||||
connectionPassed = false;
|
||||
connectionStatus.className = "test-status";
|
||||
connectionStatus.innerHTML = "<strong>尚未测试</strong><span>点击“测试连接”继续。</span>";
|
||||
render();
|
||||
}));
|
||||
back.addEventListener("click", () => { if (step > 0) { step -= 1; render(); } });
|
||||
next.addEventListener("click", () => {
|
||||
if (step < panels.length - 1) { step += 1; render(); return; }
|
||||
qs("[data-close-modal]", wizard)?.click();
|
||||
announce("教学楼西门 01 已添加,可立即查看实时画面");
|
||||
});
|
||||
qs("[data-test-connection]", wizard)?.addEventListener("click", (event) => {
|
||||
const button = event.currentTarget;
|
||||
button.disabled = true;
|
||||
button.textContent = "正在测试…";
|
||||
connectionStatus.className = "test-status testing";
|
||||
connectionStatus.innerHTML = "<strong>正在检查账号和画面</strong><span>通常几秒内完成。</span>";
|
||||
window.setTimeout(() => {
|
||||
connectionPassed = true;
|
||||
connectionStatus.className = "test-status success";
|
||||
connectionStatus.innerHTML = "<strong>连接成功</strong><span>画面正常,已自动选择高清(推荐)。</span>";
|
||||
button.disabled = false;
|
||||
button.textContent = "重新测试";
|
||||
render();
|
||||
}, 520);
|
||||
});
|
||||
qs("[data-simulate-failure]", wizard)?.addEventListener("click", () => {
|
||||
connectionPassed = false;
|
||||
connectionStatus.className = "test-status error";
|
||||
connectionStatus.innerHTML = "<strong>账号验证失败</strong><span>请返回上一步检查用户名和密码,然后重新测试。</span>";
|
||||
next.disabled = true;
|
||||
announce("连接失败:请检查账号密码后重试");
|
||||
});
|
||||
}
|
||||
|
||||
const recovery = qs("#recover-camera");
|
||||
if (recovery) {
|
||||
const password = qs("#recover-password", recovery);
|
||||
const error = qs("[data-recovery-error]", recovery);
|
||||
const status = qs("[data-recovery-status]", recovery);
|
||||
const save = qs("[data-recovery-save]", recovery);
|
||||
qs("[data-recovery-test]", recovery)?.addEventListener("click", (event) => {
|
||||
const testButton = event.currentTarget;
|
||||
if (!password.value.trim()) {
|
||||
error.hidden = false;
|
||||
password.setAttribute("aria-invalid", "true");
|
||||
password.focus();
|
||||
return;
|
||||
}
|
||||
error.hidden = true;
|
||||
password.removeAttribute("aria-invalid");
|
||||
testButton.disabled = true;
|
||||
testButton.textContent = "正在测试…";
|
||||
status.className = "test-status testing";
|
||||
status.innerHTML = "<strong>正在重新连接</strong><span>正在检查账号和实时画面。</span>";
|
||||
window.setTimeout(() => {
|
||||
status.className = "test-status success";
|
||||
status.innerHTML = "<strong>连接成功</strong><span>实时画面已恢复,可以安全保存。</span>";
|
||||
testButton.disabled = false;
|
||||
testButton.textContent = "重新测试";
|
||||
save.disabled = false;
|
||||
}, 520);
|
||||
});
|
||||
save.addEventListener("click", () => {
|
||||
qs("[data-close-modal]", recovery)?.click();
|
||||
announce("宿舍东门 02 的账号已更新,实时画面恢复正常");
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -16,6 +16,8 @@ from check_harness import ( # noqa: E402
|
||||
check_claude_code_entry,
|
||||
check_core_documents,
|
||||
check_agent_efficiency_rules,
|
||||
check_go_admin_ui_rules,
|
||||
check_goadmin_baseline,
|
||||
check_task_template,
|
||||
core_mapping_errors,
|
||||
missing_sections,
|
||||
@@ -185,6 +187,63 @@ class AgentRuleTests(unittest.TestCase):
|
||||
check_claude_code_entry(errors)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_go_admin_ui_rules_are_present(self) -> None:
|
||||
errors: list[str] = []
|
||||
check_go_admin_ui_rules(errors)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_go_admin_ui_rules_require_each_scope(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "Sense").mkdir()
|
||||
(root / "Bell").mkdir()
|
||||
(root / "AGENTS.md").write_text("", encoding="utf-8")
|
||||
(root / "Sense" / "AGENTS.md").write_text("", encoding="utf-8")
|
||||
(root / "Bell" / "AGENTS.md").write_text("", encoding="utf-8")
|
||||
errors: list[str] = []
|
||||
check_go_admin_ui_rules(errors, root)
|
||||
self.assertTrue(any(error.startswith("AGENTS.md ") for error in errors))
|
||||
self.assertTrue(any(error.startswith("Sense/AGENTS.md ") for error in errors))
|
||||
self.assertTrue(any(error.startswith("Bell/AGENTS.md ") for error in errors))
|
||||
|
||||
def test_go_admin_ui_rules_report_missing_scope_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
errors: list[str] = []
|
||||
check_go_admin_ui_rules(errors, Path(directory))
|
||||
self.assertIn("缺少 go-admin 规则文件:AGENTS.md", errors)
|
||||
self.assertIn("缺少 go-admin 规则文件:Sense/AGENTS.md", errors)
|
||||
self.assertIn("缺少 go-admin 规则文件:Bell/AGENTS.md", errors)
|
||||
|
||||
def test_goadmin_baseline_is_frozen(self) -> None:
|
||||
errors: list[str] = []
|
||||
check_goadmin_baseline(errors)
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_goadmin_baseline_rejects_unpinned_source(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "goadmin-baseline.json").write_text(
|
||||
'{"authority":"tag","runtimes":{},'
|
||||
'"sources":{"go-admin":{}},"rules":{}}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
errors: list[str] = []
|
||||
check_goadmin_baseline(errors, root)
|
||||
self.assertTrue(any("go-admin.commit" in error for error in errors))
|
||||
self.assertIn(
|
||||
"GoAdmin 技术基线必须以上游 URL 和 commit 为权威标识",
|
||||
errors,
|
||||
)
|
||||
|
||||
def test_goadmin_baseline_reports_missing_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
errors: list[str] = []
|
||||
check_goadmin_baseline(errors, Path(directory))
|
||||
self.assertEqual(
|
||||
errors,
|
||||
["缺少 GoAdmin 技术基线:goadmin-baseline.json"],
|
||||
)
|
||||
|
||||
def test_claude_code_entry_requires_exact_import_line(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
||||
@@ -75,6 +75,30 @@
|
||||
{
|
||||
"page": "Task-1-三项目并行建单与协同工单顺序",
|
||||
"path": "docs/task/1-三项目并行建单与协同工单顺序.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-3-GoAdmin默认模块精简与UI组件复用",
|
||||
"path": "docs/task/3-GoAdmin默认模块精简与UI组件复用.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-5-冻结Sense与Bell-GoAdmin技术基线",
|
||||
"path": "docs/task/5-冻结Sense与Bell-GoAdmin技术基线.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-58-建立explore-main-dev分支治理并重置GoAdmin开发基线",
|
||||
"path": "docs/task/58-建立explore-main-dev分支治理并重置GoAdmin开发基线.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-28-三项目当前MVP交互原型",
|
||||
"path": "docs/task/28-三项目当前MVP交互原型.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-30-Sense原型对齐GoAdmin与Element-Plus",
|
||||
"path": "docs/task/30-Sense原型对齐GoAdmin与Element-Plus.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-32-Sense网管与非技术人员原型",
|
||||
"path": "docs/task/32-Sense网管与非技术人员原型.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user