docs(design): close Bell prototype review gaps
This commit is contained in:
+384
-67
@@ -79,6 +79,7 @@
|
||||
.btn-danger { background: rgba(251,113,133,.1); border-color: rgba(251,113,133,.38); color: #ffb4c2; }
|
||||
.btn-quiet { border-color: transparent; background: transparent; color: var(--muted); }
|
||||
.icon-btn { width: 42px; padding: 0; }
|
||||
.text-button { min-height: 32px; padding: 2px 6px; border: 0; background: transparent; color: #b8e5ff; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.select, .field, .textarea { width: 100%; min-height: 40px; padding: 8px 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); }
|
||||
.textarea { min-height: 80px; resize: vertical; }
|
||||
.top-actions .select { width: auto; min-width: 150px; }
|
||||
@@ -132,10 +133,18 @@
|
||||
.fact span { display: block; color: var(--muted); font-size: 11px; }
|
||||
.fact strong { display: block; margin-top: 3px; font-size: 13px; font-variant-numeric: tabular-nums; }
|
||||
.privacy-note { margin: 0 12px 12px; padding: 9px 10px; display: flex; gap: 8px; border-left: 3px solid var(--cyan); background: rgba(56,189,248,.08); color: var(--muted); font-size: 12px; }
|
||||
.relation-panel { margin: 0 12px 12px; padding: 11px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); }
|
||||
.relation-panel h3 { margin: 0 0 8px; font-size: 13px; }
|
||||
.relation-list { display: grid; gap: 7px; }
|
||||
.relation-item { min-height: 40px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--text); display: flex; align-items: center; justify-content: space-between; gap: 10px; text-align: left; }
|
||||
.relation-item span { color: var(--muted); font-size: 12px; }
|
||||
.action-scroll { max-height: calc(100dvh - 210px); overflow-y: auto; }
|
||||
.ack-box { padding: 14px; border-bottom: 1px solid var(--line); }
|
||||
.ack-box .btn { width: 100%; min-height: 48px; }
|
||||
.ack-box p { margin: 9px 0 0; color: var(--muted); font-size: 12px; }
|
||||
.ack-tools { margin-top: 8px; display: flex; justify-content: flex-end; }
|
||||
.inline-alert { margin-top: 9px; padding: 9px 10px; border: 1px solid rgba(251,191,36,.4); border-radius: 8px; background: rgba(251,191,36,.09); color: #ffe8a8; font-size: 12px; }
|
||||
.inline-alert.danger { border-color: rgba(251,113,133,.42); background: rgba(251,113,133,.09); color: #ffc3ce; }
|
||||
.countdown { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center; padding: 12px; margin: 12px; border: 1px solid rgba(251,191,36,.28); border-radius: 10px; background: rgba(251,191,36,.07); }
|
||||
.countdown strong { display: block; font-size: 20px; color: #ffe18a; font-variant-numeric: tabular-nums; }
|
||||
.countdown span { color: var(--muted); font-size: 12px; }
|
||||
@@ -149,6 +158,8 @@
|
||||
.step.current .step-dot { background: var(--warning); }
|
||||
.step strong { display: block; }
|
||||
.step span { color: var(--muted); font-size: 12px; }
|
||||
.delivery-facts { margin-top: 4px; display: grid; gap: 2px; }
|
||||
.delivery-facts span { display: block; }
|
||||
.disposition { padding: 14px; border-top: 1px solid var(--line); display: grid; gap: 9px; }
|
||||
.disposition label { color: var(--muted); font-size: 12px; font-weight: 700; }
|
||||
.disposition-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
@@ -167,6 +178,8 @@
|
||||
td { font-variant-numeric: tabular-nums; }
|
||||
tr:last-child td { border-bottom: 0; }
|
||||
tr:hover td { background: color-mix(in srgb, var(--primary-soft) 50%, transparent); }
|
||||
.pager { min-height: 58px; padding: 10px 14px; border-top: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.pager-actions { display: flex; gap: 7px; }
|
||||
.rule-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.rule-card { padding: 15px; }
|
||||
.rule-card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
@@ -190,6 +203,9 @@
|
||||
.linked-zone strong, .linked-zone small { display: block; }
|
||||
.linked-zone small { color: var(--muted); }
|
||||
.workflow-note { grid-column: 1/-1; margin: 0; padding: 10px 11px; border-left: 3px solid var(--primary); background: var(--primary-soft); color: var(--muted); font-size: 12px; }
|
||||
.version-diff { display: grid; gap: 8px; }
|
||||
.version-diff article { padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); }
|
||||
.version-diff article strong { display: block; }
|
||||
.chain-grid { grid-template-columns: minmax(0, 1.4fr) minmax(300px, .8fr); }
|
||||
.chain-row { padding: 13px 14px; display: grid; grid-template-columns: 40px 1fr auto; gap: 11px; align-items: center; border-bottom: 1px solid var(--line); }
|
||||
.chain-row:last-child { border-bottom: 0; }
|
||||
@@ -206,6 +222,7 @@
|
||||
.donut::after { content: "83%\A 24h 内闭环"; white-space: pre; position: absolute; inset: 27px; border-radius: 50%; display: grid; place-items: center; text-align: center; background: var(--surface); color: var(--text); font-weight: 800; }
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 10px; color: var(--muted); font-size: 12px; }
|
||||
.legend i { display: inline-block; width: 9px; height: 9px; margin-right: 5px; border-radius: 2px; }
|
||||
.metric-note { margin: 12px 0 0; padding: 10px 12px; border-left: 3px solid var(--cyan); background: rgba(56,189,248,.08); color: var(--muted); }
|
||||
.state-layer { display: none; min-height: 420px; place-items: center; padding: 24px; border: 1px dashed var(--line); border-radius: var(--radius); background: var(--surface); text-align: center; }
|
||||
.state-layer.show { display: grid; }
|
||||
.state-layer .icon { width: 40px; height: 40px; color: var(--primary); }
|
||||
@@ -216,7 +233,7 @@
|
||||
.skeleton i:nth-child(2) { width: 72%; }
|
||||
@keyframes shimmer { to { background-position: -200% 0; } }
|
||||
dialog { width: min(610px, calc(100% - 28px)); max-height: 88dvh; padding: 0; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); color: var(--text); box-shadow: 0 24px 80px rgba(0,0,0,.5); }
|
||||
#ruleDialog { width: min(760px, calc(100% - 28px)); }
|
||||
#ruleDialog, #eventDialog, #handoverDialog, #rollbackDialog { width: min(760px, calc(100% - 28px)); }
|
||||
dialog::backdrop { background: rgba(4,7,15,.68); backdrop-filter: blur(4px); }
|
||||
.dialog-head, .dialog-foot { padding: 14px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.dialog-head h2 { margin: 0; font-size: 17px; }
|
||||
@@ -232,6 +249,7 @@
|
||||
.muted { color: var(--muted); }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-variant-numeric: tabular-nums; }
|
||||
.mobile-nav { display: none; }
|
||||
.mobile-manage { display: none; }
|
||||
@media (max-width: 1240px) {
|
||||
:root { --sidebar: 78px; }
|
||||
.brand-copy, .nav span:not(.count), .nav-label, .shift-card { display: none; }
|
||||
@@ -249,6 +267,7 @@
|
||||
.sidebar { display: none; }
|
||||
.topbar { padding: 8px 12px; }
|
||||
.site span, .top-actions .select { display: none; }
|
||||
.mobile-manage { display: inline-flex; min-width: 44px; }
|
||||
.content { padding: 12px; }
|
||||
.btn, .select, .field, .textarea { min-height: 44px; }
|
||||
.icon-btn { min-width: 44px; }
|
||||
@@ -260,6 +279,7 @@
|
||||
.site-grid { grid-template-columns: 1fr; }
|
||||
.rule-grid { grid-template-columns: 1fr 1fr; }
|
||||
.fact-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.pager { align-items: flex-start; flex-direction: column; }
|
||||
.mobile-nav { position: fixed; z-index: 50; left: 8px; right: 8px; bottom: 8px; display: grid; grid-template-columns: repeat(5, 1fr); padding: 5px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
|
||||
.mobile-nav button { min-height: 48px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); display: grid; place-items: center; font-size: 11px; }
|
||||
.mobile-nav button.active { color: #c9c1ff; background: var(--primary-soft); }
|
||||
@@ -302,13 +322,16 @@
|
||||
<div class="app">
|
||||
<aside class="sidebar" aria-label="Bell 主导航">
|
||||
<div class="brand"><div class="brand-mark">Y</div><div class="brand-copy"><strong>YoVision</strong><small>Bell · Duty Console</small></div></div>
|
||||
<div class="nav-label">Operations</div>
|
||||
<div class="nav-label">值班与业务</div>
|
||||
<nav class="nav">
|
||||
<button data-view="duty" aria-current="page"><svg class="icon"><use href="#i-inbox"/></svg><span>值班台</span><span class="count">6</span></button>
|
||||
<button data-view="events"><svg class="icon"><use href="#i-event"/></svg><span>事件中心</span></button>
|
||||
<button data-view="rules"><svg class="icon"><use href="#i-rule"/></svg><span>规则策略</span></button>
|
||||
<button data-view="chains"><svg class="icon"><use href="#i-phone"/></svg><span>升级链</span></button>
|
||||
<button data-view="reports"><svg class="icon"><use href="#i-chart"/></svg><span>运营报表</span></button>
|
||||
</nav>
|
||||
<div class="nav-label">管理</div>
|
||||
<nav class="nav">
|
||||
<button data-view="sites"><svg class="icon"><use href="#i-rule"/></svg><span>站点与 Area</span></button>
|
||||
<button data-view="audit"><svg class="icon"><use href="#i-shield"/></svg><span>审计日志</span></button>
|
||||
</nav>
|
||||
@@ -319,7 +342,8 @@
|
||||
<header class="topbar">
|
||||
<div class="site"><strong>青藤教育集团 · 青藤寄宿学校</strong><span>当前账号:林工 · 租户管理员 · Asia/Shanghai</span></div>
|
||||
<div class="top-actions">
|
||||
<select class="select" id="stateSelect" aria-label="原型状态演示"><option value="normal">正常状态</option><option value="loading">加载中</option><option value="weak">弱网 / 证据失败</option><option value="empty">队列为空</option><option value="denied">无权限</option></select>
|
||||
<select class="select" id="stateSelect" aria-label="原型状态演示"><option value="normal">正常状态</option><option value="loading">加载中</option><option value="weak">弱网 / 证据失败</option><option value="empty">队列为空</option><option value="denied">无权限</option><option value="expired">会话已过期</option></select>
|
||||
<button class="btn mobile-manage" id="mobileManage" aria-haspopup="dialog">管理</button>
|
||||
<button class="btn icon-btn" id="themeToggle" aria-label="切换明暗主题" title="切换明暗主题"><svg class="icon"><use href="#i-sun"/></svg></button>
|
||||
<button class="btn icon-btn" aria-label="查看通知" title="查看通知"><svg class="icon"><use href="#i-bell"/></svg></button>
|
||||
</div>
|
||||
@@ -329,7 +353,7 @@
|
||||
<section class="state-layer" id="stateLayer" aria-live="polite"></section>
|
||||
|
||||
<section class="view active" data-page="duty">
|
||||
<div class="page-head"><div><h1>实时值班台</h1><p>先确认高危预警,再查看证据与升级状态;重复投递不会生成重复处置。</p></div><div class="page-actions"><span class="badge success">值班组在线</span><button class="btn" id="muteButton">限时静默</button></div></div>
|
||||
<div class="page-head"><div><h1>实时值班台</h1><p>先确认高危预警,再查看证据与升级状态;重复投递不会生成重复处置。</p></div><div class="page-actions"><span class="badge success">值班组在线</span><button class="btn" id="handoverButton">发起交接班</button><button class="btn" id="muteButton">限时静默</button></div></div>
|
||||
<div class="duty-grid">
|
||||
<section class="card queue" aria-label="待处置预警队列">
|
||||
<div class="card-head"><h2>待处置预警</h2><span class="badge danger">6 个未 ack</span></div>
|
||||
@@ -350,18 +374,19 @@
|
||||
<div class="fact-grid">
|
||||
<div class="fact"><span>规则</span><strong id="factRule">非授权人员进入</strong></div><div class="fact"><span>置信度</span><strong>0.92</strong></div><div class="fact"><span>持续时间</span><strong>8.4s</strong></div><div class="fact"><span>证据状态</span><strong style="color:var(--success)">已校验</strong></div>
|
||||
</div>
|
||||
<div class="relation-panel"><h3>关联事件 · <span id="relatedEventCount">2</span></h3><div class="relation-list" id="relatedEvents"></div></div>
|
||||
<div class="privacy-note"><svg class="icon"><use href="#i-lock"/></svg><span>仅显示与本事件关联的 20 秒证据。常态录像仍在客户 NVR,本页不暴露流地址或摄像头凭据。</span></div>
|
||||
</section>
|
||||
|
||||
<aside class="card action-panel" aria-label="预警确认与处置">
|
||||
<div class="card-head"><h2>处置与升级</h2><span class="badge warning" id="ackState">等待 ack</span></div>
|
||||
<div class="action-scroll">
|
||||
<div class="ack-box"><button class="btn btn-primary" id="ackButton"><svg class="icon"><use href="#i-check"/></svg>确认接手</button><p id="ackHelp">一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。</p></div>
|
||||
<div class="ack-box"><button class="btn btn-primary" id="ackButton"><svg class="icon"><use href="#i-check"/></svg>确认接手</button><p id="ackHelp">一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。</p><div class="ack-tools"><button class="text-button" id="simulateAckConflict">演示另一值班员抢先 ack</button></div><div class="inline-alert" id="ackConflict" role="alert" hidden></div></div>
|
||||
<div><div class="countdown"><svg class="icon" style="color:var(--warning);width:26px;height:26px"><use href="#i-clock"/></svg><div><strong id="countdown">01:42</strong><span>未 ack 将升级到校级负责人</span></div></div>
|
||||
<div class="escalation"><h3>投递时间线</h3>
|
||||
<div class="step done"><i class="step-dot"></i><strong>值班室 Web + 本地声光</strong><span>23:14:10 · 已送达</span></div>
|
||||
<div class="step current"><i class="step-dot"></i><strong>值班员短信</strong><span>23:14:13 · 已送达,未确认</span></div>
|
||||
<div class="step"><i class="step-dot"></i><strong>校级负责人语音</strong><span>预计 23:16:10 · 尚未发送</span></div>
|
||||
<div class="step done"><i class="step-dot"></i><strong>值班室 Web + 本地声光</strong><div class="delivery-facts"><span>已发出 23:14:10</span><span>已送达 23:14:11</span><span>已看到 23:14:31</span></div></div>
|
||||
<div class="step current"><i class="step-dot"></i><strong>值班员短信 · 可重试</strong><div class="delivery-facts"><span>已发出 23:14:13</span><span style="color:var(--warning)">未收到送达回执 · 已等待 42 秒</span><span>通道不支持“已看到” · 重试 2/3,23:15:25</span></div></div>
|
||||
<div class="step"><i class="step-dot"></i><strong>校级负责人语音</strong><div class="delivery-facts"><span>预计 23:16:10 · 尚未发送</span><span>号码无效将标记最终失败,不继续伪装送达</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="disposition"><label for="outcome">处置结果</label><select class="select" id="outcome"><option value="">待选择</option><option>已现场核查,无人员受伤</option><option>已通知安保人员处理</option><option>已转交校方负责人</option></select><label for="note">处置备注</label><textarea class="textarea" id="note" placeholder="记录已采取的动作,不改写原始事件事实"></textarea><div class="disposition-actions"><button class="btn btn-danger" id="falsePositive">标记误报</button><button class="btn btn-success" id="completeAlert">完成处置</button></div></div>
|
||||
@@ -375,18 +400,14 @@
|
||||
<div class="grid stats">
|
||||
<article class="card stat"><span>今日事件</span><strong>47</strong><span class="muted">较昨日 -8%</span></article><article class="card stat"><span>已触发预警</span><strong>18</strong><span class="muted">规则命中后独立创建</span></article><article class="card stat"><span>误报反馈</span><strong>5</strong><span class="muted">进入标注队列</span></article><article class="card stat"><span>证据可用率</span><strong>97.9%</strong><span class="muted">1 个片段可重试</span></article>
|
||||
</div>
|
||||
<article class="card"><div class="card-head"><div class="toolbar"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" aria-label="搜索事件" placeholder="事件编号、点位或规则"></div><select class="select" aria-label="严重度筛选"><option>全部严重度</option><option>S1 高危</option><option>S2 重要</option></select><select class="select" aria-label="结果筛选"><option>全部结果</option><option>已处置</option><option>误报</option></select></div><span class="muted">共 47 条</span></div><div class="table-wrap"><table><thead><tr><th>事件编号</th><th>时间</th><th>点位</th><th>规则</th><th>事实</th><th>预警</th><th>结果</th><th>证据</th></tr></thead><tbody>
|
||||
<tr><td class="mono">EVT-01J8…7A2</td><td>23:14:08</td><td>北围墙 01</td><td>非授权人员进入</td><td><span class="badge danger">已确认</span></td><td>Alert #1842</td><td>处置中</td><td><span class="badge success">可用</span></td></tr>
|
||||
<tr><td class="mono">EVT-01J8…69P</td><td>22:48:31</td><td>宿舍东通道</td><td>区域聚集</td><td><span class="badge info">已记录</span></td><td>未触发</td><td>试运行样本</td><td><span class="badge success">可用</span></td></tr>
|
||||
<tr><td class="mono">EVT-01J8…52C</td><td>22:19:04</td><td>实验楼入口</td><td>危险区域闯入</td><td><span class="badge danger">已确认</span></td><td>Alert #1839</td><td>误报</td><td><span class="badge warning">重试中</span></td></tr>
|
||||
</tbody></table></div></article>
|
||||
<article class="card"><div class="card-head"><div class="toolbar"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" aria-label="搜索事件" placeholder="事件编号、点位或规则"></div><select class="select" aria-label="严重度筛选"><option>全部严重度</option><option>S1 高危</option><option>S2 重要</option></select><select class="select" aria-label="结果筛选"><option>全部结果</option><option>已处置</option><option>误报</option></select></div><span class="muted">共 47 条</span></div><div class="table-wrap"><table><thead><tr><th>事件编号</th><th>时间</th><th>点位</th><th>规则</th><th>事实</th><th>预警</th><th>结果</th><th>证据</th></tr></thead><tbody id="eventRows"></tbody></table></div><div class="pager"><span id="eventPageSummary">第 1 / 16 页 · 每页 3 条</span><div class="pager-actions"><button class="btn" id="eventPrev">上一页</button><button class="btn" id="eventNext">下一页</button></div></div></article>
|
||||
</section>
|
||||
|
||||
<section class="view" data-page="rules">
|
||||
<div class="rule-context" id="ruleContext" hidden><p><strong>已带入摄像头与区域上下文:</strong><span id="ruleContextCamera">北围墙 01</span> · <span id="ruleContextZone">ZONE-007</span>,可继续完成规则草稿。</p><button class="btn btn-primary" id="continueRuleDraft">继续配置</button></div>
|
||||
<div class="page-head"><div><h1>规则策略</h1><p>Bell 管理场景、范围、时段、持续时间、试运行与发布;检测区域来自对应摄像头的版本化空间配置。</p></div><div class="page-actions"><button class="btn btn-primary" id="openRuleDialog">新建规则</button></div></div>
|
||||
<div class="grid rule-grid">
|
||||
<article class="card rule-card"><div class="rule-card-top"><span class="badge success">正式生效</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>非授权人员进入</h2><p>危险区域 × 全天 × 持续 3 秒</p><dl><dt>覆盖范围</dt><dd>4 个点位</dd><dt>检测区域</dt><dd>北围墙危险区域 v7</dd><dt>继承来源</dt><dd>站点模板</dd><dt>今日命中</dt><dd>8</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>编辑规则</button><a class="btn" href="../sense/index.html?view=device-detail&camera=CAM-S2-001&zone=ZONE-007&return_to=bell-rule">校准检测区域</a></div></article>
|
||||
<article class="card rule-card"><div class="rule-card-top"><span class="badge success" id="publishedRuleBadge">正式生效 · v7</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>非授权人员进入</h2><p>危险区域 × 全天 × 持续 3 秒</p><dl><dt>覆盖范围</dt><dd>4 个点位</dd><dt>检测区域</dt><dd>北围墙危险区域 v7</dd><dt>继承来源</dt><dd>站点模板</dd><dt>今日命中</dt><dd>8</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>编辑规则</button><button class="btn" id="openRollback">版本历史 / 回滚</button><a class="btn" href="../sense/index.html?view=device-detail&camera=CAM-S2-001&zone=ZONE-007&return_to=bell-rule">校准检测区域</a></div></article>
|
||||
<article class="card rule-card"><div class="rule-card-top"><span class="badge warning" id="trialBadge">试运行</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>区域内聚集</h2><p>人数 ≥ 6 × 持续 20 秒 × 夜间</p><dl><dt>覆盖范围</dt><dd>3 个点位</dd><dt>检测区域</dt><dd>3 个区域版本</dd><dt>继承来源</dt><dd>设备覆盖</dd><dt>样本命中</dt><dd>14</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>查看试运行</button><button class="btn btn-primary" id="publishTrialRule">正式发布</button></div></article>
|
||||
<article class="card rule-card"><div class="rule-card-top"><span class="badge violet">草稿</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>方向越线</h2><p>警戒线 A → B × 上学时段</p><dl><dt>覆盖范围</dt><dd>1 个点位</dd><dt>检测区域</dt><dd>宿舍东通道 · 警戒线 v3</dd><dt>继承来源</dt><dd>设备草稿</dd><dt>最近修改</dt><dd>2h 前</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>继续编辑</button><a class="btn" href="../sense/index.html?view=device-detail&camera=CAM-S2-011&zone=ZONE-011&return_to=bell-rule">查看警戒线</a></div></article>
|
||||
</div>
|
||||
@@ -404,6 +425,7 @@
|
||||
<section class="view" data-page="reports">
|
||||
<div class="page-head"><div><h1>运营报表</h1><p>算法效果和系统 SLA 分开统计;图表同时提供表格导出。</p></div><div class="page-actions"><button class="btn"><svg class="icon"><use href="#i-download"/></svg>导出 CSV</button></div></div>
|
||||
<div class="grid chart-grid"><article class="card chart"><h2>近 7 日预警量</h2><p>按站点日期汇总;可切换到每路每天误报数。</p><div class="bars" role="img" aria-label="近七日预警量依次为 18、22、15、31、27、24、19"><div class="bar" style="--h:58%"><span>周一</span></div><div class="bar" style="--h:70%"><span>周二</span></div><div class="bar" style="--h:48%"><span>周三</span></div><div class="bar" style="--h:100%"><span>周四</span></div><div class="bar" style="--h:87%"><span>周五</span></div><div class="bar" style="--h:77%"><span>周六</span></div><div class="bar" style="--h:61%"><span>周日</span></div></div></article><article class="card chart"><h2>处置结果</h2><p>本月已闭环事件。</p><div class="donut" role="img" aria-label="正常处置 61%,误报 22%,升级未闭环 17%"></div><div class="legend"><span><i style="background:var(--success)"></i>正常处置 61%</span><span><i style="background:var(--warning)"></i>误报 22%</span><span><i style="background:var(--danger)"></i>未闭环 17%</span></div></article></div>
|
||||
<article class="card" style="margin-top:12px"><div class="card-head"><div><h2>规则验收效果</h2><span class="muted">冻结样本窗口:2026-07-01~2026-07-31 · 现场复核集 v3</span></div><button class="btn">导出验收附件</button></div><div class="table-wrap"><table><thead><tr><th>规则版本</th><th>样本窗口</th><th>正样本 / 漏检</th><th>召回率</th><th>通道天数</th><th>误报数</th><th>每路每天误报数</th></tr></thead><tbody><tr><td>非授权人员进入 · v7</td><td>07-01~07-31</td><td>186 / 8</td><td><strong>95.7%</strong></td><td>496</td><td>19</td><td><strong>0.038</strong></td></tr><tr><td>区域内聚集 · v4 试运行</td><td>07-15~07-31</td><td>72 / 6</td><td><strong>91.7%</strong></td><td>272</td><td>16</td><td><strong>0.059</strong></td></tr></tbody></table></div><p class="metric-note"><strong>口径说明:</strong>按规则版本分别报告召回率与每路每天误报数;不提供跨场景统一“准确率”。样本量和冻结窗口随导出保留。</p></article>
|
||||
</section>
|
||||
|
||||
<section class="view" data-page="sites">
|
||||
@@ -416,9 +438,7 @@
|
||||
<section class="view" data-page="audit">
|
||||
<div class="context-strip" id="auditContext" hidden><p><strong>来自 Sense 设备日志:</strong>租户 <span id="auditTenant">—</span> · 站点 <span id="auditSite">—</span> · 设备 <span id="auditDevice">—</span>。已锁定筛选上下文。</p><a class="btn" id="auditReturn" href="../sense/index.html?view=device-detail">返回设备详情 ↗</a></div>
|
||||
<div class="page-head"><div><h1>审计日志</h1><p>查询、ack、规则发布和证据下载均可追溯;不记录密码与可复用连接串。</p></div><div class="page-actions"><button class="btn">导出审计记录</button></div></div>
|
||||
<article class="card"><div class="card-head"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" id="auditSearch" aria-label="搜索审计日志" placeholder="操作、角色或对象编号"></div><span class="badge info">只读</span></div><div class="table-wrap"><table><thead><tr><th>时间</th><th>角色</th><th>动作</th><th>对象</th><th>结果</th><th>来源</th></tr></thead><tbody id="auditRows">
|
||||
<tr><td>23:14:18</td><td>站点值班员</td><td>ACK_ALERT</td><td class="mono">ALT-1842</td><td><span class="badge success">成功</span></td><td>值班室 Web</td></tr><tr><td>22:52:06</td><td>站点管理员</td><td>PUBLISH_RULE</td><td class="mono">RULE-007</td><td><span class="badge success">成功</span></td><td>管理端</td></tr><tr><td>22:20:31</td><td>只读用户</td><td>VIEW_EVIDENCE</td><td class="mono">EVT-01J8…52C</td><td><span class="badge danger">拒绝</span></td><td>响应式 H5</td></tr>
|
||||
</tbody></table></div></article>
|
||||
<article class="card"><div class="card-head"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" id="auditSearch" aria-label="搜索审计日志" placeholder="操作、角色或对象编号"></div><span class="badge info">只读</span></div><div class="table-wrap"><table><thead><tr><th>时间</th><th>角色</th><th>动作</th><th>对象</th><th>结果</th><th>来源</th></tr></thead><tbody id="auditRows"></tbody></table></div><div class="pager"><span id="auditPageSummary">第 1 / 28 页 · 每页 3 条</span><div class="pager-actions"><button class="btn" id="auditPrev">上一页</button><button class="btn" id="auditNext">下一页</button></div></div></article>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
@@ -435,8 +455,9 @@
|
||||
<div class="form-field"><label for="ruleSchedule">生效时段</label><select id="ruleSchedule" class="select"><option>全天</option><option>夜间 22:00–06:00</option><option>自定义课表</option></select></div>
|
||||
<div class="form-field"><label for="ruleDuration">持续时间</label><select id="ruleDuration" class="select"><option>超过 3 秒</option><option>立即</option><option>超过 10 秒</option><option>超过 20 秒</option></select></div>
|
||||
<div class="form-field full"><label><input id="startDryRun" type="checkbox" checked aria-label="保存后开始试运行"> 保存后开始试运行,不触发正式预警</label></div>
|
||||
<div class="inline-alert danger form-field full" id="ruleConflict" role="alert" hidden><strong>规则已由周管理员发布为 v8。</strong><br>当前草稿基于 v7,不能覆盖。请刷新到 v8 后重新合并,或另存为新草稿。</div>
|
||||
<p class="workflow-note">产品流程:先确认摄像头和检测区域,再配置业务条件;试运行样本达到现场评审要求后,才允许正式发布。</p>
|
||||
</div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-primary" id="saveRuleDraft" value="default">保存并开始试运行</button></div></form></dialog>
|
||||
</div></div><div class="dialog-foot"><button type="button" class="btn" id="simulateRuleConflict">演示版本冲突</button><button class="btn" value="cancel">取消</button><button class="btn btn-primary" id="saveRuleDraft" value="default">保存并开始试运行</button></div></form></dialog>
|
||||
|
||||
<dialog id="falseDialog" aria-labelledby="falseTitle"><form method="dialog"><div class="dialog-head"><h2 id="falseTitle">标记为误报</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><p>这只会追加事件 outcome,不会修改原始事件事实和证据。</p><div class="form-grid"><div class="form-field full"><label for="falseReason">误报原因</label><select id="falseReason" class="select" required><option value="">请选择</option><option>区域配置不准确</option><option>目标分类错误</option><option>时段策略不准确</option><option>其他</option></select></div><div class="form-field full"><label for="falseNote">补充说明(可选)</label><textarea id="falseNote" class="textarea" placeholder="便于算法团队归因,不填写真实个人信息"></textarea></div><div class="form-field full"><label><input type="checkbox" checked> 进入标注复核队列</label></div></div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-danger" id="confirmFalse" value="default">确认标记</button></div></form></dialog>
|
||||
|
||||
@@ -445,78 +466,374 @@
|
||||
<dialog id="areaDialog" aria-labelledby="areaTitle"><form method="dialog"><div class="dialog-head"><h2 id="areaTitle">编辑 Area 准入策略</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><div class="form-grid"><div class="form-field"><label for="areaName">Area</label><input id="areaName" class="field" value="北围墙"></div><div class="form-field"><label for="areaParent">父级</label><select id="areaParent" class="select"><option>校园</option><option>教学区</option><option>宿舍楼</option></select></div><div class="form-field full"><label for="areaPolicy">capture_policy</label><select id="areaPolicy" class="select"><option value="video_allowed">允许成像设备</option><option value="non_imaging_only">仅允许非成像设备</option></select></div><div class="form-field full"><label for="areaReason">变更原因</label><textarea id="areaReason" class="textarea" placeholder="必填,进入全局审计"></textarea></div></div><p class="muted">保存生成新策略版本;Sense 投影同步前会显示旧版本,不允许绕过后端准入检查。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="saveAreaPolicy">保存策略版本</button></div></form></dialog>
|
||||
|
||||
<dialog id="policyConflictDialog" aria-labelledby="policyConflictTitle"><form method="dialog"><div class="dialog-head"><h2 id="policyConflictTitle">策略与已有设备冲突</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><p>北围墙当前有 4 台成像设备。改为“仅非成像”前必须选择显式处置;系统不会自动停用、移动或保留违规设备。</p><label class="radio-card"><input type="radio" name="conflictAction" value="move"> <strong>先迁移 4 台设备到允许成像的 Area</strong><br><span class="muted">创建迁移任务,全部成功后再发布策略。</span></label><label class="radio-card"><input type="radio" name="conflictAction" value="cancel" checked> <strong>取消本次策略变更</strong><br><span class="muted">保持 v42 和现有设备不变。</span></label></div><div class="dialog-foot"><button class="btn" value="cancel">返回编辑</button><button class="btn btn-primary" value="default" id="confirmPolicyConflict">确认选择</button></div></form></dialog>
|
||||
|
||||
<dialog id="manageDialog" aria-labelledby="manageTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="manageTitle">管理</h2><span class="muted">移动端保留 5 个主入口,低频管理能力从这里进入。</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭管理菜单">×</button></div><div class="dialog-body"><div class="relation-list"><button type="button" class="relation-item" data-manage-view="sites"><strong>站点与 Area</strong><span>配额、capture_policy、RBAC →</span></button><button type="button" class="relation-item" data-manage-view="audit"><strong>审计日志</strong><span>只读、筛选、分页 →</span></button></div></div></form></dialog>
|
||||
|
||||
<dialog id="eventDialog" aria-labelledby="eventDialogTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="eventDialogTitle">事件详情</h2><span class="muted" id="eventDialogSubtitle">不可变事实</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭事件详情">×</button></div><div class="dialog-body"><div class="fact-grid" style="margin:0 0 14px"><div class="fact"><span>事件编号</span><strong class="mono" id="eventDetailId">—</strong></div><div class="fact"><span>点位</span><strong id="eventDetailSite">—</strong></div><div class="fact"><span>规则</span><strong id="eventDetailRule">—</strong></div><div class="fact"><span>证据</span><strong id="eventDetailEvidence">—</strong></div></div><div class="relation-panel" style="margin:0"><h3>触发的预警</h3><div class="relation-list" id="eventAlerts"></div></div></div><div class="dialog-foot"><button class="btn" value="cancel">关闭</button></div></form></dialog>
|
||||
|
||||
<dialog id="handoverDialog" aria-labelledby="handoverTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="handoverTitle">值班交接</h2><span class="muted">白班 · 林值班员 → 晚班</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭交接班">×</button></div><div class="dialog-body"><div class="form-grid"><div class="form-field"><label for="handoverTo">接班人</label><select class="select" id="handoverTo"><option value="">请选择并确认本人已在场</option><option>王值班员 · 晚班</option><option>赵值班员 · 晚班</option></select></div><div class="form-field"><label for="handoverAt">交接时间</label><input class="field" id="handoverAt" value="2026-08-04 20:00" readonly></div><div class="form-field full"><label>待交接 Alert</label><div class="relation-list"><div class="relation-item"><strong>ALT-1842 · 未 ack</strong><span>下一次升级 01:42</span></div><div class="relation-item"><strong>ALT-1841 · 处置中</strong><span>王值班员现场核查</span></div><div class="relation-item"><strong>ALT-1839 · 升级中</strong><span>短信重试 2/3</span></div></div></div><div class="form-field full"><label for="handoverNote">交接备注</label><textarea class="textarea" id="handoverNote" placeholder="记录现场状态、已采取动作和待跟进事项"></textarea></div></div><p class="metric-note">交接期间升级链继续计时,不暂停、不重置。接班人确认前,当前责任关系保持不变。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="confirmHandover">接班人确认并留痕</button></div></form></dialog>
|
||||
|
||||
<dialog id="rollbackDialog" aria-labelledby="rollbackTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="rollbackTitle">版本历史与回滚</h2><span class="muted">RULE-007 · 历史版本不可变</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭版本历史">×</button></div><div class="dialog-body"><div class="version-diff"><article><strong>当前 v7 · 2026-08-01 正式发布</strong><span class="muted">持续 3 秒 · 全天 · 4 个点位 · 区域 v7</span></article><article><strong>目标 v6 · 2026-07-18 历史版本</strong><span class="muted">持续 5 秒 · 夜间 22:00–06:00 · 3 个点位 · 区域 v6</span></article><article><strong>影响评估</strong><span class="muted">将减少 1 个点位并收窄时段;预计每日预警 -18%。确认后创建并发布 v8,使其配置等价于 v6,v6/v7 均保持不变。</span></article></div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-danger" id="confirmRollback">创建并发布回滚版本 v8</button></div></form></dialog>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script>
|
||||
const $ = (selector, root = document) => root.querySelector(selector);
|
||||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const toast = $('#toast'); let toastTimer;
|
||||
function notify(message) { toast.textContent = message; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 3800); }
|
||||
const toast = $('#toast');
|
||||
let toastTimer;
|
||||
function notify(message) {
|
||||
toast.textContent = message;
|
||||
toast.classList.add('show');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => toast.classList.remove('show'), 3800);
|
||||
}
|
||||
|
||||
const alerts = [
|
||||
{id:'ALT-1842',severity:'S1 高危',badge:'danger',title:'危险区域闯入',site:'北围墙 01',time:'23:14:08',age:'1 分钟',rule:'非授权人员进入',delivery:'短信已送达'},
|
||||
{id:'ALT-1841',severity:'S1 高危',badge:'danger',title:'方向越线',site:'宿舍东通道',time:'23:12:46',age:'3 分钟',rule:'夜间越线 A → B',delivery:'声光已触发'},
|
||||
{id:'ALT-1839',severity:'S2 重要',badge:'warning',title:'区域聚集',site:'操场西北角',time:'23:08:21',age:'7 分钟',rule:'人数 ≥ 6 持续 20 秒',delivery:'Web 已看到'},
|
||||
{id:'ALT-1836',severity:'S2 重要',badge:'warning',title:'危险区域滞留',site:'配电房门口',time:'22:57:03',age:'18 分钟',rule:'区域内停留 ≥ 10 秒',delivery:'等待短信回执'}
|
||||
{id:'ALT-1842',severity:'S1 高危',badge:'danger',title:'危险区域闯入',site:'北围墙 01',time:'23:14:08',age:'1 分钟',rule:'非授权人员进入',delivery:'短信等待回执',events:[{id:'EVT-01J8…7A2',reason:'同点位 30 秒窗口聚合'},{id:'EVT-01J8…7B4',reason:'同规则连续命中聚合'}]},
|
||||
{id:'ALT-1841',severity:'S1 高危',badge:'danger',title:'方向越线',site:'宿舍东通道',time:'23:12:46',age:'3 分钟',rule:'夜间越线 A → B',delivery:'声光已看到',events:[{id:'EVT-01J8…71M',reason:'单事件触发'}]},
|
||||
{id:'ALT-1839',severity:'S2 重要',badge:'warning',title:'区域聚集',site:'操场西北角',time:'23:08:21',age:'7 分钟',rule:'人数 ≥ 6 持续 20 秒',delivery:'Web 已看到',events:[{id:'EVT-01J8…52C',reason:'同站点 60 秒窗口聚合'}]},
|
||||
{id:'ALT-1836',severity:'S2 重要',badge:'warning',title:'危险区域滞留',site:'配电房门口',time:'22:57:03',age:'18 分钟',rule:'区域内停留 ≥ 10 秒',delivery:'短信最终失败',events:[{id:'EVT-01J8…44K',reason:'单事件触发'}]}
|
||||
];
|
||||
const eventRecords = [
|
||||
{id:'EVT-01J8…7A2',time:'23:14:08',site:'北围墙 01',rule:'非授权人员进入',fact:'已确认',factBadge:'danger',result:'处置中',evidence:'可用',evidenceBadge:'success',alerts:[{id:'ALT-1842',state:'等待 ack · 聚合 2 个事件'}]},
|
||||
{id:'EVT-01J8…7B4',time:'23:14:21',site:'北围墙 01',rule:'非授权人员进入',fact:'已确认',factBadge:'danger',result:'已聚合',evidence:'可用',evidenceBadge:'success',alerts:[{id:'ALT-1842',state:'等待 ack · 同点位窗口聚合'}]},
|
||||
{id:'EVT-01J8…69P',time:'22:48:31',site:'宿舍东通道',rule:'区域内聚集',fact:'已记录',factBadge:'info',result:'试运行样本',evidence:'可用',evidenceBadge:'success',alerts:[]},
|
||||
{id:'EVT-01J8…52C',time:'22:19:04',site:'实验楼入口',rule:'危险区域闯入',fact:'已确认',factBadge:'danger',result:'误报',evidence:'重试中',evidenceBadge:'warning',alerts:[{id:'ALT-1839',state:'处置完成 · 误报'}]},
|
||||
{id:'EVT-01J8…71M',time:'21:58:20',site:'宿舍东通道',rule:'夜间越线 A → B',fact:'已确认',factBadge:'danger',result:'处置中',evidence:'可用',evidenceBadge:'success',alerts:[{id:'ALT-1841',state:'已 ack · 现场核查'}]},
|
||||
{id:'EVT-01J8…44K',time:'21:31:02',site:'配电房门口',rule:'区域内停留 ≥ 10 秒',fact:'已确认',factBadge:'warning',result:'升级中',evidence:'仅抓拍',evidenceBadge:'warning',alerts:[{id:'ALT-1836',state:'短信最终失败 · 语音待发送'}]}
|
||||
];
|
||||
const auditRecords = [
|
||||
{time:'23:14:18',role:'站点值班员',action:'ACK_ALERT',object:'ALT-1842',result:'成功',badge:'success',source:'值班室 Web'},
|
||||
{time:'22:52:06',role:'站点管理员',action:'PUBLISH_RULE',object:'RULE-007',result:'成功',badge:'success',source:'管理端'},
|
||||
{time:'22:20:31',role:'只读用户',action:'VIEW_EVIDENCE',object:'EVT-01J8…52C',result:'拒绝',badge:'danger',source:'响应式 H5'},
|
||||
{time:'21:56:04',role:'站点值班员',action:'HANDOVER_SHIFT',object:'SHIFT-20260804-DAY',result:'成功',badge:'success',source:'值班室 Web'},
|
||||
{time:'21:41:12',role:'租户管理员',action:'ROLLBACK_RULE',object:'RULE-007:v8',result:'成功',badge:'success',source:'管理端'},
|
||||
{time:'20:05:55',role:'站点管理员',action:'UPDATE_AREA_POLICY',object:'area-lab-gate:v41',result:'成功',badge:'success',source:'管理端'}
|
||||
];
|
||||
|
||||
function makeTextCell(row, value, className = '') {
|
||||
const cell = document.createElement('td');
|
||||
cell.textContent = value;
|
||||
if (className) cell.className = className;
|
||||
row.append(cell);
|
||||
return cell;
|
||||
}
|
||||
function makeBadgeCell(row, value, badge) {
|
||||
const cell = document.createElement('td');
|
||||
const span = document.createElement('span');
|
||||
span.className = `badge ${badge}`;
|
||||
span.textContent = value;
|
||||
cell.append(span);
|
||||
row.append(cell);
|
||||
}
|
||||
|
||||
let selectedAlertIndex = 0;
|
||||
let ackOwner = null;
|
||||
const alertList = $('#alertList');
|
||||
function renderAlerts(selected=0) { alertList.innerHTML = alerts.map((a,i)=>`<button class="alert-card ${i===selected?'selected':''}" data-index="${i}"><div class="alert-card-top"><span class="badge ${a.badge}"><i class="severity-dot"></i>${a.severity}</span><span class="muted">${a.age}</span></div><h3>${a.title}</h3><p>${a.site} · ${a.time}</p><div class="alert-card-foot"><span>${a.delivery}</span><span class="mono">${a.id}</span></div></button>`).join(''); $$('.alert-card', alertList).forEach(b=>b.addEventListener('click',()=>selectAlert(Number(b.dataset.index)))); }
|
||||
function selectAlert(index) { renderAlerts(index); const a=alerts[index]; $('#selectedSeverity').className=`badge ${a.badge}`; $('#selectedSeverity').textContent=a.severity; $('#selectedTitle').textContent=a.title; $('#selectedMeta').textContent=`${a.site} · ${a.time} · ${a.id}`; $('#factRule').textContent=a.rule; $('#ackState').className='badge warning'; $('#ackState').textContent='等待 ack'; $('#ackButton').disabled=false; $('#ackButton').innerHTML='<svg class="icon"><use href="#i-check"/></svg>确认接手'; $('#ackHelp').textContent='一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。'; }
|
||||
renderAlerts();
|
||||
function renderAlerts(selected = selectedAlertIndex) {
|
||||
alertList.innerHTML = alerts.map((alert, index) => `<button class="alert-card ${index === selected ? 'selected' : ''}" data-index="${index}"><div class="alert-card-top"><span class="badge ${alert.badge}"><i class="severity-dot"></i>${alert.severity}</span><span class="muted">${alert.age}</span></div><h3>${alert.title}</h3><p>${alert.site} · ${alert.time}</p><div class="alert-card-foot"><span>${alert.delivery}</span><span class="mono">${alert.id}</span></div></button>`).join('');
|
||||
$$('.alert-card', alertList).forEach(button => button.addEventListener('click', () => selectAlert(Number(button.dataset.index))));
|
||||
}
|
||||
function renderRelatedEvents(alert) {
|
||||
const container = $('#relatedEvents');
|
||||
container.replaceChildren();
|
||||
alert.events.forEach(event => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'relation-item';
|
||||
const strong = document.createElement('strong');
|
||||
strong.className = 'mono';
|
||||
strong.textContent = event.id;
|
||||
const reason = document.createElement('span');
|
||||
reason.textContent = `${event.reason} →`;
|
||||
button.append(strong, reason);
|
||||
button.addEventListener('click', () => showEventDetail(event.id));
|
||||
container.append(button);
|
||||
});
|
||||
$('#relatedEventCount').textContent = String(alert.events.length);
|
||||
}
|
||||
function resetAck() {
|
||||
ackOwner = null;
|
||||
$('#ackState').className = 'badge warning';
|
||||
$('#ackState').textContent = '等待 ack';
|
||||
$('#ackButton').disabled = false;
|
||||
$('#ackButton').innerHTML = '<svg class="icon"><use href="#i-check"/></svg>确认接手';
|
||||
$('#ackHelp').textContent = '一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。';
|
||||
$('#ackConflict').hidden = true;
|
||||
}
|
||||
function selectAlert(index) {
|
||||
selectedAlertIndex = index;
|
||||
renderAlerts(index);
|
||||
const alert = alerts[index];
|
||||
$('#selectedSeverity').className = `badge ${alert.badge}`;
|
||||
$('#selectedSeverity').textContent = alert.severity;
|
||||
$('#selectedTitle').textContent = alert.title;
|
||||
$('#selectedMeta').textContent = `${alert.site} · ${alert.time} · ${alert.id}`;
|
||||
$('#factRule').textContent = alert.rule;
|
||||
renderRelatedEvents(alert);
|
||||
resetAck();
|
||||
}
|
||||
function openAlert(alertId) {
|
||||
const index = alerts.findIndex(alert => alert.id === alertId);
|
||||
if (index < 0) {
|
||||
notify(`预警 ${alertId} 不在当前队列,请从历史预警查询。`);
|
||||
return;
|
||||
}
|
||||
if ($('#eventDialog').open) $('#eventDialog').close();
|
||||
switchView('duty');
|
||||
selectAlert(index);
|
||||
notify(`已从事件打开关联预警 ${alertId}。`);
|
||||
}
|
||||
|
||||
function switchView(name) { $$('.view').forEach(v=>v.classList.toggle('active',v.dataset.page===name)); $$('[data-view]').forEach(b=>{const on=b.dataset.view===name;b.toggleAttribute('aria-current',on);b.classList.toggle('active',on);}); $('#stateSelect').value='normal'; renderState('normal'); $('#main-content').focus({preventScroll:true}); }
|
||||
$$('[data-view]').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));
|
||||
$$('.queue-tabs button').forEach(b=>b.addEventListener('click',()=>{$$('.queue-tabs button').forEach(x=>{x.classList.toggle('active',x===b);x.setAttribute('aria-pressed',x===b)});notify(`队列已切换到“${b.textContent}”`);}));
|
||||
$$('.evidence-tabs button').forEach(b=>b.addEventListener('click',()=>{$$('.evidence-tabs button').forEach(x=>{x.classList.toggle('active',x===b);x.setAttribute('aria-pressed',x===b)});notify(`正在演示“${b.textContent}”标签结构`);}));
|
||||
function showEventDetail(eventId) {
|
||||
const event = eventRecords.find(item => item.id === eventId) || eventRecords[0];
|
||||
$('#eventDialogTitle').textContent = `${event.id} · 事件详情`;
|
||||
$('#eventDialogSubtitle').textContent = `${event.time} · 事实不可由处置改写`;
|
||||
$('#eventDetailId').textContent = event.id;
|
||||
$('#eventDetailSite').textContent = event.site;
|
||||
$('#eventDetailRule').textContent = event.rule;
|
||||
$('#eventDetailEvidence').textContent = event.evidence;
|
||||
const container = $('#eventAlerts');
|
||||
container.replaceChildren();
|
||||
if (!event.alerts.length) {
|
||||
const message = document.createElement('div');
|
||||
message.className = 'inline-alert';
|
||||
message.textContent = '未创建 Alert:当前为试运行样本或已被规则抑制;不伪装成投递成功。';
|
||||
container.append(message);
|
||||
} else {
|
||||
event.alerts.forEach(alert => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'relation-item';
|
||||
const strong = document.createElement('strong');
|
||||
strong.className = 'mono';
|
||||
strong.textContent = alert.id;
|
||||
const state = document.createElement('span');
|
||||
state.textContent = `${alert.state} →`;
|
||||
button.append(strong, state);
|
||||
button.addEventListener('click', () => openAlert(alert.id));
|
||||
container.append(button);
|
||||
});
|
||||
}
|
||||
$('#eventDialog').showModal();
|
||||
}
|
||||
|
||||
function renderState(state) { const layer=$('#stateLayer'), views=$$('.view'); layer.classList.toggle('show',state!=='normal'); views.forEach(v=>v.style.display=state==='normal'?'':'none'); if(state==='normal')return; const states={
|
||||
loading:`<div><div class="skeleton" aria-label="正在加载"><i></i><i></i><i></i><i></i></div><h2>正在读取最新预警状态</h2><p>超过 300ms 显示进度;ack 和升级事实始终从服务端重新读取。</p></div>`,
|
||||
weak:`<div><svg class="icon" style="color:var(--warning)"><use href="#i-alert"/></svg><h2>结构化事件已到达,视频证据暂不可用</h2><p>弱网下先显示时间、点位、规则和升级链。视频片段可单独重试,不阻塞确认接手。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal');notify('证据重试任务已加入后台队列')">后台重试证据</button></div>`,
|
||||
empty:`<div><svg class="icon"><use href="#i-check"/></svg><h2>当前没有待处置预警</h2><p>系统在线,新的 Alert 会实时进入队列。历史事件仍可从事件中心查询。</p><button class="btn" onclick="switchView('events')">查看事件中心</button></div>`,
|
||||
denied:`<div><svg class="icon" style="color:var(--danger)"><use href="#i-lock"/></svg><h2>没有查看此站点证据的权限</h2><p>当前账号只能查看被授权站点。系统不会泄露事件是否存在、流地址或设备信息。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal')">返回可访问范围</button></div>`}; layer.innerHTML=states[state]; }
|
||||
$('#stateSelect').addEventListener('change',e=>renderState(e.target.value));
|
||||
$('#themeToggle').addEventListener('click',()=>{const h=document.documentElement;h.dataset.theme=h.dataset.theme==='dark'?'light':'dark';notify(`已切换为${h.dataset.theme==='dark'?'深色':'浅色'}主题`);});
|
||||
let eventPage = 1;
|
||||
const eventTotalPages = 16;
|
||||
function renderEventPage() {
|
||||
const start = ((eventPage - 1) * 3) % eventRecords.length;
|
||||
const pageItems = [0, 1, 2].map(offset => eventRecords[(start + offset) % eventRecords.length]);
|
||||
const body = $('#eventRows');
|
||||
body.replaceChildren();
|
||||
pageItems.forEach(event => {
|
||||
const row = document.createElement('tr');
|
||||
const idCell = document.createElement('td');
|
||||
const eventButton = document.createElement('button');
|
||||
eventButton.type = 'button';
|
||||
eventButton.className = 'text-button mono';
|
||||
eventButton.textContent = event.id;
|
||||
eventButton.addEventListener('click', () => showEventDetail(event.id));
|
||||
idCell.append(eventButton);
|
||||
row.append(idCell);
|
||||
makeTextCell(row, event.time);
|
||||
makeTextCell(row, event.site);
|
||||
makeTextCell(row, event.rule);
|
||||
makeBadgeCell(row, event.fact, event.factBadge);
|
||||
const alertCell = document.createElement('td');
|
||||
if (event.alerts.length) {
|
||||
event.alerts.forEach(alert => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'text-button mono';
|
||||
button.textContent = alert.id;
|
||||
button.addEventListener('click', () => openAlert(alert.id));
|
||||
alertCell.append(button);
|
||||
});
|
||||
} else {
|
||||
alertCell.textContent = '未触发';
|
||||
}
|
||||
row.append(alertCell);
|
||||
makeTextCell(row, event.result);
|
||||
makeBadgeCell(row, event.evidence, event.evidenceBadge);
|
||||
body.append(row);
|
||||
});
|
||||
$('#eventPageSummary').textContent = `第 ${eventPage} / ${eventTotalPages} 页 · 每页 3 条 · 共 47 条`;
|
||||
$('#eventPrev').disabled = eventPage === 1;
|
||||
$('#eventNext').disabled = eventPage === eventTotalPages;
|
||||
}
|
||||
|
||||
$('#ackButton').addEventListener('click',async e=>{const b=e.currentTarget;b.disabled=true;b.textContent='确认中…';await new Promise(r=>setTimeout(r,650));b.innerHTML='<svg class="icon"><use href="#i-check"/></svg>已由当前值班员接手';$('#ackState').className='badge success';$('#ackState').textContent='已 ack';$('#ackHelp').textContent='23:15:02 · 当前值班员 · 服务端确认成功';notify('确认成功,升级计时已停止并记录审计。');});
|
||||
$('#completeAlert').addEventListener('click',()=>{if(!$('#outcome').value){notify('请先选择处置结果。');$('#outcome').focus();return;}if($('#ackState').textContent!=='已 ack'){notify('请先确认接手,再完成处置。');$('#ackButton').focus();return;}notify('处置结果已追加,原始事件事实未被修改。');});
|
||||
$('#falsePositive').addEventListener('click',()=>$('#falseDialog').showModal());
|
||||
$('#confirmFalse').addEventListener('click',e=>{if(!$('#falseReason').value){e.preventDefault();notify('请选择误报原因。');$('#falseReason').focus();return;}notify('已标记误报并进入复核队列,原始事件保持不变。');});
|
||||
$('#muteButton').addEventListener('click',()=>$('#muteDialog').showModal());
|
||||
$('#confirmMute').addEventListener('click',e=>{if(!$('#muteCause').value.trim()){e.preventDefault();notify('请填写静默原因。');$('#muteCause').focus();return;}notify('已创建限时静默,到期将自动恢复并通知值班组。');});
|
||||
let auditPage = 1;
|
||||
const auditTotalPages = 28;
|
||||
function renderAuditPage() {
|
||||
const start = ((auditPage - 1) * 3) % auditRecords.length;
|
||||
const pageItems = [0, 1, 2].map(offset => auditRecords[(start + offset) % auditRecords.length]);
|
||||
const body = $('#auditRows');
|
||||
body.replaceChildren();
|
||||
pageItems.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
makeTextCell(row, item.time);
|
||||
makeTextCell(row, item.role);
|
||||
makeTextCell(row, item.action);
|
||||
makeTextCell(row, item.object, 'mono');
|
||||
makeBadgeCell(row, item.result, item.badge);
|
||||
makeTextCell(row, item.source);
|
||||
body.append(row);
|
||||
});
|
||||
$('#auditPageSummary').textContent = `第 ${auditPage} / ${auditTotalPages} 页 · 每页 3 条 · 共 84 条`;
|
||||
$('#auditPrev').disabled = auditPage === 1;
|
||||
$('#auditNext').disabled = auditPage === auditTotalPages;
|
||||
}
|
||||
|
||||
$$('[data-edit-area]').forEach(button=>button.addEventListener('click',()=>{const isDorm=button.dataset.editArea==='dorm';$('#areaName').value=isDorm?'宿舍卧室':button.dataset.editArea==='lab'?'实验楼入口':'北围墙';$('#areaPolicy').value=isDorm?'non_imaging_only':'video_allowed';$('#areaDialog').showModal();}));
|
||||
$('#newArea').addEventListener('click',()=>{$('#areaName').value='';$('#areaPolicy').value='video_allowed';$('#areaDialog').showModal();});
|
||||
$('#newSite').addEventListener('click',()=>notify('新建站点需先填写时区、场景包、配额和管理员,本原型只枚举入口。'));
|
||||
$('#saveAreaPolicy').addEventListener('click',()=>{if(!$('#areaReason').value.trim()){notify('请填写变更原因。');$('#areaReason').focus();return;}if($('#areaName').value==='北围墙'&&$('#areaPolicy').value==='non_imaging_only'){$('#areaDialog').close();$('#policyConflictDialog').showModal();return;}$('#areaDialog').close();notify('Area 策略新版本已保存,等待 Sense 拉取投影。');});
|
||||
$('#confirmPolicyConflict').addEventListener('click',()=>{const action=$('input[name="conflictAction"]:checked').value;notify(action==='move'?'已创建 4 台设备迁移任务;完成前不会发布策略。':'已取消策略变更,v42 和已有设备保持不变。');});
|
||||
const validViews = new Set($$('.view').map(view => view.dataset.page));
|
||||
function switchView(requested) {
|
||||
const name = validViews.has(requested) ? requested : 'duty';
|
||||
$$('.view').forEach(view => view.classList.toggle('active', view.dataset.page === name));
|
||||
$$('[data-view]').forEach(button => {
|
||||
const active = button.dataset.view === name;
|
||||
button.toggleAttribute('aria-current', active);
|
||||
button.classList.toggle('active', active);
|
||||
});
|
||||
$('#stateSelect').value = 'normal';
|
||||
renderState('normal');
|
||||
$('#main-content').focus({preventScroll:true});
|
||||
}
|
||||
$$('[data-view]').forEach(button => button.addEventListener('click', () => switchView(button.dataset.view)));
|
||||
$$('.queue-tabs button').forEach(button => button.addEventListener('click', () => {
|
||||
$$('.queue-tabs button').forEach(item => { const active = item === button; item.classList.toggle('active', active); item.setAttribute('aria-pressed', String(active)); });
|
||||
notify(`队列已切换到“${button.textContent}”`);
|
||||
}));
|
||||
$$('.evidence-tabs button').forEach(button => button.addEventListener('click', () => {
|
||||
$$('.evidence-tabs button').forEach(item => { const active = item === button; item.classList.toggle('active', active); item.setAttribute('aria-pressed', String(active)); });
|
||||
notify(`正在演示“${button.textContent}”标签结构`);
|
||||
}));
|
||||
|
||||
function renderState(state) {
|
||||
const layer = $('#stateLayer');
|
||||
const views = $$('.view');
|
||||
layer.classList.toggle('show', state !== 'normal');
|
||||
views.forEach(view => { view.style.display = state === 'normal' ? '' : 'none'; });
|
||||
if (state === 'normal') return;
|
||||
const states = {
|
||||
loading:'<div><div class="skeleton" aria-label="正在加载"><i></i><i></i><i></i><i></i></div><h2>正在读取最新预警状态</h2><p>超过 300ms 显示进度;ack 和升级事实始终从服务端重新读取。</p></div>',
|
||||
weak:'<div><svg class="icon" style="color:var(--warning)"><use href="#i-alert"/></svg><h2>结构化事件已到达,视频证据暂不可用</h2><p>弱网下先显示时间、点位、规则和升级链。视频片段可单独重试,不阻塞确认接手。</p><button class="btn" data-state-recover="weak">后台重试证据</button></div>',
|
||||
empty:'<div><svg class="icon"><use href="#i-check"/></svg><h2>当前没有待处置预警</h2><p>系统在线,新的 Alert 会实时进入队列。历史事件仍可从事件中心查询。</p><button class="btn" data-empty-events>查看事件中心</button></div>',
|
||||
denied:'<div><svg class="icon" style="color:var(--danger)"><use href="#i-lock"/></svg><h2>没有查看此站点证据的权限</h2><p>当前账号只能查看被授权站点。系统不会泄露事件是否存在、流地址或设备信息。</p><button class="btn" data-state-recover="denied">返回可访问范围</button></div>',
|
||||
expired:'<div><svg class="icon" style="color:var(--danger)"><use href="#i-lock"/></svg><h2>会话已过期</h2><p>为保护预警和证据,页面已停止提交 ack 与处置。重新登录后从服务端读取最新责任人和升级状态,不重放旧操作。</p><button class="btn" data-state-recover="expired">重新登录并刷新状态</button></div>'
|
||||
};
|
||||
layer.innerHTML = states[state];
|
||||
$('[data-state-recover]', layer)?.addEventListener('click', () => { $('#stateSelect').value = 'normal'; renderState('normal'); notify(state === 'weak' ? '证据重试任务已加入后台队列。' : '已刷新当前权限与服务端状态。'); });
|
||||
$('[data-empty-events]', layer)?.addEventListener('click', () => switchView('events'));
|
||||
}
|
||||
|
||||
$('#stateSelect').addEventListener('change', event => renderState(event.target.value));
|
||||
$('#themeToggle').addEventListener('click', () => { const root = document.documentElement; root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark'; notify(`已切换为${root.dataset.theme === 'dark' ? '深色' : '浅色'}主题`); });
|
||||
$('#eventPrev').addEventListener('click', () => { if (eventPage > 1) { eventPage -= 1; renderEventPage(); } });
|
||||
$('#eventNext').addEventListener('click', () => { if (eventPage < eventTotalPages) { eventPage += 1; renderEventPage(); } });
|
||||
$('#auditPrev').addEventListener('click', () => { if (auditPage > 1) { auditPage -= 1; renderAuditPage(); } });
|
||||
$('#auditNext').addEventListener('click', () => { if (auditPage < auditTotalPages) { auditPage += 1; renderAuditPage(); } });
|
||||
|
||||
$('#ackButton').addEventListener('click', async event => {
|
||||
const button = event.currentTarget;
|
||||
button.disabled = true;
|
||||
button.textContent = '确认中…';
|
||||
await new Promise(resolve => setTimeout(resolve, 650));
|
||||
if (ackOwner && ackOwner !== 'current') {
|
||||
button.textContent = '当前预警已由他人接手';
|
||||
$('#ackConflict').hidden = false;
|
||||
$('#ackConflict').textContent = '确认未生效:王值班员已于 23:15:01 抢先 ack。已刷新为服务端结果,没有覆盖对方。';
|
||||
notify('并发确认未覆盖:请与当前处置人协作。');
|
||||
return;
|
||||
}
|
||||
ackOwner = 'current';
|
||||
button.innerHTML = '<svg class="icon"><use href="#i-check"/></svg>已由当前值班员接手';
|
||||
$('#ackState').className = 'badge success';
|
||||
$('#ackState').textContent = '已 ack';
|
||||
$('#ackHelp').textContent = '23:15:02 · 当前值班员 · 服务端确认成功;通道回执仍单独保留。';
|
||||
notify('确认成功,升级计时已停止并记录审计。');
|
||||
});
|
||||
$('#simulateAckConflict').addEventListener('click', () => {
|
||||
ackOwner = 'other';
|
||||
$('#ackButton').disabled = true;
|
||||
$('#ackButton').textContent = '已由王值班员接手';
|
||||
$('#ackState').className = 'badge info';
|
||||
$('#ackState').textContent = '已 ack · 他人';
|
||||
$('#ackHelp').textContent = '23:15:01 · 王值班员 · 服务端首个成功结果';
|
||||
$('#ackConflict').hidden = false;
|
||||
$('#ackConflict').textContent = '你的 ack 未生效:王值班员抢先成功。页面已刷新实际处置人,没有静默覆盖或显示双成功。';
|
||||
});
|
||||
$('#completeAlert').addEventListener('click', () => {
|
||||
if (!$('#outcome').value) { notify('请先选择处置结果。'); $('#outcome').focus(); return; }
|
||||
if (ackOwner !== 'current') { notify(ackOwner === 'other' ? '当前处置人为王值班员,你不能覆盖其处置结果。' : '请先确认接手,再完成处置。'); $('#ackButton').focus(); return; }
|
||||
notify('处置结果已追加,原始事件事实未被修改。');
|
||||
});
|
||||
$('#falsePositive').addEventListener('click', () => $('#falseDialog').showModal());
|
||||
$('#confirmFalse').addEventListener('click', event => { if (!$('#falseReason').value) { event.preventDefault(); notify('请选择误报原因。'); $('#falseReason').focus(); return; } notify('已标记误报并进入复核队列,原始事件保持不变。'); });
|
||||
$('#muteButton').addEventListener('click', () => $('#muteDialog').showModal());
|
||||
$('#confirmMute').addEventListener('click', event => { if (!$('#muteCause').value.trim()) { event.preventDefault(); notify('请填写静默原因。'); $('#muteCause').focus(); return; } notify('已创建限时静默,到期将自动恢复并通知值班组。'); });
|
||||
$('#handoverButton').addEventListener('click', () => $('#handoverDialog').showModal());
|
||||
$('#confirmHandover').addEventListener('click', () => { if (!$('#handoverTo').value) { notify('请选择接班人并由本人确认。'); $('#handoverTo').focus(); return; } $('#handoverDialog').close(); notify('交接已由接班人确认并写入审计;3 条 Alert 的升级链继续运行。'); });
|
||||
|
||||
$('#mobileManage').addEventListener('click', () => $('#manageDialog').showModal());
|
||||
$$('[data-manage-view]').forEach(button => button.addEventListener('click', () => { $('#manageDialog').close(); switchView(button.dataset.manageView); notify(`已进入${button.textContent.trim()}。`); }));
|
||||
|
||||
$$('[data-edit-area]').forEach(button => button.addEventListener('click', () => { const isDorm = button.dataset.editArea === 'dorm'; $('#areaName').value = isDorm ? '宿舍卧室' : button.dataset.editArea === 'lab' ? '实验楼入口' : '北围墙'; $('#areaPolicy').value = isDorm ? 'non_imaging_only' : 'video_allowed'; $('#areaDialog').showModal(); }));
|
||||
$('#newArea').addEventListener('click', () => { $('#areaName').value = ''; $('#areaPolicy').value = 'video_allowed'; $('#areaDialog').showModal(); });
|
||||
$('#newSite').addEventListener('click', () => notify('新建站点需先填写时区、场景包、配额和管理员,本原型只枚举入口。'));
|
||||
$('#saveAreaPolicy').addEventListener('click', () => { if (!$('#areaReason').value.trim()) { notify('请填写变更原因。'); $('#areaReason').focus(); return; } if ($('#areaName').value === '北围墙' && $('#areaPolicy').value === 'non_imaging_only') { $('#areaDialog').close(); $('#policyConflictDialog').showModal(); return; } $('#areaDialog').close(); notify('Area 策略新版本已保存,等待 Sense 拉取投影。'); });
|
||||
$('#confirmPolicyConflict').addEventListener('click', () => { const action = $('input[name="conflictAction"]:checked').value; notify(action === 'move' ? '已创建 4 台设备迁移任务;完成前不会发布策略。' : '已取消策略变更,v42 和已有设备保持不变。'); });
|
||||
|
||||
const ruleDialog = $('#ruleDialog');
|
||||
function openRuleEditor() { ruleDialog.showModal(); }
|
||||
$('#openRuleDialog').addEventListener('click', openRuleEditor);
|
||||
$('#continueRuleDraft').addEventListener('click', openRuleEditor);
|
||||
$$('[data-edit-rule]').forEach(button=>button.addEventListener('click', openRuleEditor));
|
||||
function syncSenseZoneLink(){ $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent($('#ruleCamera').value)}&zone=${encodeURIComponent($('#ruleZone').value)}&return_to=bell-rule`; }
|
||||
$('#ruleCamera').addEventListener('change',()=>{ syncSenseZoneLink(); notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); });
|
||||
$('#ruleZone').addEventListener('change',syncSenseZoneLink);
|
||||
$('#saveRuleDraft').addEventListener('click',()=>notify($('#startDryRun').checked?'规则草稿已保存并进入试运行,不会触发正式预警。':'规则草稿已保存,尚未开始试运行。'));
|
||||
$('#publishTrialRule').addEventListener('click',()=>{if(confirm('正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?')){$('#trialBadge').className='badge success';$('#trialBadge').textContent='正式生效';notify('规则版本已正式发布,变更已记录审计并可回滚。');}});
|
||||
$$('[data-edit-rule]').forEach(button => button.addEventListener('click', openRuleEditor));
|
||||
function syncSenseZoneLink() { $('#editRuleZone').href = `../sense/index.html?view=device-detail&camera=${encodeURIComponent($('#ruleCamera').value)}&zone=${encodeURIComponent($('#ruleZone').value)}&return_to=bell-rule`; }
|
||||
$('#ruleCamera').addEventListener('change', () => { syncSenseZoneLink(); notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); });
|
||||
$('#ruleZone').addEventListener('change', syncSenseZoneLink);
|
||||
$('#simulateRuleConflict').addEventListener('click', () => { $('#ruleConflict').hidden = false; $('#saveRuleDraft').disabled = true; notify('已读取服务端 v8;当前 v7 草稿被阻止覆盖。'); });
|
||||
$('#saveRuleDraft').addEventListener('click', event => { if (!$('#ruleConflict').hidden) { event.preventDefault(); notify('版本冲突未解决,不能覆盖 v8。'); return; } notify($('#startDryRun').checked ? '规则草稿已保存并进入试运行,不会触发正式预警。' : '规则草稿已保存,尚未开始试运行。'); });
|
||||
$('#publishTrialRule').addEventListener('click', () => { if (confirm('正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?')) { $('#trialBadge').className = 'badge success'; $('#trialBadge').textContent = '正式生效'; notify('规则版本已正式发布,变更已记录审计并可回滚。'); } });
|
||||
$('#openRollback').addEventListener('click', () => $('#rollbackDialog').showModal());
|
||||
$('#confirmRollback').addEventListener('click', () => { $('#rollbackDialog').close(); $('#publishedRuleBadge').textContent = '正式生效 · v8(回滚)'; notify('已创建并发布 v8,其配置等价于 v6;v6、v7 历史保持不变。'); });
|
||||
|
||||
let remain = 102;
|
||||
setInterval(() => { if (ackOwner || remain <= 0) return; remain -= 1; const minutes = String(Math.floor(remain / 60)).padStart(2, '0'); const seconds = String(remain % 60).padStart(2, '0'); $('#countdown').textContent = `${minutes}:${seconds}`; }, 1000);
|
||||
|
||||
renderAlerts();
|
||||
renderRelatedEvents(alerts[0]);
|
||||
renderEventPage();
|
||||
renderAuditPage();
|
||||
|
||||
let remain=102;
|
||||
setInterval(()=>{if($('#ackState').textContent==='已 ack'||remain<=0)return;remain--;const m=String(Math.floor(remain/60)).padStart(2,'0'),s=String(remain%60).padStart(2,'0');$('#countdown').textContent=`${m}:${s}`;},1000);
|
||||
const routeParams = new URLSearchParams(location.search);
|
||||
if (routeParams.get('view')) switchView(routeParams.get('view'));
|
||||
if (routeParams.get('view')==='audit' && (routeParams.has('device')||routeParams.has('site'))) {
|
||||
const tenant=routeParams.get('tenant')||'当前租户',site=routeParams.get('site')||'当前站点',device=routeParams.get('device')||'全部设备';
|
||||
$('#auditContext').hidden=false;$('#auditTenant').textContent=tenant;$('#auditSite').textContent=site;$('#auditDevice').textContent=device;$('#auditSearch').value=device;
|
||||
$('#auditRows').insertAdjacentHTML('afterbegin',`<tr><td>10:47:36</td><td>站点管理员</td><td>PAUSE_INFERENCE</td><td class="mono">${device}</td><td><span class="badge success">成功</span></td><td>Sense outbox · relay 已送达</td></tr>`);
|
||||
$('#auditReturn').href=`../sense/index.html?view=device-detail&device=${encodeURIComponent(device)}&return_to=bell-audit`;
|
||||
notify('已应用来自 Sense 的租户、站点和设备审计筛选。');
|
||||
if (routeParams.get('view') === 'audit' && (routeParams.has('device') || routeParams.has('site'))) {
|
||||
const tenant = routeParams.get('tenant') || '当前租户';
|
||||
const site = routeParams.get('site') || '当前站点';
|
||||
const device = routeParams.get('device') || '全部设备';
|
||||
$('#auditContext').hidden = false;
|
||||
$('#auditTenant').textContent = tenant;
|
||||
$('#auditSite').textContent = site;
|
||||
$('#auditDevice').textContent = device;
|
||||
$('#auditSearch').value = device;
|
||||
auditRecords.unshift({time:'10:47:36',role:'站点管理员',action:'PAUSE_INFERENCE',object:device,result:'成功',badge:'success',source:'Sense outbox · relay 已送达'});
|
||||
renderAuditPage();
|
||||
$('#auditReturn').href = `../sense/index.html?view=device-detail&device=${encodeURIComponent(device)}&return_to=bell-audit`;
|
||||
notify('已安全应用来自 Sense 的租户、站点和设备审计筛选。');
|
||||
}
|
||||
if (routeParams.has('event')) {
|
||||
switchView('events');
|
||||
showEventDetail(routeParams.get('event'));
|
||||
}
|
||||
if (routeParams.has('alert')) openAlert(routeParams.get('alert'));
|
||||
if (routeParams.has('camera')) {
|
||||
switchView('rules'); $('#ruleContext').hidden=false;
|
||||
const camera=routeParams.get('camera'); if([...$('#ruleCamera').options].some(option=>option.value===camera)) $('#ruleCamera').value=camera;
|
||||
const zone=routeParams.get('zone'); if(zone&&[...$('#ruleZone').options].some(option=>option.value===zone)) $('#ruleZone').value=zone;
|
||||
$('#ruleContextCamera').textContent=$('#ruleCamera').selectedOptions[0].textContent; $('#ruleContextZone').textContent=zone||$('#ruleZone').value;
|
||||
switchView('rules');
|
||||
$('#ruleContext').hidden = false;
|
||||
const camera = routeParams.get('camera');
|
||||
if ([...$('#ruleCamera').options].some(option => option.value === camera)) $('#ruleCamera').value = camera;
|
||||
const zone = routeParams.get('zone');
|
||||
if (zone && [...$('#ruleZone').options].some(option => option.value === zone)) $('#ruleZone').value = zone;
|
||||
$('#ruleContextCamera').textContent = $('#ruleCamera').selectedOptions[0].textContent;
|
||||
$('#ruleContextZone').textContent = zone || $('#ruleZone').value;
|
||||
syncSenseZoneLink();
|
||||
notify('已保留摄像头与检测区域上下文,可继续配置规则。');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user