162 lines
56 KiB
HTML
162 lines
56 KiB
HTML
<!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>
|