feat: simplify Sense prototype for operators (#32)

This commit is contained in:
QiuSW
2026-08-12 11:28:09 +08:00
parent 0bdd88ba9c
commit 25c25346bc
3 changed files with 240 additions and 58 deletions
+67 -58
View File
@@ -3,11 +3,12 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="YoVision Sense 当前 MVP 交互原型,直接对齐 go-admin-ui 与 Element Plus">
<title>YoVision Sense · go-admin-ui 对齐原型</title>
<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">
@@ -21,25 +22,21 @@
<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 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>
<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>Brain、Bell 离线不影响 Sense;摄像头凭据只写不读。</span></div>
<div class="side-note"><strong>日常使用提示</strong><span>先处理概况页中的异常项;技术参数只在“高级信息”中展示。</span></div>
</aside>
<div class="main-column main-container">
@@ -48,91 +45,103 @@
<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>
<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" 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>
<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>
<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>设备、媒体与配置的最小可见状态。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="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">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>
<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"><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 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>
<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></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>
<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>
<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" data-reset-device-query><svg aria-hidden="true"><use href="#icon-refresh"></use></svg>重置</button></div>
<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 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>
<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>
<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>
<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>通用筛选与状态组件复用 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>
<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>通用表单与按钮复用 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>
<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-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>
<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>
+51
View File
@@ -0,0 +1,51 @@
.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; }
}
+122
View File
@@ -0,0 +1,122 @@
(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 的账号已更新,实时画面恢复正常");
});
}
})();