Files
yovision/docs/design/bell/index.html
T
ila 5251199205
Harness governance / validate (push) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
docs(design): update Bell workflow
2026-08-04 09:04:52 +08:00

341 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark light">
<link rel="icon" href="data:,">
<title>YoVision Bell · 预警处置原型</title>
<style>
:root {
--bg: #090d18;
--surface: #101725;
--surface-2: #151e2e;
--surface-3: #1b2638;
--text: #f5f7fb;
--muted: #a3b0c2;
--subtle: #73839a;
--line: #29364a;
--primary: #8b7cf6;
--primary-strong: #7161e8;
--primary-soft: rgba(139, 124, 246, .14);
--cyan: #38bdf8;
--success: #34d399;
--warning: #fbbf24;
--danger: #fb7185;
--shadow: 0 18px 46px rgba(0, 0, 0, .25);
--radius: 14px;
--sidebar: 216px;
--banner: 28px;
}
html[data-theme="light"] {
--bg: #f1f4f8;
--surface: #ffffff;
--surface-2: #f8fafc;
--surface-3: #eaf0f6;
--text: #131d2c;
--muted: #52647a;
--subtle: #738096;
--line: #d2dbe6;
--primary-soft: rgba(113, 97, 232, .1);
--shadow: 0 14px 34px rgba(32, 52, 78, .12);
}
* { box-sizing: border-box; }
html { min-width: 320px; background: var(--bg); }
body { margin: 0; min-height: 100dvh; background: var(--bg); color: var(--text); font: 14px/1.5 Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
button, input, select, textarea { font: inherit; }
button, select, input[type="checkbox"], input[type="radio"] { cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible { outline: 3px solid rgba(139, 124, 246, .45); outline-offset: 2px; }
.prototype-banner { position: fixed; z-index: 1000; inset: 0 0 auto 0; height: var(--banner); display: flex; align-items: center; justify-content: center; background: #f6c344; color: #211900; font-size: 12px; font-weight: 800; letter-spacing: .07em; }
.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto; }
.app { min-height: 100dvh; padding-top: var(--banner); display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); }
.sidebar { position: sticky; top: var(--banner); z-index: 30; height: calc(100dvh - var(--banner)); padding: 18px 12px; border-right: 1px solid var(--line); background: var(--surface); display: flex; flex-direction: column; }
.brand { display: flex; align-items: center; gap: 10px; padding: 2px 10px 20px; }
.brand-mark { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 10px; background: linear-gradient(145deg, #8b7cf6, #38bdf8); color: white; font-weight: 900; box-shadow: 0 8px 22px rgba(113, 97, 232, .25); }
.brand strong { display: block; font-size: 16px; }
.brand small { color: var(--muted); }
.nav-label { margin: 8px 10px 6px; color: var(--subtle); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .12em; }
.nav { display: grid; gap: 4px; }
.nav button { min-height: 44px; padding: 0 12px; border: 0; border-radius: 10px; background: transparent; color: var(--muted); display: flex; align-items: center; gap: 10px; text-align: left; transition: .2s ease; }
.nav button:hover { color: var(--text); background: var(--surface-2); }
.nav button[aria-current="page"] { color: var(--text); background: var(--primary-soft); box-shadow: inset 3px 0 var(--primary); }
.nav .count { margin-left: auto; min-width: 22px; padding: 1px 6px; border-radius: 999px; background: var(--surface-3); color: var(--text); text-align: center; font-size: 12px; }
.shift-card { margin-top: auto; padding: 12px; border: 1px solid var(--line); border-radius: 11px; background: var(--surface-2); }
.shift-card strong { display: block; }
.shift-card span { color: var(--muted); font-size: 12px; }
.shift-bar { margin-top: 9px; height: 5px; border-radius: 99px; background: var(--surface-3); overflow: hidden; }
.shift-bar i { display: block; width: 63%; height: 100%; background: var(--success); }
.main { min-width: 0; }
.topbar { position: sticky; top: var(--banner); z-index: 20; min-height: 64px; padding: 10px 20px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--surface) 94%, transparent); backdrop-filter: blur(12px); }
.site strong { display: block; font-size: 15px; }
.site span { color: var(--muted); font-size: 12px; }
.top-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.btn { min-height: 40px; border: 1px solid var(--line); border-radius: 9px; padding: 8px 13px; background: var(--surface-2); color: var(--text); display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 750; text-decoration: none; transition: .2s ease; }
.btn:hover { border-color: var(--primary); }
.btn:disabled { cursor: not-allowed; opacity: .5; }
.btn-primary { background: var(--primary-strong); border-color: var(--primary-strong); color: white; }
.btn-success { background: #117a5d; border-color: #159271; color: white; }
.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; }
.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; }
.content { padding: 18px; }
.view { display: none; animation: reveal .22s ease-out; }
.view.active { display: block; }
@keyframes reveal { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.page-head { margin-bottom: 15px; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.page-head h1 { margin: 0 0 4px; font-size: 24px; line-height: 1.25; }
.page-head p { margin: 0; color: var(--muted); }
.page-actions, .toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
.card-head { min-height: 52px; padding: 11px 14px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.card-head h2, .card-head h3 { margin: 0; font-size: 15px; }
.card-body { padding: 14px; }
.badge { display: inline-flex; align-items: center; gap: 5px; min-height: 24px; padding: 2px 8px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); font-size: 12px; white-space: nowrap; }
.badge.success { color: #78edc2; border-color: rgba(52,211,153,.34); background: rgba(52,211,153,.1); }
.badge.warning { color: #ffe18a; border-color: rgba(251,191,36,.36); background: rgba(251,191,36,.1); }
.badge.danger { color: #ff9eb0; border-color: rgba(251,113,133,.4); background: rgba(251,113,133,.1); }
.badge.info { color: #a5e2ff; border-color: rgba(56,189,248,.35); background: rgba(56,189,248,.1); }
.badge.violet { color: #c9c1ff; border-color: rgba(139,124,246,.38); background: var(--primary-soft); }
.duty-grid { display: grid; grid-template-columns: 320px minmax(420px, 1fr) 330px; gap: 12px; min-height: calc(100dvh - 156px); }
.queue, .evidence, .action-panel { min-width: 0; overflow: hidden; }
.queue-head { padding: 10px; border-bottom: 1px solid var(--line); }
.queue-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; padding: 4px; border-radius: 9px; background: var(--surface-2); }
.queue-tabs button { min-height: 34px; border: 0; border-radius: 7px; background: transparent; color: var(--muted); }
.queue-tabs button.active { background: var(--surface-3); color: var(--text); }
.alert-list { padding: 8px; display: grid; gap: 7px; max-height: calc(100dvh - 265px); overflow-y: auto; }
.alert-card { width: 100%; padding: 11px; border: 1px solid transparent; border-radius: 10px; background: transparent; color: var(--text); text-align: left; transition: .18s ease; }
.alert-card:hover { background: var(--surface-2); }
.alert-card.selected { background: var(--primary-soft); border-color: rgba(139,124,246,.45); }
.alert-card-top, .alert-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.alert-card h3 { margin: 8px 0 3px; font-size: 14px; }
.alert-card p { margin: 0; color: var(--muted); font-size: 12px; }
.alert-card-foot { margin-top: 9px; color: var(--subtle); font-size: 12px; }
.severity-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.evidence-summary { padding: 12px 14px; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--line); }
.evidence-summary h2 { margin: 0 0 5px; font-size: 18px; }
.evidence-summary p { margin: 0; color: var(--muted); }
.evidence-tabs { display: flex; gap: 4px; padding: 8px 12px 0; }
.evidence-tabs button { min-height: 36px; padding: 0 12px; border: 0; border-radius: 8px 8px 0 0; background: transparent; color: var(--muted); }
.evidence-tabs button.active { background: var(--surface-2); color: var(--text); }
.video-stage { position: relative; aspect-ratio: 16/9; margin: 0 12px 12px; background: #02060d; overflow: hidden; border: 1px solid var(--line); border-radius: 10px; }
.video-stage svg { width: 100%; height: 100%; display: block; }
.video-controls { position: absolute; inset: auto 0 0; min-height: 44px; padding: 6px 10px; display: flex; align-items: center; gap: 9px; color: white; background: linear-gradient(transparent, rgba(0,0,0,.88)); }
.video-controls button { width: 34px; height: 34px; border: 0; border-radius: 7px; background: rgba(255,255,255,.08); color: white; }
.timeline-bar { flex: 1; height: 5px; border-radius: 99px; background: rgba(255,255,255,.22); overflow: hidden; }
.timeline-bar i { display: block; width: 54%; height: 100%; background: var(--primary); }
.fact-grid { margin: 0 12px 12px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.fact { padding: 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); }
.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; }
.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; }
.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; }
.escalation { padding: 0 14px 14px; }
.escalation h3 { margin: 0 0 10px; font-size: 14px; }
.step { position: relative; padding: 0 0 16px 27px; }
.step::before { content: ""; position: absolute; left: 7px; top: 17px; bottom: 0; width: 1px; background: var(--line); }
.step:last-child::before { display: none; }
.step-dot { position: absolute; left: 0; top: 3px; width: 15px; height: 15px; border-radius: 50%; border: 3px solid var(--surface); background: var(--subtle); box-shadow: 0 0 0 1px var(--line); }
.step.done .step-dot { background: var(--success); }
.step.current .step-dot { background: var(--warning); }
.step strong { display: block; }
.step span { color: var(--muted); font-size: 12px; }
.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; }
.grid { display: grid; gap: 12px; }
.stats { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: 12px; }
.stat { padding: 15px; min-height: 110px; display: flex; flex-direction: column; justify-content: space-between; }
.stat span { color: var(--muted); }
.stat strong { font-size: 27px; font-variant-numeric: tabular-nums; }
.search { position: relative; min-width: 230px; }
.search .icon { position: absolute; left: 11px; top: 11px; color: var(--subtle); pointer-events: none; }
.search input { padding-left: 38px; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 870px; }
th, td { padding: 12px 14px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; }
th { background: var(--surface-2); color: var(--muted); font-size: 12px; }
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); }
.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; }
.rule-card h2 { margin: 12px 0 5px; font-size: 16px; }
.rule-card p { margin: 0 0 14px; color: var(--muted); }
.rule-card dl { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 7px; color: var(--muted); }
.rule-card dd { margin: 0; color: var(--text); }
.rule-card-actions { margin-top: 14px; padding-top: 12px; display: flex; flex-wrap: wrap; gap: 7px; border-top: 1px solid var(--line); }
.rule-context { margin-bottom: 12px; padding: 11px 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid rgba(56,189,248,.34); border-radius: 10px; background: rgba(56,189,248,.08); }
.rule-context[hidden] { display: none; }
.rule-context p { margin: 0; color: var(--muted); }
.linked-zone { grid-column: 1/-1; padding: 11px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); }
.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; }
.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; }
.chain-num { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; background: var(--primary-soft); color: #cfc8ff; font-weight: 800; }
.chain-row small { color: var(--muted); }
.chart-grid { grid-template-columns: 1.35fr .65fr; }
.chart { padding: 16px; }
.chart h2 { margin: 0; font-size: 15px; }
.chart p { margin: 4px 0 14px; color: var(--muted); }
.bars { height: 220px; display: flex; align-items: end; gap: 16px; padding: 10px 6px 26px; border-bottom: 1px solid var(--line); }
.bar { position: relative; flex: 1; min-width: 24px; height: var(--h); border-radius: 7px 7px 0 0; background: linear-gradient(180deg, var(--primary), #4f46a8); }
.bar span { position: absolute; inset: calc(100% + 5px) 0 auto; text-align: center; color: var(--muted); font-size: 11px; }
.donut { width: 170px; height: 170px; margin: 18px auto; border-radius: 50%; background: conic-gradient(var(--success) 0 61%, var(--warning) 61% 83%, var(--danger) 83%); position: relative; }
.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; }
.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); }
.state-layer h2 { margin: 12px 0 4px; font-size: 18px; }
.state-layer p { margin: 0 0 14px; max-width: 460px; color: var(--muted); }
.skeleton { width: min(460px, 90%); display: grid; gap: 10px; }
.skeleton i { height: 15px; border-radius: 6px; background: linear-gradient(90deg, var(--surface-3), var(--line), var(--surface-3)); background-size: 200% 100%; animation: shimmer 1.4s infinite; }
.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)); }
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; }
.dialog-body { padding: 18px; overflow: auto; }
.dialog-foot { border: 0; border-top: 1px solid var(--line); justify-content: flex-end; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.form-field { display: grid; gap: 6px; }
.form-field.full { grid-column: 1/-1; }
.form-field label { color: var(--muted); font-size: 12px; font-weight: 700; }
.toast { position: fixed; right: 20px; bottom: 20px; z-index: 1100; max-width: min(420px, calc(100% - 40px)); padding: 12px 14px; border: 1px solid rgba(52,211,153,.35); border-radius: 10px; background: #0b2c25; color: #d9fff3; box-shadow: var(--shadow); transform: translateY(20px); opacity: 0; pointer-events: none; transition: .2s ease; }
.toast.show { transform: none; opacity: 1; }
.muted { color: var(--muted); }
.mono { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-variant-numeric: tabular-nums; }
.mobile-nav { display: none; }
@media (max-width: 1240px) {
:root { --sidebar: 78px; }
.brand-copy, .nav span:not(.count), .nav-label, .shift-card { display: none; }
.brand { justify-content: center; padding-inline: 0; }
.nav button { justify-content: center; padding: 0; }
.nav .count { position: absolute; margin: -24px 0 0 25px; }
.duty-grid { grid-template-columns: 285px minmax(390px, 1fr); }
.action-panel { grid-column: 1/-1; }
.action-scroll { max-height: none; display: grid; grid-template-columns: rep…4257 tokens truncated…t>覆盖范围</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&amp;camera=CAM-S2-011&amp;return=bell-rule">查看警戒线</a></div></article>
</div>
</section>
<section class="view" data-page="chains">
<div class="page-head"><div><h1>升级链</h1><p>生产前至少两条独立路径;无回执不能当作成功。</p></div><div class="page-actions"><button class="btn btn-primary">编辑升级链</button></div></div>
<div class="grid chain-grid"><article class="card"><div class="card-head"><h2>校园高危事件 · 默认链</h2><span class="badge success">已发布 v4</span></div>
<div class="chain-row"><div class="chain-num">1</div><div><strong>值班室 Web + 本地声光</strong><small>立即 · 站点局域网内</small></div><span class="badge success">主路径</span></div>
<div class="chain-row"><div class="chain-num">2</div><div><strong>当班值班员短信</strong><small>立即 · 送达后等待 2 分钟</small></div><span class="badge info">独立路径</span></div>
<div class="chain-row"><div class="chain-num">3</div><div><strong>校级负责人语音</strong><small>未 ack 时升级 · 等待 3 分钟</small></div><span class="badge warning">兜底</span></div>
</article><article class="card"><div class="card-head"><h2>静默规则</h2><span class="badge info">无永久选项</span></div><div class="card-body"><label for="muteDuration" class="muted">维护窗口</label><select id="muteDuration" class="select" style="margin-top:6px"><option>30 分钟</option><option>1 小时</option><option>2 小时</option><option>4 小时(上限)</option></select><p class="muted">静默仅影响选定规则和点位,到时自动恢复;S1 事件不可静默。</p><button class="btn">创建维护窗口</button></div></article></div>
</section>
<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>
</section>
<section class="view" data-page="audit">
<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" 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>
<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>
</section>
</div>
</main>
</div>
<nav class="mobile-nav" aria-label="移动端主导航"><button class="active" data-view="duty">值班</button><button data-view="events">事件</button><button data-view="rules">规则</button><button data-view="chains">升级</button><button data-view="reports">报表</button></nav>
<dialog id="ruleDialog" aria-labelledby="ruleTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="ruleTitle">配置告警规则</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="ruleScene">场景模板</label><select id="ruleScene" class="select"><option>非授权人员进入</option><option>区域内聚集</option><option>方向越线</option></select></div>
<div class="form-field"><label for="ruleScope">继承与覆盖</label><select id="ruleScope" class="select"><option>当前设备覆盖</option><option>继承站点模板</option><option>覆盖站点模板</option></select></div>
<div class="form-field"><label for="ruleCamera">摄像头</label><select id="ruleCamera" class="select"><option value="CAM-S2-001">北围墙 01</option><option value="CAM-S2-006">实验楼入口 02</option><option value="CAM-S2-011">宿舍东通道</option></select></div>
<div class="form-field"><label for="ruleZone">检测区域</label><select id="ruleZone" class="select"><option value="ZONE-007">北围墙危险区域 · v7</option><option>北围墙方向警戒线 · v3</option></select></div>
<div class="linked-zone"><span><strong>空间坐标由摄像头详情维护</strong><small>区域版本变化不会绕过规则试运行或自动正式发布。</small></span><a class="btn" id="editRuleZone" href="../sense/index.html?view=device-detail&amp;camera=CAM-S2-001&amp;return=bell-rule">编辑检测区域</a></div>
<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>
<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>
<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>
<dialog id="muteDialog" aria-labelledby="muteTitle"><form method="dialog"><div class="dialog-head"><h2 id="muteTitle">创建限时静默</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="muteScope">影响范围</label><select id="muteScope" class="select"><option>北围墙 01 · 当前规则</option><option>北围墙区域</option></select></div><div class="form-field"><label for="muteTime">持续时间</label><select id="muteTime" class="select"><option>30 分钟</option><option>1 小时</option><option>2 小时</option><option>4 小时(上限)</option></select></div><div class="form-field full"><label for="muteCause">原因</label><textarea id="muteCause" class="textarea" required placeholder="例如:现场施工维护,已安排人工巡查"></textarea></div></div><p class="muted">到期自动恢复;S1 高危事件与设备离线运维告警不受此静默影响。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-primary" id="confirmMute" value="default">确认并记录审计</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 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:'等待短信回执'}
];
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 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-selected',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-selected',x===b)});notify(`正在演示“${b.textContent}”标签结构`);}));
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'?'深色':'浅色'}主题`);});
$('#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('已创建限时静默,到期将自动恢复并通知值班组。');});
const ruleDialog = $('#ruleDialog');
function openRuleEditor() { ruleDialog.showModal(); }
$('#openRuleDialog').addEventListener('click', openRuleEditor);
$('#continueRuleDraft').addEventListener('click', openRuleEditor);
$$('[data-edit-rule]').forEach(button=>button.addEventListener('click', openRuleEditor));
$('#ruleCamera').addEventListener('change',e=>{ $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent(e.target.value)}&return=bell-rule`; notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); });
$('#saveRuleDraft').addEventListener('click',()=>notify($('#startDryRun').checked?'规则草稿已保存并进入试运行,不会触发正式预警。':'规则草稿已保存,尚未开始试运行。'));
$('#publishTrialRule').addEventListener('click',()=>{if(confirm('正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?')){$('#trialBadge').className='badge success';$('#trialBadge').textContent='正式生效';notify('规则版本已正式发布,变更已记录审计并可回滚。');}});
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.has('camera')) {
switchView('rules'); $('#ruleContext').hidden=false;
const camera=routeParams.get('camera'); if([...$('#ruleCamera').options].some(option=>option.value===camera)) $('#ruleCamera').value=camera;
$('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent(camera)}&return=bell-rule`;
notify('已保留摄像头与检测区域上下文,可继续配置规则。');
}
</script>
</body>
</html>