feat: align Sense prototype with go-admin-ui (#30)

This commit is contained in:
QiuSW
2026-08-12 10:41:28 +08:00
parent 533225d712
commit 1da77fe0b1
4 changed files with 319 additions and 54 deletions
+10 -1
View File
@@ -14,7 +14,7 @@ python -m http.server 4173 --directory prototypes
## 原型范围
- `sense/index.html`:设备接入、ONVIF/RTSP、MediaMTX、实时监看和区域配置。
- `sense/index.html`:设备接入、ONVIF/RTSP、MediaMTX、实时监看和区域配置;直接映射 go-admin-ui 与 Element Plus 的通用管理端组件。
- `brain/index.html`:合成输入到项目内匿名事件的内部流水线;Brain 无客户管理 UI。
- `bell/index.html`:合成事件、不可变 Event、规则/Alert、ack/close 和审计时间线。
- `assets/`:三套原型复用的样式和交互脚本。
@@ -28,6 +28,15 @@ python -m http.server 4173 --directory prototypes
- Bell 可注入合成事件,并演示 Alert 的 ack 和 close。
- 所有弹窗支持 `Esc` 关闭,交互元素支持键盘焦点。
## Sense 的 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` 权限按钮模式。
- Sense 专用组件:视频舞台 `SenseVideoStage` 与区域画布 `SenseZoneCanvas`;正式实现前先确认 go-admin-ui 内没有等价组件。
- 原型仍是无构建依赖的 HTML/CSS/JavaScript,并未嵌入或复制 go-admin-ui / Element Plus 源码;这些名称用于约束正式 Vue 实现的组件落点。
## 事实来源边界
- 产品范围以 Gitea Wiki 的 Product Requirements、Product Roadmap 和对应工单为准。
+181
View File
@@ -0,0 +1,181 @@
/*
* Sense 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.
*/
.sense-go-admin {
--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;
}
.sense-go-admin .go-admin-shell { grid-template-columns: var(--ga-sidebar-width) minmax(0, 1fr); }
.sense-go-admin .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; }
.sense-go-admin .sidebar-logo { height: 64px; min-height: 64px; padding: 0 16px; gap: 12px; color: #fff; text-decoration: none; background: #001529; }
.sense-go-admin .brand-mark { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 5px; background: var(--ga-primary); }
.sense-go-admin .brand-copy { min-width: 0; }
.sense-go-admin .brand-name { white-space: nowrap; }
.sense-go-admin .brand-subtitle { color: rgba(255,255,255,.56); white-space: nowrap; }
.sense-go-admin .el-menu { gap: 0; padding: 8px 0; }
.sense-go-admin .menu-group,
.sense-go-admin .el-menu-item { min-height: 50px; height: 50px; padding: 0 18px; border-radius: 0; gap: 14px; color: rgba(255,255,255,.82); }
.sense-go-admin .menu-group { display: flex; align-items: center; font-size: 14px; font-weight: 500; }
.sense-go-admin .menu-group .menu-arrow { width: 14px; margin-left: auto; transform: rotate(90deg); }
.sense-go-admin .el-menu-item:hover { color: #fff; background: var(--ga-sidebar-hover); }
.sense-go-admin .el-menu-item.active { color: var(--ga-primary); background: var(--ga-sidebar-hover); box-shadow: inset 3px 0 0 var(--ga-primary); }
.sense-go-admin .side-note { margin: 12px; padding: 11px 12px; border-radius: 4px; color: rgba(255,255,255,.72); }
.sense-go-admin .main-container { min-width: 0; background: var(--ga-app-bg); }
.sense-go-admin .fixed-header { position: sticky; top: 0; z-index: 30; }
.sense-go-admin .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; }
.sense-go-admin .navbar::after { content: ""; position: absolute; right: 0; bottom: 0; left: 0; height: 2px; background: linear-gradient(90deg, #1677ff, #40a9ff); opacity: .7; }
.sense-go-admin .navbar-action,
.sense-go-admin .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; }
.sense-go-admin .navbar-action:hover,
.sense-go-admin .avatar-button:hover { color: var(--ga-primary); background: rgba(22,119,255,.06); }
.sense-go-admin .navbar-action svg { width: 19px; height: 19px; }
.sense-go-admin .mobile-menu { display: none; }
.sense-go-admin .breadcrumb { display: flex; align-items: center; gap: 9px; color: #97a8be; font-size: 13px; white-space: nowrap; }
.sense-go-admin .breadcrumb a { color: #606266; text-decoration: none; }
.sense-go-admin .breadcrumb strong { color: #303133; font-weight: 500; }
.sense-go-admin .avatar-button { min-width: auto; }
.sense-go-admin .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); }
.sense-go-admin .prototype-pill { min-height: 25px; border-radius: 4px; }
.sense-go-admin .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); }
.sense-go-admin .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; }
.sense-go-admin .tags-view-item:hover { color: var(--ga-primary); background: #e6f4ff; }
.sense-go-admin .tags-view-item.active { color: var(--ga-primary); border-color: #91caff; border-bottom-color: #fff; background: #fff; font-weight: 500; }
.sense-go-admin .tags-view-item span { margin-left: 4px; color: #909399; }
.sense-go-admin .app-main { width: 100%; min-height: calc(100vh - 90px); padding: 16px; background: var(--ga-app-bg); }
.sense-go-admin .compact-header { margin-bottom: 14px; }
.sense-go-admin .compact-header h1 { font-size: 20px; font-weight: 500; letter-spacing: 0; }
.sense-go-admin .compact-header p { font-size: 13px; }
.sense-go-admin .el-card { border-color: var(--ga-border); border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,.03); }
.sense-go-admin .card-header { min-height: 48px; padding: 10px 14px; }
.sense-go-admin .card-body { padding: 14px; }
.sense-go-admin .metric { padding: 14px; }
.sense-go-admin .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; }
.sense-go-admin .el-button svg { width: 15px; height: 15px; }
.sense-go-admin .el-button:hover { color: var(--ga-primary); border-color: #a0cfff; background: #ecf5ff; }
.sense-go-admin .el-button-primary { color: #fff; border-color: var(--ga-primary); background: var(--ga-primary); }
.sense-go-admin .el-button-primary:hover { color: #fff; border-color: var(--ga-primary-hover); background: var(--ga-primary-hover); }
.sense-go-admin .el-button-link { min-height: 30px; height: 30px; padding: 3px 4px; border: 0; color: var(--ga-primary); background: transparent; }
.sense-go-admin .el-button:disabled { color: #a8abb2; border-color: #e4e7ed; background: #f5f7fa; }
.sense-go-admin .el-form { color: var(--ga-text); }
.sense-go-admin .el-form-inline { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 18px; padding-bottom: 14px; border-bottom: 1px solid #ebeef5; }
.sense-go-admin .form-item { min-width: 180px; display: grid; grid-template-columns: auto minmax(150px, 220px); align-items: center; gap: 8px; }
.sense-go-admin .form-item label { color: #606266; font-size: 13px; white-space: nowrap; }
.sense-go-admin .form-actions { display: flex; gap: 8px; }
.sense-go-admin .el-input,
.sense-go-admin .el-select { min-height: 32px; height: 32px; padding: 4px 10px; border-color: #dcdfe6; border-radius: 4px; color: #606266; font-size: 13px; }
.sense-go-admin .el-input:focus,
.sense-go-admin .el-select:focus { border-color: var(--ga-primary); outline: 2px solid rgba(22,119,255,.16); }
.sense-go-admin .vertical-form { display: grid; gap: 16px; }
.sense-go-admin .vertical-form .form-item { grid-template-columns: 92px minmax(0,1fr); }
.sense-go-admin .vertical-form .help { margin: -8px 0 0 100px; }
.sense-go-admin .form-footer { display: flex; justify-content: flex-end; }
.sense-go-admin .operation-bar { min-height: 48px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sense-go-admin .operation-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.sense-go-admin .permission-hint { display: inline-flex; align-items: center; gap: 5px; color: #909399; font-size: 12px; }
.sense-go-admin .permission-hint svg { width: 14px; height: 14px; }
.sense-go-admin .el-table { min-width: 760px; border: 1px solid #ebeef5; }
.sense-go-admin .el-table th,
.sense-go-admin .el-table td { padding: 9px 10px; border-right: 1px solid #ebeef5; border-bottom-color: #ebeef5; }
.sense-go-admin .el-table th { background: #f5f7fa; color: #909399; font-weight: 500; }
.sense-go-admin .el-table tbody tr:nth-child(even) { background: #fafafa; }
.sense-go-admin .el-table tbody tr:hover { background: #f5f7fa; }
.sense-go-admin .checkbox-faux { width: 14px; height: 14px; display: inline-block; border: 1px solid #dcdfe6; border-radius: 2px; background: #fff; }
.sense-go-admin .action-divider { height: 14px; margin: 0 3px; display: inline-block; border-left: 1px solid #dcdfe6; vertical-align: middle; }
.sense-go-admin .el-tag { min-height: 24px; padding: 2px 8px; border: 1px solid currentColor; border-radius: 4px; font-weight: 400; }
.sense-go-admin .el-tag::before { display: none; }
.sense-go-admin .pagination-container { min-height: 48px; display: flex; align-items: center; justify-content: flex-end; gap: 6px; color: #606266; font-size: 12px; }
.sense-go-admin .pagination-container button { min-width: 30px; height: 28px; border: 0; border-radius: 3px; background: #f4f4f5; color: #606266; }
.sense-go-admin .pagination-container button.active { color: #fff; background: var(--ga-primary); }
.sense-go-admin .pagination-container button:disabled { color: #c0c4cc; cursor: not-allowed; }
.sense-go-admin .pagination-size { height: 28px; border: 1px solid #dcdfe6; border-radius: 4px; color: #606266; }
.sense-go-admin .pagination-total { margin-right: 4px; }
.sense-go-admin .description-list { display: grid; }
.sense-go-admin .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; }
.sense-go-admin .description-row:last-child { border-bottom: 0; }
.sense-go-admin .description-row > span:first-child { color: #909399; font-size: 12px; }
.sense-go-admin .description-row strong { font-size: 13px; font-weight: 500; }
.sense-go-admin .sense-video-stage,
.sense-go-admin .sense-zone-canvas { border-radius: 4px; }
.sense-go-admin .el-overlay { background: rgba(0,0,0,.5); }
.sense-go-admin .el-dialog { width: min(600px,100%); border-radius: 4px; }
.sense-go-admin .el-dialog-header,
.sense-go-admin .el-dialog-footer { padding: 16px 20px; }
.sense-go-admin .el-dialog-body { padding: 20px; }
.sense-go-admin .dialog-close { min-height: 32px; height: 32px; color: #909399; }
.sense-go-admin .go-admin-shell.compact { grid-template-columns: 54px minmax(0,1fr); }
.sense-go-admin .go-admin-shell.compact .sidebar-container { width: 54px; }
.sense-go-admin .go-admin-shell.compact .brand-copy,
.sense-go-admin .go-admin-shell.compact .el-menu-item span,
.sense-go-admin .go-admin-shell.compact .menu-group span,
.sense-go-admin .go-admin-shell.compact .menu-arrow,
.sense-go-admin .go-admin-shell.compact .side-note { display: none; }
.sense-go-admin .go-admin-shell.compact .sidebar-logo,
.sense-go-admin .go-admin-shell.compact .menu-group,
.sense-go-admin .go-admin-shell.compact .el-menu-item { padding-right: 0; padding-left: 0; justify-content: center; }
@media (max-width: 900px) {
.sense-go-admin .go-admin-shell,
.sense-go-admin .go-admin-shell.compact { display: block; }
.sense-go-admin .sidebar-container,
.sense-go-admin .go-admin-shell.compact .sidebar-container { position: fixed; top: 0; left: 0; z-index: 40; width: min(300px,88vw); height: 100vh; transform: translateX(-102%); }
.sense-go-admin .sidebar-container.open,
.sense-go-admin .go-admin-shell.compact .sidebar-container.open { transform: translateX(0); }
.sense-go-admin .go-admin-shell.compact .brand-copy,
.sense-go-admin .go-admin-shell.compact .el-menu-item span,
.sense-go-admin .go-admin-shell.compact .menu-group span,
.sense-go-admin .go-admin-shell.compact .menu-arrow,
.sense-go-admin .go-admin-shell.compact .side-note { display: initial; }
.sense-go-admin .go-admin-shell.compact .sidebar-logo,
.sense-go-admin .go-admin-shell.compact .menu-group,
.sense-go-admin .go-admin-shell.compact .el-menu-item { padding: 0 18px; justify-content: flex-start; }
.sense-go-admin .desktop-collapse { display: none; }
.sense-go-admin .mobile-menu { display: inline-flex; }
.sense-go-admin .breadcrumb { max-width: 46vw; overflow: hidden; }
.sense-go-admin .breadcrumb a,
.sense-go-admin .breadcrumb span { display: none; }
.sense-go-admin .app-main { padding: 12px; }
.sense-go-admin .tags-view-container { height: 48px; padding-top: 8px; }
.sense-go-admin .tags-view-item { height: 40px; }
.sense-go-admin .el-button { min-height: 44px; height: 44px; }
.sense-go-admin .el-input,
.sense-go-admin .el-select { min-height: 44px; height: 44px; font-size: 16px; }
.sense-go-admin .el-form-inline { align-items: stretch; }
.sense-go-admin .el-form-inline .form-item { width: 100%; grid-template-columns: 82px minmax(0,1fr); }
.sense-go-admin .el-form-inline .form-actions { margin-left: 90px; }
.sense-go-admin .operation-bar { align-items: flex-start; flex-direction: column; padding: 9px 0; }
.sense-go-admin .pagination-container button,
.sense-go-admin .pagination-size { min-width: 40px; height: 40px; }
}
@media (max-width: 430px) {
.sense-go-admin .desktop-only,
.sense-go-admin .prototype-pill { display: none; }
.sense-go-admin .topbar-actions { gap: 0; }
.sense-go-admin .navbar-action,
.sense-go-admin .avatar-button { min-width: 42px; padding: 0 8px; }
.sense-go-admin .vertical-form .form-item { grid-template-columns: 1fr; gap: 5px; }
.sense-go-admin .vertical-form .help { margin-left: 0; }
.sense-go-admin .pagination-container { justify-content: flex-start; overflow-x: auto; }
}
+85 -53
View File
@@ -3,11 +3,13 @@
<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 · 当前 MVP 原型</title>
<meta name="description" content="YoVision Sense 当前 MVP 交互原型,直接对齐 go-admin-ui 与 Element Plus">
<title>YoVision Sense · go-admin-ui 对齐原型</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="../assets/styles.css">
<link rel="stylesheet" href="./go-admin-ui.css">
</head>
<body>
<body class="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>
@@ -16,91 +18,121 @@
<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-activity" viewBox="0 0 24 24"><path d="M3 12h4l2-7 4 14 2-7h6"/></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-lock" viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></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">
<aside class="sidebar" data-sidebar aria-label="Sense 主导航">
<a class="brand" href="../index.html" style="text-decoration:none">
<span class="brand-mark">S</span><span><span class="brand-name">YoVision Sense</span><span class="brand-subtitle">视频感知管理面</span></span>
<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>
<div class="side-label">MVP 工作区</div>
<nav class="side-nav">
<button class="active" data-view-target="overview" data-title="运行总览"><svg aria-hidden="true"><use href="#icon-grid"></use></svg>运行总览</button>
<button data-view-target="devices" data-title="设备接入"><svg aria-hidden="true"><use href="#icon-camera"></use></svg>设备接入</button>
<button data-view-target="live" data-title="实时监看"><svg aria-hidden="true"><use href="#icon-monitor"></use></svg>实时监看</button>
<button data-view-target="zones" data-title="区域配置"><svg aria-hidden="true"><use href="#icon-zone"></use></svg>区域配置</button>
<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>当前页面不需要 Brain 或 Bell 在线;摄像头凭据只写不读,演示中不展示真实密码。</div>
<div class="side-note"><strong>独立产品边界</strong><span>Brain、Bell 离线不影响 Sense;摄像头凭据只写不读。</span></div>
</aside>
<div class="main-column">
<header class="topbar">
<div class="topbar-left">
<button class="btn btn-icon mobile-menu" type="button" data-mobile-menu aria-label="打开主导航" aria-expanded="false"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button>
<div><div class="page-eyebrow">青藤实验站点 · Sense</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>
<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" tabindex="-1">
<main id="main-content" class="content app-main" tabindex="-1">
<section class="view active" data-view="overview">
<div class="page-header"><div><h1>运行总览</h1><p>设备、媒体与配置的最小可见状态。Brain、Bell 当前均未连接,不影响 Sense 独立运行。</p></div><div class="page-actions"><button class="btn btn-primary" data-open-modal="add-device"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>添加设备</button></div></div>
<div class="page-header compact-header"><div><h1>运行总览</h1><p>设备、媒体与配置的最小可见状态。Brain、Bell 当前均未连接,不影响 Sense 独立运行。</p></div><div class="page-actions"><button class="btn el-button el-button-primary" data-open-modal="add-device" title="权限:sense:device:add"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>添加设备</button></div></div>
<div class="metric-grid">
<article class="metric"><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"><div class="metric-label">MediaMTX 路径</div><div class="metric-value">5/5</div><div class="metric-foot"><strong>已收敛</strong><span>· 12 秒前</span></div></article>
<article class="metric"><div class="metric-label">区域版本</div><div class="metric-value">8</div><div class="metric-foot"><strong>7 生效</strong><span>· 1 草稿</span></div></article>
<article class="metric"><div class="metric-label">需要处理</div><div class="metric-value">2</div><div class="metric-foot"><span class="status status-warning">认证 / 校时</span></div></article>
<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">MediaMTX 路径</div><div class="metric-value">5/5</div><div class="metric-foot"><strong>已收敛</strong><span>· 12 秒前</span></div></article>
<article class="metric el-card"><div class="metric-label">区域版本</div><div class="metric-value">8</div><div class="metric-foot"><strong>7 生效</strong><span>· 1 草稿</span></div></article>
<article class="metric el-card"><div class="metric-label">需要处理</div><div class="metric-value">2</div><div class="metric-foot"><span class="status status-warning el-tag">认证 / 校时</span></div></article>
</div>
<div class="grid grid-2">
<article class="card"><header class="card-header"><div><h2>设备与媒体状态</h2><p class="card-subtitle">期望态与实际态的快速对照</p></div><button class="btn btn-quiet" data-view-target="devices">查看设备</button></header><div class="card-body">
<article class="card el-card"><header class="card-header"><div><h2>设备与媒体状态</h2><p class="card-subtitle">期望态与实际态的快速对照</p></div><button class="btn btn-quiet el-button el-button-link" data-view-target="devices">查看设备</button></header><div class="card-body">
<div class="activity-list">
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">北围墙 01</div><div class="activity-meta">主码流 1080p · MediaMTX reader 1</div></div><span class="status status-success">在线</span></div>
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">宿舍东门 02</div><div class="activity-meta">ONVIF 认证失败 · 凭据未回显</div></div><span class="status status-danger">需处理</span></div>
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">食堂后门 01</div><div class="activity-meta">已发现 · 等待选择 Profile</div></div><span class="status status-warning">待激活</span></div>
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">北围墙 01</div><div class="activity-meta">主码流 1080p · MediaMTX reader 1</div></div><span class="status status-success el-tag">在线</span></div>
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">宿舍东门 02</div><div class="activity-meta">ONVIF 认证失败 · 凭据未回显</div></div><span class="status status-danger el-tag">需处理</span></div>
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">食堂后门 01</div><div class="activity-meta">已发现 · 等待选择 Profile</div></div><span class="status status-warning el-tag">待激活</span></div>
</div>
</div></article>
<article class="card"><header class="card-header"><div><h2>独立运行状态</h2><p class="card-subtitle">跨项目未连接不构成故障</p></div></header><div class="card-body stack">
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-monitor"></use></svg></div><div><div class="activity-title">Sense Core</div><div class="activity-meta">API、数据库、审计可用</div></div><span class="status status-success">正常</span></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">Brain 适配器</div><div class="activity-meta">MVP #8 不要求连接</div></div><span class="status status-neutral">未配置</span></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">Bell Connector</div><div class="activity-meta">后续协同工单实现</div></div><span class="status status-neutral">未安装</span></div>
<article class="card el-card"><header class="card-header"><div><h2>独立运行状态</h2><p class="card-subtitle">跨项目未连接不构成故障</p></div></header><div class="card-body stack">
<div class="description-row"><span>Sense Core</span><strong>API、数据库、审计可用</strong><span class="status status-success el-tag">正常</span></div>
<div class="description-row"><span>Brain 适配器</span><strong>MVP #8 不要求连接</strong><span class="status status-neutral el-tag">未配置</span></div>
<div class="description-row"><span>Bell Connector</span><strong>后续协同工单实现</strong><span class="status status-neutral el-tag">未安装</span></div>
</div></article>
</div>
</section>
<section class="view" data-view="devices" hidden>
<div class="page-header"><div><h1>设备接入</h1><p>从发现、验证到激活的单设备闭环;凭据更新后只显示脱敏结果。</p></div><div class="page-actions"><button class="btn btn-primary" data-open-modal="add-device"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>添加设备</button></div></div>
<article class="card"><div class="card-body">
<div class="toolbar"><div class="field grow"><label for="device-filter">搜索设备</label><div class="inline"><svg aria-hidden="true"><use href="#icon-search"></use></svg><input id="device-filter" class="input" data-filter-input="#device-table" placeholder="名称、地址或状态"></div></div><div class="field"><label for="device-status">状态</label><select id="device-status" class="input"><option>全部状态</option><option>在线</option><option>需处理</option><option>待激活</option></select></div></div>
<div class="table-wrap"><table id="device-table"><thead><tr><th>设备</th><th>管理地址</th><th>接入状态</th><th>选定 Profile</th><th>操作</th></tr></thead><tbody>
<tr><td><span class="table-title">北围墙 01</span><span class="table-subtitle">video · ONVIF</span></td><td>192.168.10.21</td><td><span class="status status-success">在线</span></td><td>MainStream · 1080p</td><td><button class="btn btn-quiet" data-toast="已打开北围墙 01 详情">查看</button></td></tr>
<tr><td><span class="table-title">宿舍东门 02</span><span class="table-subtitle">video · ONVIF</span></td><td>192.168.10.32</td><td><span class="status status-danger">认证失败</span></td><td>SubStream · 720p</td><td><button class="btn btn-quiet" data-toast="凭据更新窗口仅允许写入,不会回显旧密码">更新凭据</button></td></tr>
<tr><td><span class="table-title">食堂后门 01</span><span class="table-subtitle">video · 手工添加</span></td><td>192.168.10.45</td><td><span class="status status-warning">待激活</span></td><td>尚未选择</td><td><button class="btn btn-quiet" data-toast="已开始重新探测 Profile">探测</button></td></tr>
<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-device-query>
<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" data-reset-device-query><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 el-button-primary" data-open-modal="add-device" title="权限:sense:device:add"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>新增</button><button class="btn el-button" data-toast="已开始发现局域网 ONVIF 设备" title="权限:sense:device:discover"><svg aria-hidden="true"><use href="#icon-search"></use></svg>发现设备</button><button class="btn el-button" disabled title="权限:sense:device:disable;请先选择设备"><svg aria-hidden="true"><use href="#icon-lock"></use></svg>批量停用</button></div>
<span class="permission-hint"><svg aria-hidden="true"><use href="#icon-lock"></use></svg>操作按钮受 RBAC 权限控制</span>
</div>
<div class="table-wrap"><table id="device-table" class="el-table"><thead><tr><th><span class="checkbox-faux" aria-hidden="true"></span><span class="sr-only">选择</span></th><th>设备</th><th>管理地址</th><th>接入状态</th><th>选定 Profile</th><th>操作</th></tr></thead><tbody>
<tr><td><span class="checkbox-faux" aria-hidden="true"></span></td><td><span class="table-title">北围墙 01</span><span class="table-subtitle">video · ONVIF</span></td><td>192.168.10.21</td><td><span class="status status-success el-tag">在线</span></td><td>MainStream · 1080p</td><td><button class="btn btn-quiet el-button-link" data-toast="已打开北围墙 01 详情">查看</button><span class="action-divider"></span><button class="btn btn-quiet el-button-link" data-toast="已打开编辑窗口">修改</button></td></tr>
<tr><td><span class="checkbox-faux" aria-hidden="true"></span></td><td><span class="table-title">宿舍东门 02</span><span class="table-subtitle">video · ONVIF</span></td><td>192.168.10.32</td><td><span class="status status-danger el-tag">认证失败</span></td><td>SubStream · 720p</td><td><button class="btn btn-quiet el-button-link" data-toast="凭据更新窗口只允许写入,不会回显旧密码">更新凭据</button></td></tr>
<tr><td><span class="checkbox-faux" aria-hidden="true"></span></td><td><span class="table-title">食堂后门 01</span><span class="table-subtitle">video · 手工添加</span></td><td>192.168.10.45</td><td><span class="status status-warning el-tag">待激活</span></td><td>尚未选择</td><td><button class="btn btn-quiet el-button-link" data-toast="已开始重新探测 Profile">探测</button></td></tr>
</tbody></table></div>
<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"><div><h1>实时监看</h1><p>单路视频优先;明确展示加载、失败、重连和当前 Profile。</p></div><div class="page-actions"><select class="input" aria-label="选择摄像头"><option>北围墙 01</option><option>宿舍东门 02</option></select><button class="btn" data-toast="已发起重连,当前画面保持显示">重新连接</button></div></div>
<div class="grid grid-2"><article class="card"><div class="card-body"><div class="video-stage" role="img" aria-label="北围墙摄像头的模拟实时画面,画面中显示已配置的危险区域"><div class="camera-caption"><span class="status status-success">实时</span>北围墙 01 · MainStream</div><div class="zone-shape"></div><span class="zone-label">危险区域 A</span><span class="video-time">2026-08-12 10:18:24 CST</span></div></div></article><article class="card"><header class="card-header"><h2>播放状态</h2></header><div class="card-body stack"><div><span class="metric-label">协议</span><div class="table-title">WebRTC / MediaMTX</div></div><div><span class="metric-label">源 Profile</span><div class="table-title">MainStream · 1920×1080</div></div><div><span class="metric-label">连接质量</span><div class="table-title">1.8 Mbps · 延迟 340 ms</div></div><div><span class="metric-label">最近重连</span><div class="table-title">无 · 已稳定 04:18:32</div></div></div></article></div>
<div class="page-header compact-header"><div><h1>实时监看</h1><p>通用筛选与状态组件复用 Element Plus;视频舞台是 Sense 专用业务组件。</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"><div><h2>实时画面</h2><p class="card-subtitle">SenseVideoStage · 业务专用组件</p></div><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 · MainStream</div><div class="zone-shape"></div><span class="zone-label">危险区域 A</span><span class="video-time">2026-08-12 10:18:24 CST</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>WebRTC / MediaMTX</strong></div><div class="description-row"><span>源 Profile</span><strong>MainStream · 1920×1080</strong></div><div class="description-row"><span>连接质量</span><strong>1.8 Mbps · 延迟 340 ms</strong></div><div class="description-row"><span>最近重连</span><strong>无 · 已稳定 04:18:32</strong></div></div></article></div>
</section>
<section class="view" data-view="zones" hidden>
<div class="page-header"><div><h1>区域配置</h1><p>区域与方向线绑定设备和 Profile;分辨率变化时必须显式重新校准。</p></div><div class="page-actions"><button class="btn" data-toast="方向已反转,草稿尚未发布">反转方向</button><button class="btn btn-primary" data-toast="区域草稿已保存为 v8,尚未发布">保存草稿</button></div></div>
<div class="grid grid-2"><article class="card"><div class="card-body"><div class="video-stage" role="img" aria-label="区域编辑画布,包含五边形危险区域"><div class="camera-caption">北围墙 01 · MainStream · 1920×1080</div><div class="zone-shape"></div><span class="zone-label">危险区域 A · 草稿</span></div></div></article><article class="card"><header class="card-header"><h2>区域属性</h2><span class="status status-warning">草稿 v8</span></header><div class="card-body stack"><div class="field"><label for="zone-name">区域名称</label><input id="zone-name" class="input" value="危险区域 A"></div><div class="field"><label for="zone-rule">区域类型</label><select id="zone-rule" class="input"><option>危险区域</option><option>方向警戒线</option><option>聚集区域</option></select></div><div class="field"><label for="zone-profile">绑定 Profile</label><input id="zone-profile" class="input" value="MainStream · 1920×1080" readonly></div><p class="help">坐标按当前 Profile 归一化保存。切换分辨率后,此版本将进入“需重新校准”。</p><button class="btn btn-primary" data-toast="区域 v8 已发布,v7 保留用于审计">发布区域版本</button></div></article></div>
<div class="page-header compact-header"><div><h1>区域配置</h1><p>通用表单与按钮复用 Element Plus;区域绘制画布是 Sense 专用业务组件。</p></div><div class="page-actions"><button class="btn el-button" data-toast="方向已反转,草稿尚未发布">反转方向</button><button class="btn el-button el-button-primary" data-toast="区域草稿已保存为 v8,尚未发布" title="权限:sense:zone:edit">保存草稿</button></div></div>
<div class="grid grid-2"><article class="card el-card"><header class="card-header"><div><h2>区域画布</h2><p class="card-subtitle">SenseZoneCanvas · 业务专用组件</p></div></header><div class="card-body"><div class="video-stage sense-zone-canvas" role="img" aria-label="区域编辑画布,包含五边形危险区域"><div class="camera-caption">北围墙 01 · MainStream · 1920×1080</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">草稿 v8</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-profile">绑定 Profile</label><input id="zone-profile" class="input el-input" value="MainStream · 1920×1080" readonly></div><p class="help">坐标按当前 Profile 归一化保存。切换分辨率后,此版本将进入“需重新校准”。</p><div class="form-footer"><button type="button" class="btn el-button el-button-primary" data-toast="区域 v8 已发布,v7 保留用于审计" title="权限:sense:zone:publish">发布区域版本</button></div></form></div></article></div>
</section>
</main>
</div>
</div>
<div class="modal-backdrop" id="add-device" aria-hidden="true">
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="add-device-title">
<header class="modal-header"><h2 id="add-device-title">添加视频设备</h2><button class="btn btn-icon btn-quiet" data-close-modal aria-label="关闭添加设备窗口">×</button></header>
<div class="modal-body stack"><div class="field"><label for="device-name">设备名称</label><input id="device-name" class="input" value="教学楼西门 01"></div><div class="field"><label for="device-address">管理地址</label><input id="device-address" class="input" inputmode="url" value="192.168.10.88"></div><div class="field"><label for="device-user">ONVIF 用户名</label><input id="device-user" class="input" autocomplete="off" value="operator"></div><div class="field"><label for="device-password">密码</label><input id="device-password" class="input" type="password" autocomplete="new-password" placeholder="只写不读,保存后不会回显"><span class="help">原型不会保存输入内容;正式产品中密码不得进入日志或 URL。</span></div></div>
<footer class="modal-footer"><button class="btn" data-close-modal>取消</button><button class="btn btn-primary" data-action="add-device">保存为待激活</button></footer>
<div class="modal-backdrop el-overlay" id="add-device" aria-hidden="true">
<section class="modal el-dialog" role="dialog" aria-modal="true" aria-labelledby="add-device-title">
<header class="modal-header el-dialog-header"><h2 id="add-device-title">添加视频设备</h2><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"><div class="form-item"><label for="device-name">设备名称</label><input id="device-name" class="input el-input" value="教学楼西门 01"></div><div class="form-item"><label for="device-address">管理地址</label><input id="device-address" class="input el-input" inputmode="url" value="192.168.10.88"></div><div class="form-item"><label for="device-user">ONVIF 用户名</label><input id="device-user" class="input el-input" autocomplete="off" value="operator"></div><div class="form-item"><label for="device-password">密码</label><input id="device-password" class="input el-input" type="password" autocomplete="new-password" placeholder="只写不读,保存后不会回显"><span class="help">原型不会保存输入内容;正式产品中密码不得进入日志或 URL。</span></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" data-action="add-device">确定</button></footer>
</section>
</div>
<div class="toast-region" data-toast-region aria-live="polite"></div>
<script src="../assets/app.js"></script>
<script src="./sense.js"></script>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
(function () {
"use strict";
const shell = document.querySelector("[data-go-admin-shell]");
const collapse = document.querySelector("[data-collapse-sidebar]");
const queryForm = document.querySelector("[data-device-query]");
const queryInput = document.querySelector("#device-filter");
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 ? "展开主导航" : "折叠主导航");
});
}
if (queryForm) {
queryForm.addEventListener("submit", (event) => {
event.preventDefault();
if (queryInput) queryInput.dispatchEvent(new Event("input", { bubbles: true }));
});
queryForm.addEventListener("reset", () => {
window.setTimeout(() => {
if (queryInput) queryInput.dispatchEvent(new Event("input", { bubbles: true }));
}, 0);
});
}
syncTags("overview");
})();