Files
yovision/docs/design/sense/index.html
T
QiuSW e0b0bfafeb
Harness governance / validate (push) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
docs(design): close Sense second-review gaps
2026-08-04 11:08:41 +08:00

162 lines
56 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>YoVision Sense · 接入运维工作台原型</title>
<style>
:root{color-scheme:dark;--bg:#07101d;--surface:#0d1928;--surface2:#122235;--line:#294057;--text:#eff7ff;--muted:#9fb2c6;--primary:#55d6be;--primary2:#113e3b;--info:#69b9ff;--warn:#ffc462;--danger:#ff8290;--success:#67d69b;--shadow:0 18px 55px rgba(0,0,0,.32);font-family:Inter,"Segoe UI","Microsoft YaHei",sans-serif}
*{box-sizing:border-box}html{background:var(--bg)}body{margin:0;background:radial-gradient(circle at 70% 0,#112941 0,transparent 38%),var(--bg);color:var(--text);font-size:14px;min-height:100vh}button,input,select,textarea{font:inherit}button,a,select,input{touch-action:manipulation}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:3px solid #9ce7ff;outline-offset:2px}[hidden]{display:none!important}.prototype{position:sticky;top:0;z-index:100;height:28px;display:grid;place-items:center;background:#ffd66b;color:#241d00;font-weight:800;font-size:12px;letter-spacing:.04em}.shell{min-height:calc(100vh - 28px);display:grid;grid-template-columns:240px minmax(0,1fr)}.sidebar{position:sticky;top:28px;height:calc(100vh - 28px);padding:18px 14px;border-right:1px solid var(--line);background:rgba(8,18,31,.92);backdrop-filter:blur(14px)}.brand{display:flex;align-items:center;gap:10px;padding:4px 8px 22px}.brand-mark{width:36px;height:36px;border-radius:11px;display:grid;place-items:center;background:linear-gradient(135deg,var(--primary),#5c9dff);color:#06151b;font-weight:900}.brand strong{display:block;font-size:17px}.brand small{color:var(--muted)}.nav{display:grid;gap:6px}.nav button,.mobile-nav button{border:0;cursor:pointer}.nav button{min-height:46px;padding:0 12px;border-radius:10px;background:transparent;color:var(--muted);display:flex;align-items:center;gap:10px;text-align:left}.nav button:hover,.nav button.active{background:var(--primary2);color:var(--text)}.nav button.active{box-shadow:inset 3px 0 var(--primary)}.count{margin-left:auto;min-width:22px;padding:2px 7px;border-radius:999px;background:#263d50;color:var(--text);font-size:12px;text-align:center}.side-note{position:absolute;left:14px;right:14px;bottom:18px;padding:12px;border:1px solid var(--line);border-radius:11px;color:var(--muted);font-size:12px;line-height:1.55}.main{min-width:0}.topbar{min-height:70px;padding:12px 24px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:16px;background:rgba(8,18,31,.72);backdrop-filter:blur(10px)}.context,.top-actions,.actions,.inline{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.context-block{display:grid;gap:3px}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}.context strong{font-size:15px}.select,.input,.textarea{min-height:42px;border:1px solid var(--line);border-radius:9px;background:#091726;color:var(--text);padding:0 11px}.textarea{padding:10px;min-height:82px;resize:vertical}.select:disabled,.input:disabled,.btn:disabled{opacity:.5;cursor:not-allowed}.content{padding:24px;max-width:1540px;margin:auto}.view{display:none}.view.active{display:block}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:20px}.page-head h1{font-size:25px;margin:0 0 6px}.page-head p,.muted{color:var(--muted);margin:0}.btn{min-height:42px;padding:0 14px;border:1px solid var(--line);border-radius:9px;background:#15283b;color:var(--text);cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:7px}.btn:hover{border-color:#5b809f}.btn-primary{background:var(--primary);border-color:var(--primary);color:#06221e;font-weight:800}.btn-danger{background:#51232d;border-color:#86414d;color:#fff}.btn-quiet{background:transparent}.btn-sm{min-height:36px;padding:0 10px;font-size:12px}.icon-btn{width:42px;padding:0;font-size:20px}.grid{display:grid;gap:14px}.stats{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:14px}.card{border:1px solid var(--line);border-radius:14px;background:linear-gradient(150deg,rgba(18,34,53,.96),rgba(10,24,39,.96));box-shadow:var(--shadow)}button.card{color:inherit;text-align:left;cursor:pointer}.stat{padding:17px}.stat b{display:block;font-size:27px;margin:8px 0 4px}.stat small{color:var(--muted)}.cols{grid-template-columns:minmax(0,1.55fr) minmax(300px,.8fr)}.card-head{padding:15px 17px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:12px;align-items:center}.card-head h2,.card-head h3{margin:0;font-size:16px}.card-body{padding:17px}.banner{margin-bottom:14px;padding:13px 15px;border:1px solid #92702f;border-radius:11px;background:#302815;color:#ffe9b0;display:flex;justify-content:space-between;gap:12px;align-items:center}.banner.danger{border-color:#84424d;background:#351b23;color:#ffd9de}.badge{display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border-radius:999px;border:1px solid #3b5369;color:#cbd9e6;font-size:12px;white-space:nowrap}.badge.success{color:#aef2cb;border-color:#36785b;background:#153b2d}.badge.warn{color:#ffe0a1;border-color:#8c682d;background:#372b17}.badge.danger{color:#ffc6ce;border-color:#8b4651;background:#3d1d25}.badge.info{color:#cce9ff;border-color:#3a6e98;background:#15314a}.table-wrap{overflow:auto}.table{width:100%;border-collapse:collapse;min-width:760px}.table th,.table td{padding:12px 14px;border-bottom:1px solid var(--line);text-align:left;vertical-align:middle}.table th{color:var(--muted);font-size:12px;font-weight:600;background:#0b1928}.table tr:last-child td{border:0}.table tbody tr:hover{background:rgba(89,181,255,.05)}.link-btn{border:0;background:transparent;color:#8fd6ff;padding:0;cursor:pointer;text-align:left}.filters{padding:14px;display:grid;grid-template-columns:minmax(220px,1fr) 160px 170px auto;gap:10px}.pager{padding:12px 14px;border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:12px}.monitor-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.video-card{overflow:hidden}.video{height:150px;display:grid;place-items:center;background:linear-gradient(145deg,#17273a,#07101a);color:#88a3bb;position:relative}.video:before,.video:after{content:"";position:absolute;background:#2e4961}.video:before{width:60%;height:1px;transform:rotate(20deg)}.video:after{height:60%;width:1px;transform:rotate(-20deg)}.video-card .card-body{padding:12px}.list{display:grid;gap:10px}.list-item{padding:12px;border:1px solid var(--line);border-radius:10px;background:#0b1928;display:flex;justify-content:space-between;gap:12px;align-items:center}.list-item strong{display:block;margin-bottom:3px}.detail-summary{padding:16px;margin-bottom:14px;display:flex;justify-content:space-between;gap:16px;align-items:center}.tabs{display:flex;gap:4px;overflow:auto;border-bottom:1px solid var(--line);padding:0 12px}.tabs button{min-height:46px;padding:0 13px;border:0;border-bottom:3px solid transparent;background:transparent;color:var(--muted);cursor:pointer;white-space:nowrap}.tabs button[aria-selected="true"]{color:var(--text);border-color:var(--primary)}.tabpanel{padding:17px}.kv{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.kv>div{padding:12px;border:1px solid var(--line);border-radius:10px;background:#0b1928}.kv dt{font-size:12px;color:var(--muted);margin-bottom:6px}.kv dd{margin:0;overflow-wrap:anywhere}.section-title{margin:22px 0 10px;font-size:15px}.cap-grid,.system-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.cap{padding:14px;border:1px solid var(--line);border-radius:10px;background:#0b1928}.cap strong{display:block;margin-bottom:8px}.cap p{margin:4px 0;color:var(--muted)}.zone-layout{grid-template-columns:minmax(320px,1.35fr) minmax(260px,.65fr)}.zone-canvas{min-height:330px;border:1px solid #38556e;border-radius:11px;background:linear-gradient(145deg,#172a3e,#081522);position:relative;overflow:hidden}.zone-canvas svg{width:100%;height:100%;position:absolute;inset:0}.zone-tools{display:grid;gap:10px;align-content:start}.vertex{display:grid;grid-template-columns:1fr 1fr 42px;gap:7px}.vertex input{min-width:0}.task-progress{height:8px;background:#24394c;border-radius:999px;overflow:hidden}.task-progress i{display:block;height:100%;background:var(--primary)}.ops-summary{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:14px}.ops-tabs{margin-bottom:0}.ops-panel{border-top:0;border-radius:0 0 14px 14px}.system-tile{padding:15px}.system-tile h3{margin:0 0 12px}.system-tile dl{margin:0;display:grid;grid-template-columns:1fr auto;gap:9px}.system-tile dt{color:var(--muted)}.system-tile dd{margin:0}.empty-state{padding:42px 20px;text-align:center;color:var(--muted)}.empty-state h2{color:var(--text)}.state-overlay{position:fixed;z-index:90;inset:98px 0 0 240px;background:rgba(7,16,29,.94);display:grid;place-items:center;padding:24px}.state-overlay>div{max-width:520px;text-align:center}.state-overlay h2{font-size:24px}.spinner{width:38px;height:38px;margin:auto;border:4px solid #24435b;border-top-color:var(--primary);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}dialog{width:min(700px,calc(100% - 28px));max-height:88dvh;padding:0;border:1px solid var(--line);border-radius:15px;background:var(--surface);color:var(--text);box-shadow:0 25px 80px rgba(0,0,0,.55)}dialog::backdrop{background:rgba(0,7,13,.72);backdrop-filter:blur(4px)}.dialog-head,.dialog-foot{padding:14px 17px;display:flex;justify-content:space-between;align-items:center;gap:10px;border-bottom:1px solid var(--line)}.dialog-head h2{margin:0;font-size:17px}.dialog-body{padding:17px;overflow:auto}.dialog-foot{border-bottom:0;border-top:1px solid var(--line);justify-content:flex-end}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:13px}.field{display:grid;gap:6px}.field.full{grid-column:1/-1}.field label{color:#c9d7e5}.radio-card{display:block;padding:12px;border:1px solid var(--line);border-radius:10px;margin-bottom:9px}.mobile-nav{display:none}.toast{position:fixed;z-index:120;right:18px;bottom:18px;max-width:390px;padding:13px 16px;border:1px solid #3f8467;border-radius:10px;background:#163829;color:#d9ffea;box-shadow:var(--shadow)}
@media(max-width:1100px){.stats,.ops-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.monitor-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.cap-grid,.system-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.kv{grid-template-columns:repeat(2,minmax(0,1fr))}.cols{grid-template-columns:1fr}}
@media(max-width:850px){body{padding-bottom:74px}.shell{display:block}.sidebar{display:none}.topbar{padding:10px 12px;align-items:flex-start}.context{gap:7px}.context-block:first-child,.user-label,.manage-link{display:none}.top-actions{justify-content:flex-end}.content{padding:15px 12px}.page-head{align-items:flex-start;flex-direction:column}.stats,.ops-summary,.monitor-grid,.cap-grid,.system-grid,.kv,.zone-layout{grid-template-columns:1fr}.filters{grid-template-columns:1fr 1fr}.filters .input{grid-column:1/-1}.detail-summary{align-items:flex-start;flex-direction:column}.btn,.btn-sm,.select,.input,.textarea,.link-btn{min-height:44px}.btn,.link-btn{min-width:44px}.link-btn{display:inline-flex;align-items:center}.mobile-nav{position:fixed;z-index:70;display:grid;grid-template-columns:repeat(5,1fr);left:7px;right:7px;bottom:7px;padding:5px;border:1px solid var(--line);border-radius:13px;background:#0c1928;box-shadow:var(--shadow)}.mobile-nav button{min-height:50px;border-radius:8px;background:transparent;color:var(--muted);font-size:11px}.mobile-nav button.active{background:var(--primary2);color:var(--text)}.state-overlay{inset:98px 0 0}.form-grid{grid-template-columns:1fr}.field.full{grid-column:auto}.banner{align-items:flex-start;flex-direction:column}.table{min-width:720px}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation:none!important;transition:none!important}}
</style>
</head>
<body>
<div class="prototype">PROTOTYPE - 仅供枚举交互,非实现依据</div>
<svg width="0" height="0" aria-hidden="true"><symbol id="logo" viewBox="0 0 24 24"><path fill="currentColor" d="M3 5h18v4H3zm0 6h11v8H3zm13 0h5v8h-5z"/></symbol></svg>
<div class="shell">
<aside class="sidebar">
<div class="brand"><span class="brand-mark">S</span><div><strong>YoVision Sense</strong><small>接入运维工作台</small></div></div>
<nav class="nav" aria-label="Sense 一级导航">
<button data-view="overview"><span>◫</span><span>运行总览</span></button>
<button data-view="monitor"><span>▶</span><span>实时监控</span><span class="count">4</span></button>
<button data-view="devices" class="active" aria-current="page"><span>▣</span><span>设备</span><span class="count">17</span></button>
<button data-view="onboarding"><span>⇧</span><span>接入任务</span><span class="count">2</span></button>
<button data-view="operations"><span>⌁</span><span>运维中心</span><span class="count">4</span></button>
</nav>
<div class="side-note">服务器时间:Asia/Shanghai<br><span id="serverClock">2026-08-04 10:48:22</span><br>Area/配额投影:v42 · 2 分钟前</div>
</aside>
<main class="main" id="main-content" tabindex="-1">
<header class="topbar">
<div class="context">
<div class="context-block"><span class="eyebrow">租户</span><strong>青藤教育集团</strong></div>
<div class="context-block"><label class="eyebrow" for="siteSwitcher">当前站点</label><select class="select" id="siteSwitcher"><option value="ivy">青藤寄宿学校</option><option value="east">城东试验点</option></select></div>
<a class="btn btn-quiet manage-link" href="../bell/index.html?view=sites&tenant=tenant-ivy&site=site-ivy&return_to=sense" aria-label="到 Bell 管理站点与区域">管理站点 / Area ↗</a>
</div>
<div class="top-actions">
<label class="eyebrow" for="roleSwitcher">演示角色</label><select class="select" id="roleSwitcher"><option value="admin">站点管理员</option><option value="readonly">只读用户</option></select>
<label class="eyebrow" for="stateSelect">页面状态</label><select class="select" id="stateSelect"><option value="normal">正常</option><option value="loading">加载中</option><option value="empty">空数据</option><option value="error">读取失败</option><option value="quota_unavailable">配额不可用</option><option value="policy_unavailable">Area 策略不可用</option><option value="denied">无权限</option></select>
<span class="user-label">林工</span>
</div>
</header>
<div class="content">
<div id="degradeBanner" class="banner" hidden><div><strong id="degradeTitle">依赖不可用</strong><div id="degradeText">已有设备与视频继续运行;只阻止相关新增或启用操作。</div></div><button class="btn btn-sm" id="retryDependency">重新检查</button></div>
<section class="view" data-page="overview">
<div class="page-head"><div><h1>运行总览</h1><p>先看影响范围,再进入设备或运维中心处理。</p></div><button class="btn btn-primary" data-write-action data-open-add>添加设备</button></div>
<div class="grid stats">
<button class="card stat" data-jump="devices"><span class="muted">设备台账</span><b>17</b><small>视频 16 · 雷达 1(适配器未就绪)</small></button>
<button class="card stat" data-jump="monitor"><span class="muted">当前 Readers</span><b>15</b><small>并发预览上限 4 路</small></button>
<button class="card stat" data-jump="operations" data-op-tab="reconcile"><span class="muted">待收敛</span><b>2</b><small>媒体 1 · 推理 1</small></button>
<button class="card stat" data-jump="operations" data-op-tab="alerts"><span class="muted">运维告警</span><b>4</b><small>与 Bell 业务预警分离</small></button>
</div>
<div class="grid cols">
<article class="card"><div class="card-head"><h2>需要关注</h2><button class="btn btn-sm" data-jump="operations">查看全部</button></div><div class="card-body list"><div class="list-item"><div><strong>北门 02 · 认证失败</strong><span class="muted">旧凭据不会回显,等待管理员更新</span></div><span class="badge danger">阻断接入</span></div><div class="list-item"><div><strong>宿舍东侧 03 · 推理未收敛</strong><span class="muted">退避第 2 次 · 下次 10:49:10</span></div><span class="badge warn">待重试</span></div><div class="list-item"><div><strong>edge-east · 隧道断开</strong><span class="muted">视频数据面仍正常 · 补传队列 86 MB</span></div><span class="badge warn">控制面异常</span></div></div></article>
<article class="card"><div class="card-head"><h2>容量</h2></div><div class="card-body"><p><strong>视频 16 / 128</strong></p><div class="task-progress"><i style="width:12.5%"></i></div><p class="muted">默认交付 16,不是硬编码上限。当前媒体分片承载 16 / 初始配置建议 32,启用 1 / 最多 4。</p><button class="btn btn-sm" data-jump="operations" data-op-tab="shards">查看分片依据</button></div></article>
</div>
</section>
<section class="view" data-page="monitor">
<div class="page-head"><div><h1>实时监控</h1><p>默认仅 4 路低码率预览;128 路不同时加载。</p></div><div class="actions"><select class="select" aria-label="监控布局"><option>2 × 2</option><option>1 + 3</option></select><button class="btn" data-write-action>保存布局</button></div></div>
<div class="grid monitor-grid"><article class="card video-card"><div class="video">低码率预览</div><div class="card-body"><strong>北围墙 01</strong><p class="muted">在线 · reader 2</p></div></article><article class="card video-card"><div class="video">低码率预览</div><div class="card-body"><strong>北围墙 02</strong><p class="muted">在线 · reader 1</p></div></article><article class="card video-card"><div class="video">认证失败</div><div class="card-body"><strong>北门 02</strong><p><span class="badge danger">无画面</span></p></div></article><article class="card video-card"><div class="video">待激活</div><div class="card-body"><strong>食堂后门 04</strong><p><span class="badge warn">尚未接入</span></p></div></article></div>
</section>
<section class="view active" data-page="devices">
<div class="page-head"><div><h1>设备</h1><p><span id="siteName">青藤寄宿学校</span> · 统一台账按模态与能力展示。</p></div><div class="actions"><button class="btn" data-jump="onboarding">批量接入</button><button class="btn btn-primary" data-write-action data-open-add>添加设备</button></div></div>
<article class="card"><div class="filters"><label class="field"><span class="eyebrow">搜索</span><input class="input" id="deviceSearch" placeholder="名称 / 序列号"></label><label class="field"><span class="eyebrow">模态</span><select class="select" id="modalityFilter"><option value="all">全部模态</option><option value="video">视频</option><option value="radar">雷达</option></select></label><label class="field"><span class="eyebrow">状态</span><select class="select" id="statusFilter"><option value="all">全部状态</option><option value="online">在线</option><option value="pending">待激活</option><option value="auth_failed">认证失败</option><option value="offline">离线</option><option value="adapter_not_ready">适配器未就绪</option></select></label><button class="btn" id="clearFilters">清除筛选</button></div><div class="table-wrap"><table class="table"><thead><tr><th>设备</th><th>模态 / 能力</th><th>状态</th><th>期望 / 实际</th><th>Area</th><th>最后变化</th><th>操作</th></tr></thead><tbody id="deviceRows"></tbody></table></div><div class="pager"><span id="pageSummary">第 1 / 2 页</span><div class="actions"><button class="btn btn-sm" id="prevPage">上一页</button><button class="btn btn-sm" id="nextPage">下一页</button></div></div></article>
</section>
<section class="view" data-page="device-detail">
<button class="btn btn-quiet" id="backToDevices">← 返回设备</button>
<article class="card detail-summary"><div><span class="eyebrow" id="detailModality">视频设备</span><h1 id="detailName" style="margin:5px 0">北围墙 01</h1><div class="inline"><span id="detailStatus" class="badge success">在线</span><span class="muted" id="detailSerial">SN-HK-001</span></div></div><div class="actions" id="detailActions"><button class="btn" id="auditLink">操作日志 ↗</button><button class="btn" data-write-action id="credentialBtn">更新凭据</button><button class="btn" data-write-action id="profileBtn">切换 Profile</button><button class="btn btn-danger" data-write-action id="lifecycleBtn">接入控制</button></div></article>
<article class="card"><div class="tabs" role="tablist" aria-label="设备详情"><button role="tab" id="tab-summary" aria-controls="panel-summary" aria-selected="true" tabindex="0">概览</button><button role="tab" id="tab-capabilities" aria-controls="panel-capabilities" aria-selected="false" tabindex="-1">能力探测</button><button role="tab" id="tab-health" aria-controls="panel-health" aria-selected="false" tabindex="-1">连接与健康</button><button role="tab" id="tab-zones" aria-controls="panel-zones" aria-selected="false" tabindex="-1" data-video-only>画面与检测区域</button></div>
<section class="tabpanel" role="tabpanel" id="panel-summary" aria-labelledby="tab-summary"><div class="kv" id="summaryKv"></div><h2 class="section-title">生命周期</h2><div class="list" id="lifecycleList"></div></section>
<section class="tabpanel" role="tabpanel" id="panel-capabilities" aria-labelledby="tab-capabilities" hidden><div class="actions" style="justify-content:space-between"><p class="muted">最近探测:<time title="2026-08-04 10:42:18 +08:00">6 分钟前</time> · 来源 ONVIF probe-8421</p><button class="btn" data-write-action id="reprobeBtn">重新探测并比较</button></div><div class="grid cap-grid" id="capabilityGrid"></div></section>
<section class="tabpanel" role="tabpanel" id="panel-health" aria-labelledby="tab-health" hidden><div class="grid system-grid"><article class="cap"><strong>连接</strong><p id="healthConnection">媒体在线</p><p>重连 0 次 · RTT 12ms</p></article><article class="cap"><strong>设备时间</strong><p>漂移 +0.2s</p><p>最近校时 <time title="2026-08-04 10:43:03 +08:00">5 分钟前</time></p></article><article class="cap"><strong>下游实际态</strong><p>MediaMTX:running</p><p>Brain:subscribed · Storage:ready</p></article></div></section>
<section class="tabpanel" role="tabpanel" id="panel-zones" aria-labelledby="tab-zones" hidden><div class="grid zone-layout"><div class="zone-canvas" id="zoneCanvas" aria-label="检测区域示意画布"><svg viewBox="0 0 100 100" preserveAspectRatio="none"><polygon id="zonePolygon" points="18,72 38,34 74,42 82,78" fill="rgba(85,214,190,.18)" stroke="#55d6be" stroke-width="1.2" vector-effect="non-scaling-stroke"/><polyline id="zoneLine" points="" fill="none" stroke="#ffc462" stroke-width="1.4"/></svg></div><div class="zone-tools"><div><strong>危险区域 A · v7</strong><p class="muted">归一化坐标;与 Bell 规则版本分离。</p></div><div id="vertexList"></div><div class="actions"><button class="btn btn-sm" data-write-action id="addVertex">添加点</button><button class="btn btn-sm" data-write-action id="reverseZone">反转方向</button><button class="btn btn-sm" data-write-action id="finishZone">完成绘制</button></div><button class="btn btn-primary" data-write-action id="saveZone">保存区域草稿</button><a class="btn" id="bellRuleLink" href="../bell/index.html?view=rules&camera=dev-001&zone=zone-a&return_to=sense">到 Bell 配置业务规则 ↗</a><p class="muted" id="draftState">草稿已自动保存到当前会话</p></div></div></section>
</article>
</section>
<section class="view" data-page="onboarding">
<div class="page-head"><div><h1>接入任务</h1><p>CSV 导入、发现与逐项激活均保留可重试结果。</p></div><div class="actions"><button class="btn">下载模板</button><button class="btn btn-primary" data-write-action id="newImport">新建批量导入</button></div></div>
<div class="grid stats"><article class="card stat"><span class="muted">运行中</span><b>1</b><small>发现与探测</small></article><article class="card stat"><span class="muted">待激活</span><b>2</b><small>写入台账但尚未接入</small></article><article class="card stat"><span class="muted">部分成功</span><b>1</b><small>可仅重试失败项</small></article><article class="card stat"><span class="muted">过去 24h</span><b>16</b><small>成功激活</small></article></div>
<article class="card"><div class="table-wrap"><table class="table"><thead><tr><th>任务</th><th>方式</th><th>进度</th><th>逐项结果</th><th>操作者 / 时间</th><th>操作</th></tr></thead><tbody><tr><td><strong>宿舍楼补点</strong><br><span class="muted">task-20260804-17</span></td><td>CSV</td><td><span class="badge warn">部分成功</span></td><td>成功 3 · 失败 1 · 待激活 1</td><td>林工<br><time class="muted" title="2026-08-04 10:31:08 +08:00">17 分钟前</time></td><td><button class="btn btn-sm" data-task-result>查看结果</button></td></tr><tr><td><strong>北区自动发现</strong><br><span class="muted">task-20260804-18</span></td><td>ONVIF 发现</td><td><div class="task-progress"><i style="width:64%"></i></div></td><td>探测 7 / 11</td><td>系统<br><time class="muted" title="2026-08-04 10:46:22 +08:00">2 分钟前</time></td><td><button class="btn btn-sm">后台继续</button></td></tr></tbody></table></div></article>
</section>
<section class="view" data-page="operations">
<div class="page-head"><div><h1>运维中心</h1><p>接入控制面的差异与基础设施告警;不包含 Bell 业务预警。</p></div><button class="btn" id="refreshOps">刷新实际态</button></div>
<div class="grid ops-summary"><article class="card stat"><span class="muted">待收敛</span><b>2</b><small>最高优先级 90</small></article><article class="card stat"><span class="muted">媒体分片</span><b>1 / 4</b><small>当前承载 16 / 配置建议 32</small></article><article class="card stat"><span class="muted">边缘节点</span><b>2 / 3</b><small>1 个控制面隧道断开</small></article><article class="card stat"><span class="muted">运维告警</span><b>4</b><small>P1 1 · P2 3</small></article></div>
<article class="card"><div class="tabs ops-tabs" role="tablist" aria-label="运维中心分类"><button role="tab" id="op-tab-reconcile" aria-controls="op-panel-reconcile" aria-selected="true" tabindex="0" data-ops-tab="reconcile">对账</button><button role="tab" id="op-tab-shards" aria-controls="op-panel-shards" aria-selected="false" tabindex="-1" data-ops-tab="shards">分片</button><button role="tab" id="op-tab-edge" aria-controls="op-panel-edge" aria-selected="false" tabindex="-1" data-ops-tab="edge">边缘与隧道</button><button role="tab" id="op-tab-alerts" aria-controls="op-panel-alerts" aria-selected="false" tabindex="-1" data-ops-tab="alerts">运维告警</button><button role="tab" id="op-tab-status" aria-controls="op-panel-status" aria-selected="false" tabindex="-1" data-ops-tab="status">系统状态</button></div>
<section class="tabpanel" role="tabpanel" id="op-panel-reconcile" aria-labelledby="op-tab-reconcile"><div class="table-wrap"><table class="table"><thead><tr><th>对象 / 优先级</th><th>期望态</th><th>实际态</th><th>差异与退避</th><th>下次重试</th><th>操作</th></tr></thead><tbody><tr><td><strong>宿舍东侧 03</strong><br><span class="badge danger">P90</span></td><td>Brain subscribed</td><td>worker not_found</td><td>第 2 次 · 20s</td><td><time title="2026-08-04 10:49:10 +08:00">22 秒后</time></td><td><button class="btn btn-sm" data-write-action>立即重试</button></td></tr><tr><td><strong>北围墙 02</strong><br><span class="badge warn">P60</span></td><td>Media running</td><td>path idle</td><td>第 1 次 · 10s</td><td><time title="2026-08-04 10:49:24 +08:00">36 秒后</time></td><td><button class="btn btn-sm" data-write-action>立即重试</button></td></tr></tbody></table></div><div class="card-body"><div class="banner danger"><div><strong>发现 5 个孤儿 path</strong><div>占当前 32 个实际 path 的 15.6%,超过 10% 安全闸,自动删除已阻止。</div></div><button class="btn btn-danger" data-write-action id="reviewOrphans">人工复核</button></div></div></section>
<section class="tabpanel" role="tabpanel" id="op-panel-shards" aria-labelledby="op-tab-shards" hidden><div class="card-body grid system-grid"><article class="cap"><strong>media-01</strong><p><span class="badge success">健康</span> · 当前承载 16 路</p><p>输入 54 Mbps · readers 15 · 重连 1</p><p>`max_streams=32` 是初始配置建议,不是产品硬上限,最终需压测确认。</p></article><article class="cap"><strong>inference-01</strong><p><span class="badge success">健康</span> · 15 / 16 源</p><p>p95 73ms · batch 4 · reconnect 0</p><p>扩容以 GPU、FPS、分辨率基准为准。</p></article><article class="cap"><strong>故障域说明</strong><p>当前启用 1 个媒体分片,逻辑最多 4 个。</p><p>迁移工作流属于 M4;本原型只展示归属和影响。</p></article></div></section>
<section class="tabpanel" role="tabpanel" id="op-panel-edge" aria-labelledby="op-tab-edge" hidden><div class="table-wrap"><table class="table"><thead><tr><th>节点 / 版本</th><th>在线时长 / 承载</th><th>控制面隧道</th><th>视频数据面</th><th>补传</th><th>最近心跳</th></tr></thead><tbody><tr><td>edge-north<br><span class="muted">agent 0.3.2</span></td><td>12d 06h · 9 路</td><td><span class="badge success">connected</span></td><td><span class="badge success">normal</span></td><td>0</td><td><time title="2026-08-04 10:48:39 +08:00">9 秒前</time></td></tr><tr><td>edge-east<br><span class="muted">agent 0.3.2</span></td><td>3d 11h · 7 路</td><td><span class="badge danger">disconnected</span></td><td><span class="badge success">normal</span></td><td>86 MB · 42%</td><td><time title="2026-08-04 10:46:42 +08:00">2 分钟前</time></td></tr><tr><td>edge-spare<br><span class="muted">agent 0.3.1</span></td><td>维护中 · 0 路</td><td><span class="badge warn">maintenance</span></td><td>无绑定</td><td>0</td><td><time title="2026-08-04 09:20:00 +08:00">1 小时前</time></td></tr></tbody></table></div></section>
<section class="tabpanel" role="tabpanel" id="op-panel-alerts" aria-labelledby="op-tab-alerts" hidden><div class="card-body list"><div class="list-item"><div><strong>P1 · Area 策略投影超过 5 分钟未更新</strong><span class="muted">仅阻止成像设备新增/启用;已有流继续</span></div><button class="btn btn-sm">查看依赖</button></div><div class="list-item"><div><strong>P2 · edge-east 隧道断开</strong><span class="muted">数据面正常,补传队列 86 MB</span></div><button class="btn btn-sm">查看节点</button></div><div class="list-item"><div><strong>P2 · 北门 02 认证失败</strong><span class="muted">连续 3 次认证拒绝,自动重试已暂停</span></div><button class="btn btn-sm" data-write-action>更新凭据</button></div></div></section>
<section class="tabpanel" role="tabpanel" id="op-panel-status" aria-labelledby="op-tab-status" hidden><div class="card-body grid system-grid"><article class="cap"><strong>Sense API</strong><p><span class="badge success">健康</span></p><p>运行 12d 06h · queue 2</p></article><article class="cap"><strong>MediaMTX</strong><p><span class="badge success">健康</span></p><p>1 / 1 实例 · 32 paths</p></article><article class="cap"><strong>PostgreSQL</strong><p><span class="badge success">健康</span></p><p>连接 8 / 50 · p95 7ms</p></article></div></section>
</article>
</section>
</div>
</main>
</div>
<nav class="mobile-nav" aria-label="移动端主导航"><button data-view="overview">总览</button><button data-view="monitor">监控</button><button data-view="devices" class="active" aria-current="page">设备</button><button data-view="onboarding">接入</button><button data-view="operations">运维</button></nav>
<div class="state-overlay" id="stateOverlay" hidden><div id="stateOverlayBody"></div></div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<dialog id="addDialog" aria-labelledby="addTitle"><form method="dialog"><div class="dialog-head"><h2 id="addTitle">添加设备</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭">×</button></div><div class="dialog-body"><div class="form-grid"><label class="field"><span>设备模态</span><select class="select" id="addModality"><option value="video">视频摄像头</option><option value="radar">毫米波雷达(M6)</option></select></label><label class="field"><span>Area</span><select class="select" id="addArea"><option value="gate">北门 · 允许成像</option><option value="dorm">宿舍卧室 · 仅非成像</option></select></label><label class="field full"><span>名称</span><input class="input" placeholder="例如:北门 03" required></label><label class="field"><span>稳定序列号</span><input class="input" placeholder="不使用 IP 作为身份" required></label><label class="field" id="profileField"><span>连接方式</span><select class="select"><option>ONVIF 探测</option><option>手动 RTSP</option></select></label></div><div class="banner danger" id="privacyBlock" hidden><div><strong>该 Area 不允许成像设备</strong><div>请改选允许成像的 Area;不能通过换显示名称绕过。</div></div></div><p class="muted">保存后进入“待激活”,探测成功且配额/Area 策略通过后才接入。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="savePending">保存为待激活</button></div></form></dialog>
<dialog id="credentialDialog" aria-labelledby="credentialTitle"><form method="dialog"><div class="dialog-head"><h2 id="credentialTitle">更新设备凭据</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭">×</button></div><div class="dialog-body"><div class="banner"><div><strong>旧凭据永不回显</strong><div>提交后立即写入密钥存储,并触发一次认证探测。</div></div></div><div class="form-grid"><label class="field"><span>用户名</span><input class="input" autocomplete="off"></label><label class="field"><span>新密码</span><input class="input" type="password" autocomplete="new-password"></label></div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-primary" value="default" id="saveCredential">更新并探测</button></div></form></dialog>
<dialog id="profileDialog" aria-labelledby="profileTitle"><form method="dialog"><div class="dialog-head"><h2 id="profileTitle">切换 ONVIF Profile</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭">×</button></div><div class="dialog-body"><label class="radio-card"><input type="radio" name="profile" checked> Profile_1 · 1920×1080 · H.264 · 当前生效</label><label class="radio-card"><input type="radio" name="profile"> Profile_2 · 640×360 · H.264</label><p class="muted">切换会重新建立媒体 path,短时影响观看、推理与录制;检测区域进入待校准,不自动修改坐标。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-primary" value="default" id="saveProfile">确认切换</button></div></form></dialog>
<dialog id="capDiffDialog" aria-labelledby="capDiffTitle"><form method="dialog"><div class="dialog-head"><h2 id="capDiffTitle">能力探测差异</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭">×</button></div><div class="dialog-body"><p>新探测 probe-8429 · <time title="2026-08-04 10:49:22 +08:00">刚刚</time></p><div class="table-wrap"><table class="table"><thead><tr><th>字段</th><th>上次探测</th><th>本次探测</th><th>生效值</th></tr></thead><tbody><tr><td>主 Profile</td><td>Profile_1</td><td>Profile_1</td><td>Profile_1</td></tr><tr><td>事件订阅</td><td>PullPoint</td><td>PullPoint + motion</td><td><span class="badge warn">待确认</span></td></tr><tr><td>设备时间</td><td>+0.2s</td><td>+1.8s</td><td>等待校时</td></tr></tbody></table></div></div><div class="dialog-foot"><button class="btn" value="cancel">保留当前</button><button class="btn btn-primary" value="default">应用可兼容差异</button></div></form></dialog>
<dialog id="operationDialog" aria-labelledby="operationTitle"><form method="dialog"><div class="dialog-head"><h2 id="operationTitle">设备接入控制</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭">×</button></div><div class="dialog-body"><label class="radio-card"><input type="radio" name="operation" value="pause" checked> <strong>暂停推理订阅</strong><br><span class="muted">解除推理侧对该路的订阅;有其他观看者时上游继续拉流,无其他 reader 时上游按需停止。</span></label><label class="radio-card"><input type="radio" name="operation" value="disable"> <strong>停用设备接入</strong><br><span class="muted">期望态改为 disabled;影响观看、推理、录制和后续证据回捞,实际态由对账器收敛。</span></label><label class="radio-card"><input type="radio" name="operation" value="kick"> <strong>仅踢当前会话</strong><br><span class="muted">用于异常连接恢复,不改变设备期望态,客户端可以重新连接。</span></label><div class="banner danger"><div><strong>这是高风险操作</strong><div>每次请求都会留下审计结果;幂等只避免重复副作用。</div></div></div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-danger" id="confirmOperation">确认执行</button></div></form></dialog>
<dialog id="taskDialog" aria-labelledby="taskTitle"><form method="dialog"><div class="dialog-head"><h2 id="taskTitle">宿舍楼补点 · 逐项结果</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭">×</button></div><div class="dialog-body"><div class="table-wrap"><table class="table"><thead><tr><th>行</th><th>设备</th><th>结果</th><th>原因 / 下一步</th><th>操作</th></tr></thead><tbody><tr><td>2</td><td>食堂后门 04</td><td><span class="badge warn">待激活</span></td><td>探测完成,等待确认</td><td><button type="button" class="btn btn-sm" data-write-action data-retry-row>复核并激活</button></td></tr><tr><td>3</td><td>宿舍东侧 03</td><td><span class="badge success">成功</span></td><td>已接入 media-01</td><td>—</td></tr><tr><td>4</td><td>北门 02</td><td><span class="badge danger">失败</span></td><td>AUTH_REJECTED · 更新凭据</td><td><button type="button" class="btn btn-sm" data-write-action data-retry-row>重试此行</button></td></tr></tbody></table></div><p class="muted">部分成功不会整体回滚:成功项已生效,失败项保持可重试。</p></div><div class="dialog-foot"><button type="button" class="btn" id="exportErrors">导出错误清单</button><button class="btn" value="cancel">关闭</button><button type="button" class="btn btn-primary" data-write-action id="retryFailed">仅重试失败项</button></div></form></dialog>
<dialog id="orphanDialog" aria-labelledby="orphanTitle"><form method="dialog"><div class="dialog-head"><h2 id="orphanTitle">孤儿 path 人工恢复</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭">×</button></div><div class="dialog-body"><p>安全闸已阻止本批删除,原型不提供绕过入口。先导出 5 个对象及 publisher、reader、录像索引与待回捞证据,再由独立维护任务分批处置。</p><label class="field"><span>处置原因</span><textarea class="textarea" placeholder="必填,写入操作审计"></textarea></label></div><div class="dialog-foot"><button class="btn" value="cancel">关闭</button><button class="btn btn-primary" value="default">创建人工处置任务</button></div></form></dialog>
<script>
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const devices=[
{id:'dev-001',name:'北围墙 01',serial:'SN-HK-001',modality:'video',status:'online',desired:'enabled',actual:'running',area:'北围墙',changed:'2026-08-04 10:43:03',ago:'5 分钟前'},
{id:'dev-002',name:'北围墙 02',serial:'SN-HK-002',modality:'video',status:'online',desired:'enabled',actual:'media idle',area:'北围墙',changed:'2026-08-04 10:47:32',ago:'1 分钟前'},
{id:'dev-003',name:'宿舍东侧 03',serial:'SN-DA-003',modality:'video',status:'online',desired:'enabled',actual:'inference missing',area:'宿舍外廊',changed:'2026-08-04 10:45:10',ago:'3 分钟前'},
{id:'dev-004',name:'北门 02',serial:'SN-HK-009',modality:'video',status:'auth_failed',desired:'enabled',actual:'auth rejected',area:'北门',changed:'2026-08-04 10:31:08',ago:'17 分钟前'},
{id:'dev-005',name:'食堂后门 04',serial:'SN-UN-021',modality:'video',status:'pending',desired:'pending',actual:'not provisioned',area:'食堂后门',changed:'2026-08-04 10:40:00',ago:'8 分钟前'},
{id:'dev-006',name:'体育馆 01',serial:'SN-HK-017',modality:'video',status:'offline',desired:'enabled',actual:'unreachable',area:'体育馆',changed:'2026-08-04 09:48:00',ago:'1 小时前'},
{id:'dev-007',name:'宿舍走廊雷达 01',serial:'RD-MW-001',modality:'radar',status:'adapter_not_ready',desired:'pending',actual:'adapter not ready',area:'宿舍走廊',changed:'2026-08-04 09:20:00',ago:'1 小时前'},
{id:'dev-008',name:'图书馆 01',serial:'SN-HK-022',modality:'video',status:'online',desired:'enabled',actual:'running',area:'图书馆',changed:'2026-08-04 10:47:40',ago:'1 分钟前'}
];
let page=1,currentDevice=devices[0],role='admin',dependency='normal',activeSite='ivy',points=[[18,72],[38,34],[74,42],[82,78]];
const statusMeta={online:['在线','success'],pending:['待激活','warn'],auth_failed:['认证失败','danger'],offline:['离线','danger'],adapter_not_ready:['适配器未就绪','warn']};
function toast(msg){const t=$('#toast');t.textContent=msg;t.hidden=false;clearTimeout(window.toastTimer);window.toastTimer=setTimeout(()=>t.hidden=true,3200)}
function switchView(name){const navName=name==='device-detail'?'devices':name;$$('.view').forEach(v=>v.classList.toggle('active',v.dataset.page===name));$$('[data-view]').forEach(b=>{const on=b.dataset.view===navName;b.classList.toggle('active',on);b.toggleAttribute('aria-current',on)});$('#main-content').focus({preventScroll:true})}
$$('[data-view]').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));$$('[data-jump]').forEach(b=>b.addEventListener('click',()=>{switchView(b.dataset.jump);if(b.dataset.opTab)selectOpsTab(b.dataset.opTab)}));
function renderDevices(){const q=$('#deviceSearch').value.trim().toLowerCase(),m=$('#modalityFilter').value,s=$('#statusFilter').value,siteRows=activeSite==='ivy'?devices:devices.slice(0,3);const rows=siteRows.filter(d=>(!q||(d.name+d.serial).toLowerCase().includes(q))&&(m==='all'||d.modality===m)&&(s==='all'||d.status===s));const pages=Math.max(1,Math.ceil(rows.length/5));page=Math.min(page,pages);const slice=rows.slice((page-1)*5,page*5);$('#deviceRows').innerHTML=slice.length?slice.map(d=>{const meta=statusMeta[d.status];return `<tr><td><button class="link-btn" data-device="${d.id}"><strong>${d.name}</strong><br><span class="muted">${d.serial}</span></button></td><td>${d.modality==='video'?'视频 · 成像/媒体':'雷达 · 遥测(M6)'}</td><td><span class="badge ${meta[1]}">${meta[0]}</span></td><td>${d.desired}<br><span class="muted">${d.actual}</span></td><td>${d.area}</td><td><time title="${d.changed} +08:00">${d.ago}</time></td><td><button class="btn btn-sm" data-device="${d.id}">查看</button></td></tr>`}).join(''):`<tr><td colspan="7"><div class="empty-state"><h2>没有匹配设备</h2><p>清除筛选或检查当前站点。</p></div></td></tr>`;$('#pageSummary').textContent=`第 ${page} / ${pages} 页 · 共 ${rows.length} 项`;$('#prevPage').disabled=page<=1;$('#nextPage').disabled=page>=pages;$$('[data-device]').forEach(b=>b.onclick=()=>openDevice(b.dataset.device))}
['deviceSearch','modalityFilter','statusFilter'].forEach(id=>$('#'+id).addEventListener(id==='deviceSearch'?'input':'change',()=>{page=1;renderDevices()}));$('#clearFilters').onclick=()=>{$('#deviceSearch').value='';$('#modalityFilter').value='all';$('#statusFilter').value='all';page=1;renderDevices()};$('#prevPage').onclick=()=>{page--;renderDevices()};$('#nextPage').onclick=()=>{page++;renderDevices()};
function openDevice(id){currentDevice=devices.find(d=>d.id===id)||devices[0];const meta=statusMeta[currentDevice.status];$('#detailName').textContent=currentDevice.name;$('#detailSerial').textContent=currentDevice.serial;$('#detailModality').textContent=currentDevice.modality==='video'?'视频设备':'雷达设备 · M6';$('#detailStatus').textContent=meta[0];$('#detailStatus').className='badge '+meta[1];$$('[data-video-only]').forEach(e=>e.hidden=currentDevice.modality!=='video');$('#profileBtn').hidden=currentDevice.modality!=='video'||role==='readonly';$('#credentialBtn').hidden=currentDevice.modality!=='video'||role==='readonly';$('#summaryKv').innerHTML=`<div><dt>期望态</dt><dd>${currentDevice.desired}</dd></div><div><dt>实际态</dt><dd>${currentDevice.actual}</dd></div><div><dt>Area</dt><dd>${currentDevice.area} · policy v42</dd></div><div><dt>分片</dt><dd>${currentDevice.modality==='video'?'media-01 / inference-01':'signal plane(M6)'}</dd></div>`;$('#lifecycleList').innerHTML=currentDevice.status==='pending'?`<div class="list-item"><div><strong>等待激活</strong><span class="muted">先完成重新探测、校时、视频配额和 Area policy v42 复核,再写 enabled 期望态。</span></div><div class="actions"><button class="btn" data-write-action id="recheckPending">重新探测 / 复核</button><button class="btn btn-primary" data-write-action id="activateNow">确认激活</button></div></div>`:currentDevice.status==='auth_failed'?`<div class="list-item"><div><strong>认证失败,自动重试已暂停</strong><span class="muted">AUTH_REJECTED · 更新凭据后重新探测。</span></div><button class="btn btn-primary" data-write-action id="fixCredential">更新凭据</button></div>`:`<div class="list-item"><div><strong>接入已激活</strong><span class="muted">期望态 ${currentDevice.desired} · 实际态 ${currentDevice.actual}</span></div><span class="badge ${meta[1]}">${meta[0]}</span></div>`;const caps=currentDevice.modality==='video'?[['设备身份','Hikvision DS-2CD · FW 5.7.18','ONVIF probe-8421'],['采购白名单','已验证型号 · M0 文档记录','只读结果,无在线维护页'],['认证','WS-Security UsernameToken','探测值 = 生效值'],['Profiles','2 个 · H.264','Profile_1 生效'],['校时','SetSystemDateAndTime 支持','漂移 +0.2s'],['事件订阅','PullPoint 支持','motion 待确认'],['媒体','RTSP main/sub','主码流推理 · 子码流预览']]:[['适配器','mmWave 协议适配器','adapter_not_ready'],['交付阶段','M6','当前没有真实连接'],['能力声明','telemetry / occupancy','仅设计值,尚未探测']];$('#capabilityGrid').innerHTML=caps.map(x=>`<article class="cap"><strong>${x[0]}</strong><p>${x[1]}</p><p>${x[2]}</p></article>`).join('');$('#healthConnection').textContent=currentDevice.status==='adapter_not_ready'?'协议适配器尚未交付;无连接、无模拟遥测':currentDevice.actual;$('#auditLink').onclick=()=>location.href=`../bell/index.html?view=audit&tenant=tenant-ivy&site=site-ivy&device=${currentDevice.id}&return_to=sense-device`;$('#bellRuleLink').href=`../bell/index.html?view=rules&camera=${currentDevice.id}&zone=zone-a&return_to=sense-device`;switchDetailTab('summary');switchView('device-detail');const a=$('#activateNow'),f=$('#fixCredential'),r=$('#recheckPending');if(a)a.onclick=()=>toast('已提交激活期望态;等待对账收敛');if(f)f.onclick=()=>$('#credentialDialog').showModal();if(r)r.onclick=()=>toast('探测与校时完成;配额 16/128、Area policy v42 均通过');applyRole()}
$('#backToDevices').onclick=()=>switchView('devices');
function bindTabs(selector,callback){const tabs=$$(selector);tabs.forEach((tab,i)=>{tab.addEventListener('click',()=>callback(tab));tab.addEventListener('keydown',e=>{if(!['ArrowLeft','ArrowRight','Home','End'].includes(e.key))return;e.preventDefault();let n=e.key==='Home'?0:e.key==='End'?tabs.length-1:(i+(e.key==='ArrowRight'?1:-1)+tabs.length)%tabs.length;tabs[n].focus();callback(tabs[n])})})}
function switchDetailTab(name){const tab=$('#tab-'+name);if(!tab||tab.hidden)return;$$('[id^="tab-"]').filter(t=>t.closest('.card')&&t.getAttribute('role')==='tab'&&!t.dataset.opsTab).forEach(t=>{const on=t===tab;t.setAttribute('aria-selected',on);t.tabIndex=on?0:-1;const p=$('#'+t.getAttribute('aria-controls'));if(p)p.hidden=!on})}
bindTabs('.detail-summary + .card .tabs [role="tab"]',t=>switchDetailTab(t.id.replace('tab-','')));
function selectOpsTab(name){$$('[data-ops-tab]').forEach(t=>{const on=t.dataset.opsTab===name;t.setAttribute('aria-selected',on);t.tabIndex=on?0:-1;$('#'+t.getAttribute('aria-controls')).hidden=!on})}
bindTabs('[data-ops-tab]',t=>selectOpsTab(t.dataset.opsTab));
$$('[data-open-add]').forEach(b=>b.onclick=()=>$('#addDialog').showModal());function updatePrivacy(){const block=$('#addModality').value==='video'&&$('#addArea').value==='dorm';$('#privacyBlock').hidden=!block;$('#savePending').disabled=block||dependency!=='normal';$('#profileField').hidden=$('#addModality').value!=='video'}$('#addModality').onchange=updatePrivacy;$('#addArea').onchange=updatePrivacy;$('#savePending').onclick=()=>{$('#addDialog').close();toast('设备已保存为待激活,可在接入任务查看逐项结果')};
$('#credentialBtn').onclick=()=>$('#credentialDialog').showModal();$('#profileBtn').onclick=()=>$('#profileDialog').showModal();$('#reprobeBtn').onclick=()=>$('#capDiffDialog').showModal();$('#lifecycleBtn').onclick=()=>$('#operationDialog').showModal();$('#confirmOperation').onclick=()=>{const op=$('input[name="operation"]:checked').value;$('#operationDialog').close();toast(op==='pause'?'推理订阅:已解除;其他 reader:2;上游:继续拉流':op==='disable'?'期望态已改为 disabled;实际态正在收敛':'当前会话已踢出;期望态未改变')};$('#saveCredential').onclick=()=>toast('新凭据已写入,正在重新探测;旧值未回显');$('#saveProfile').onclick=()=>toast('Profile 切换已进入对账队列;检测区域标记待校准');$$('[data-task-result]').forEach(b=>b.onclick=()=>$('#taskDialog').showModal());$('#retryFailed').onclick=()=>{toast('已创建仅含 1 个失败项的重试任务');$('#taskDialog').close()};$$('[data-retry-row]').forEach(b=>b.onclick=()=>toast('已创建单行重试任务,不影响已成功项'));$('#exportErrors').onclick=()=>toast('错误清单已按当前任务生成(原型不写入文件)');$('#reviewOrphans').onclick=()=>$('#orphanDialog').showModal();
function renderVertices(){const box=$('#vertexList');box.innerHTML=points.map((p,i)=>`<div class="vertex"><input class="input" aria-label="点 ${i+1} X" type="number" min="0" max="100" value="${p[0]}" data-axis="0" data-index="${i}"><input class="input" aria-label="点 ${i+1} Y" type="number" min="0" max="100" value="${p[1]}" data-axis="1" data-index="${i}"><button class="btn icon-btn" data-delete-point="${i}" data-write-action aria-label="删除点 ${i+1}">×</button></div>`).join('');$('#zonePolygon').setAttribute('points',points.map(p=>p.join(',')).join(' '));$$('[data-axis]').forEach(i=>i.onchange=()=>{points[+i.dataset.index][+i.dataset.axis]=Math.max(0,Math.min(100,+i.value));renderVertices();saveDraft()});$$('[data-delete-point]').forEach(b=>b.onclick=()=>{if(points.length<=3)return toast('多边形至少需要 3 个点');points.splice(+b.dataset.deletePoint,1);renderVertices();saveDraft()});applyRole()}
function saveDraft(){sessionStorage.setItem('sense-zone-draft',JSON.stringify(points));$('#draftState').textContent='草稿已自动保存到当前会话 · 刚刚'}$('#addVertex').onclick=()=>{points.push([50,50]);renderVertices();saveDraft()};$('#reverseZone').onclick=()=>{points.reverse();renderVertices();saveDraft()};$('#finishZone').onclick=()=>toast(points.length>=3?'绘制已完成,可保存草稿':'至少需要 3 个点');$('#saveZone').onclick=()=>toast('检测区域草稿已保存为 v8;不会自动发布 Bell 规则');
function applyRole(){role=$('#roleSwitcher').value;$$('[data-write-action]').forEach(e=>e.hidden=role==='readonly');if(role==='readonly')toast('已切换为只读:所有写操作已隐藏')}
$('#roleSwitcher').onchange=applyRole;
function renderState(state){dependency=state;const overlay=$('#stateOverlay'),body=$('#stateOverlayBody'),banner=$('#degradeBanner');overlay.hidden=true;banner.hidden=true;$$('[data-write-action]').forEach(e=>{if(role!=='readonly')e.hidden=false;e.disabled=false});if(state==='loading'){body.innerHTML='<div class="spinner"></div><h2>正在读取站点状态</h2><p class="muted">不会用旧数据伪装最新状态。</p>';overlay.hidden=false}else if(state==='empty'){body.innerHTML='<h2>这个站点还没有设备</h2><p class="muted">可从接入任务发现或批量导入设备。</p><button class="btn btn-primary" onclick="document.querySelector(\'[data-view=onboarding]\').click()">开始接入</button>';overlay.hidden=false}else if(state==='error'){body.innerHTML='<h2>无法读取最新状态</h2><p class="muted">当前数据可能已过期,未知不会显示为在线。</p><button class="btn" onclick="document.querySelector(\'#stateSelect\').value=\'normal\';renderState(\'normal\')">重试</button>';overlay.hidden=false}else if(state==='denied'){body.innerHTML='<h2>无权访问此站点</h2><p class="muted">请求已被安全拒绝;页面不会确认目标设备是否存在。</p><button class="btn" onclick="history.back()">返回上一页</button>';overlay.hidden=false}else if(state==='quota_unavailable'||state==='policy_unavailable'){banner.hidden=false;$('#degradeTitle').textContent=state==='quota_unavailable'?'配额服务暂时不可用':'Area 策略投影暂时不可用';$('#degradeText').textContent=state==='quota_unavailable'?'已有设备、视频和列表继续可用;新增与启用已禁用。':'已有链路保持原状态;涉及设备新增或启用的写操作已禁用并产生运维告警。';$$('[data-open-add],#savePending,#activateNow').forEach(e=>{if(e)e.disabled=true})}applyRole();updatePrivacy()}
$('#stateSelect').onchange=e=>renderState(e.target.value);$('#retryDependency').onclick=()=>{$('#stateSelect').value='normal';renderState('normal');toast('依赖已恢复,写操作重新可用')};
$('#siteSwitcher').onchange=e=>{activeSite=e.target.value;$('#siteName').textContent=e.target.selectedOptions[0].textContent;$$('[data-view="devices"] .count').forEach(c=>c.textContent=activeSite==='ivy'?'17':'3');page=1;renderDevices();toast(`站点上下文已切换,当前示例设备 ${activeSite==='ivy'?'17':'3'} 台`)};$('#refreshOps').onclick=()=>toast('实际态已刷新 · 2026-08-04 10:49:22 +08:00');
const route=new URLSearchParams(location.search);if(route.get('denied')==='1'){$('#stateSelect').value='denied';renderState('denied')}if(route.get('view'))switchView(route.get('view'));if(route.get('device'))openDevice(route.get('device'));
const draft=sessionStorage.getItem('sense-zone-draft');if(draft){try{points=JSON.parse(draft)}catch{}}renderVertices();renderDevices();applyRole();setInterval(()=>{const d=new Date();$('#serverClock').textContent=new Intl.DateTimeFormat('zh-CN',{timeZone:'Asia/Shanghai',hour12:false,year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'}).format(d)},1000);
</script>
</body>
</html>