1000 lines
112 KiB
HTML
1000 lines
112 KiB
HTML
<!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; }
|
||
[hidden] { display: none !important; }
|
||
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; }
|
||
.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; }
|
||
.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; }
|
||
.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; }
|
||
.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; }
|
||
.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; }
|
||
.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); }
|
||
.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; }
|
||
.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); }
|
||
.site-grid { grid-template-columns: minmax(260px,.72fr) minmax(520px,1.45fr); }
|
||
.site-list { padding: 8px; display: grid; gap: 7px; }
|
||
.site-item { width: 100%; min-height: 62px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); display: flex; justify-content: space-between; align-items: center; gap: 10px; text-align: left; }
|
||
.site-item.active { border-color: var(--primary); background: var(--primary-soft); }
|
||
.site-item small { display: block; color: var(--muted); }
|
||
.policy-note { margin: 12px 14px 0; padding: 10px 12px; border-left: 3px solid var(--warning); background: rgba(251,191,36,.08); color: var(--muted); }
|
||
.context-strip { margin-bottom: 12px; padding: 11px 13px; border: 1px solid rgba(56,189,248,.34); border-radius: 10px; background: rgba(56,189,248,.08); display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||
.context-strip 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; }
|
||
.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-subnav { margin-bottom: 12px; padding: 4px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; border: 1px solid var(--line); border-radius: 11px; background: var(--surface); }
|
||
.chain-subnav button { min-height: 44px; border: 0; border-radius: 8px; background: transparent; color: var(--muted); font-weight: 750; }
|
||
.chain-subnav button.active { background: var(--primary-soft); color: var(--text); box-shadow: inset 0 0 0 1px rgba(139,124,246,.36); }
|
||
.chain-panel { display: none; }
|
||
.chain-panel.active { 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; }
|
||
.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); }
|
||
.target-ref { display: inline-block; margin-top: 3px; color: #b8e5ff; font: 12px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||
.resolve-card { padding: 14px; }
|
||
.resolve-card h2 { margin: 0 0 4px; font-size: 15px; }
|
||
.resolve-card > p { margin: 0 0 12px; color: var(--muted); }
|
||
.resolve-list { display: grid; gap: 8px; }
|
||
.resolve-item { padding: 10px 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); }
|
||
.resolve-item span, .resolve-item small { display: block; color: var(--muted); }
|
||
.resolve-item strong { display: block; margin: 2px 0; }
|
||
.roster-week { padding: 12px; display: grid; grid-template-columns: repeat(7, minmax(125px, 1fr)); gap: 8px; overflow-x: auto; }
|
||
.roster-day { min-width: 125px; padding: 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); }
|
||
.roster-day.current { border-color: var(--primary); background: var(--primary-soft); }
|
||
.roster-day > strong { display: block; margin-bottom: 8px; }
|
||
.shift-slot { padding: 8px 0; border-top: 1px solid var(--line); }
|
||
.shift-slot:first-of-type { border-top: 0; }
|
||
.shift-slot span, .shift-slot small { display: block; color: var(--muted); font-size: 12px; }
|
||
.shift-slot strong { display: block; margin: 2px 0; font-size: 13px; }
|
||
.object-note { padding: 11px 14px; border-top: 1px solid var(--line); color: var(--muted); }
|
||
.contact-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-bottom: 12px; }
|
||
.contact-summary .stat { min-height: 96px; }
|
||
.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; }
|
||
.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); }
|
||
.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, #eventDialog, #handoverDialog, #rollbackDialog, #escalationDialog, #scheduleDialog, #substituteDialog, #contactDialog { 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; }
|
||
.radio-card { display: block; margin-bottom: 9px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); }
|
||
.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; }
|
||
.mobile-manage { 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: repeat(3, 1fr); }
|
||
.disposition { border-top: 0; border-left: 1px solid var(--line); }
|
||
}
|
||
@media (max-width: 850px) {
|
||
:root { --sidebar: 0px; }
|
||
.app { display: block; padding-bottom: 66px; }
|
||
.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; }
|
||
.duty-grid, .chain-grid, .chart-grid { grid-template-columns: 1fr; }
|
||
.alert-list, .action-scroll { max-height: none; }
|
||
.action-scroll { display: block; }
|
||
.disposition { border-left: 0; border-top: 1px solid var(--line); }
|
||
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
.site-grid { grid-template-columns: 1fr; }
|
||
.roster-week { grid-template-columns: repeat(2, minmax(145px, 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); }
|
||
}
|
||
@media (max-width: 560px) {
|
||
.page-head { display: grid; }
|
||
.stats, .rule-grid { grid-template-columns: 1fr; }
|
||
.chain-subnav { grid-template-columns: 1fr; }
|
||
.contact-summary { grid-template-columns: 1fr; }
|
||
.roster-week { grid-template-columns: 1fr; }
|
||
.form-grid { grid-template-columns: 1fr; }
|
||
.form-field.full { grid-column: auto; }
|
||
.video-stage { margin-inline: 8px; }
|
||
.fact-grid { margin-inline: 8px; }
|
||
.evidence-summary { display: grid; }
|
||
.disposition-actions { grid-template-columns: 1fr; }
|
||
}
|
||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<svg aria-hidden="true" width="0" height="0" style="position:absolute">
|
||
<symbol id="i-inbox" viewBox="0 0 24 24"><path d="M4 4h16v16H4Z"/><path d="m4 14 5-1 2 3h2l2-3 5 1"/></symbol>
|
||
<symbol id="i-event" viewBox="0 0 24 24"><path d="M6 3h12v18H6z"/><path d="M9 8h6m-6 4h6m-6 4h4"/></symbol>
|
||
<symbol id="i-rule" viewBox="0 0 24 24"><path d="M4 6h7m6 0h3M4 12h3m6 0h7M4 18h9m6 0h1"/><circle cx="14" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="16" cy="18" r="2"/></symbol>
|
||
<symbol id="i-phone" viewBox="0 0 24 24"><path d="M5 4h4l2 5-3 2a16 16 0 0 0 5 5l2-3 5 2v4c-8 2-17-7-15-15Z"/></symbol>
|
||
<symbol id="i-chart" viewBox="0 0 24 24"><path d="M4 20V9m6 11V4m6 16v-7m4 7H2"/></symbol>
|
||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="m12 3 8 3v6c0 5-3 8-8 9-5-1-8-4-8-9V6Z"/><path d="m9 12 2 2 4-5"/></symbol>
|
||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></symbol>
|
||
<symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></symbol>
|
||
<symbol id="i-bell" viewBox="0 0 24 24"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/><path d="M10 21h4"/></symbol>
|
||
<symbol id="i-check" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></symbol>
|
||
<symbol id="i-alert" viewBox="0 0 24 24"><path d="M12 3 2 20h20Z"/><path d="M12 9v4m0 3v.01"/></symbol>
|
||
<symbol id="i-play" viewBox="0 0 24 24"><path d="m8 5 11 7-11 7Z"/></symbol>
|
||
<symbol id="i-expand" viewBox="0 0 24 24"><path d="M8 3H3v5m13-5h5v5M8 21H3v-5m13 5h5v-5"/></symbol>
|
||
<symbol id="i-download" viewBox="0 0 24 24"><path d="M12 3v12m0 0 5-5m-5 5-5-5M4 20h16"/></symbol>
|
||
<symbol id="i-lock" viewBox="0 0 24 24"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></symbol>
|
||
<symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v6l4 2"/></symbol>
|
||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/></symbol>
|
||
</svg>
|
||
|
||
<div class="prototype-banner">PROTOTYPE - 仅供枚举交互,非实现依据</div>
|
||
<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">值班与业务</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>
|
||
<div class="shift-card"><strong>安保值班组 · 白班</strong><span>08:00–20:00 · 4 人在线</span><div class="shift-bar"><i></i></div></div>
|
||
</aside>
|
||
|
||
<main class="main" id="main-content" tabindex="-1">
|
||
<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><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>
|
||
</header>
|
||
|
||
<div class="content">
|
||
<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="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>
|
||
<div class="queue-head"><div class="queue-tabs" aria-label="预警队列筛选"><button class="active" aria-pressed="true">全部 9</button><button aria-pressed="false">高危 3</button><button aria-pressed="false">我的 2</button></div></div>
|
||
<div class="alert-list" id="alertList"></div>
|
||
</section>
|
||
|
||
<section class="card evidence" aria-label="事件事实与证据">
|
||
<div class="evidence-summary"><div><span class="badge danger" id="selectedSeverity">高危 · S1</span><h2 id="selectedTitle">危险区域闯入</h2><p id="selectedMeta">北围墙 01 · 23:14:08 · EVT-01J8…7A2</p></div><button class="btn icon-btn" aria-label="更多事件操作"><svg class="icon"><use href="#i-more"/></svg></button></div>
|
||
<div class="evidence-tabs" aria-label="证据类型筛选"><button class="active" aria-pressed="true">视频片段</button><button aria-pressed="false">抓拍 2</button><button aria-pressed="false">结构化事实</button></div>
|
||
<div class="video-stage" id="videoStage">
|
||
<svg viewBox="0 0 960 540" role="img" aria-label="北围墙 01 事件视频模拟画面,匿名目标进入危险区域">
|
||
<defs><linearGradient id="bellSky" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#102337"/><stop offset="1" stop-color="#060a11"/></linearGradient><pattern id="bellScan" width="8" height="8" patternUnits="userSpaceOnUse"><path d="M0 8h8" stroke="#fff" stroke-opacity=".035"/></pattern></defs>
|
||
<rect width="960" height="540" fill="url(#bellSky)"/><path d="M0 340 180 260 355 316 520 232 690 306 830 230 960 285V540H0Z" fill="#142638"/><rect y="414" width="960" height="126" fill="#111820"/><path d="M0 416h960M0 474h960" stroke="#6d8496" stroke-opacity=".48"/><path d="M150 305v235m250-240v240m280-255v255m170-280v280" stroke="#758b9d" stroke-width="4"/><polygon points="92,362 305,318 558,342 525,480 126,478" fill="#fb7185" fill-opacity=".15" stroke="#fb7185" stroke-width="4" stroke-dasharray="10 7"/><rect x="354" y="330" width="63" height="148" fill="none" stroke="#fbbf24" stroke-width="4"/><text x="354" y="319" fill="#ffe18a" font-size="17">person · 0.92</text><circle cx="385" cy="310" r="24" fill="none" stroke="#fbbf24" stroke-width="4"/><rect width="960" height="540" fill="url(#bellScan)"/><text x="26" y="38" fill="#d6e8f5" font-size="18">北围墙 01 · 事件片段</text><text x="26" y="66" fill="#91a6b8" font-size="14">事发前 8s · 事发后 12s · 匿名目标</text>
|
||
</svg>
|
||
<div class="video-controls"><button aria-label="播放片段"><svg class="icon"><use href="#i-play"/></svg></button><span class="mono">00:11 / 00:20</span><div class="timeline-bar"><i></i></div><button aria-label="全屏查看"><svg class="icon"><use href="#i-expand"/></svg></button><button aria-label="下载授权证据"><svg class="icon"><use href="#i-download"/></svg></button></div>
|
||
</div>
|
||
<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 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><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>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="view" data-page="events">
|
||
<div class="page-head"><div><h1>事件中心</h1><p>事件是不可变事实;预警与处置结果作为关联记录单独展示。</p></div><div class="page-actions"><button class="btn"><svg class="icon"><use href="#i-download"/></svg>导出筛选结果</button></div></div>
|
||
<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 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" 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>
|
||
</section>
|
||
|
||
<section class="view" data-page="chains">
|
||
<div class="page-head"><div><h1>升级链</h1><p>联系人、值班排班和升级策略统一设计、分对象管理;策略引用动态目标,不写死手机号。</p></div><div class="page-actions"><span class="badge success">当前排班覆盖完整</span></div></div>
|
||
<div class="chain-subnav" aria-label="升级链二级模块"><button class="active" data-chain-section="strategy" aria-pressed="true">升级策略</button><button data-chain-section="roster" aria-pressed="false">值班与排班</button><button data-chain-section="contacts" aria-pressed="false">联系人和通道</button></div>
|
||
|
||
<section class="chain-panel active" data-chain-panel="strategy">
|
||
<div class="grid chain-grid"><article class="card"><div class="card-head"><div><h2>校园高危事件 · 默认链</h2><span class="muted">每一步引用类型化目标,解析后才创建投递</span></div><span class="badge success">已发布 v4</span></div>
|
||
<div class="chain-row"><div class="chain-num">1</div><div><strong>值班室 Web + 本地声光</strong><small>立即 · 站点局域网内</small><span class="target-ref">team:DUTY-ROOM</span></div><span class="badge success">固定值班组</span></div>
|
||
<div class="chain-row"><div class="chain-num">2</div><div><strong>当前排班值班员 · 短信</strong><small>立即 · 送达后等待 2 分钟</small><span class="target-ref">on_call_schedule:SCH-CAMPUS-SECURITY@v12</span></div><span class="badge info">动态排班</span></div>
|
||
<div class="chain-row"><div class="chain-num">3</div><div><strong>校级负责人 · 语音</strong><small>未 ack 时升级 · 等待 3 分钟</small><span class="target-ref">person:USR-SCHOOL-LEAD</span></div><span class="badge warning">指定人员</span></div>
|
||
<div class="object-note">升级策略只保存目标引用和通道偏好,不复制联系人号码或轮换字段。</div>
|
||
</article><article class="card resolve-card"><div class="card-head" style="padding:0 0 11px;margin-bottom:12px"><div><h2>目标解析预览</h2><span class="muted">事件时间 2026-08-04 23:14 · Asia/Shanghai</span></div><button class="btn" id="editEscalation">编辑策略</button></div><div class="resolve-list"><div class="resolve-item"><span>排班目标</span><strong>校园安保值班表 · v12</strong><small class="mono">SCH-CAMPUS-SECURITY</small></div><div class="resolve-item"><span>当前解析结果</span><strong id="resolvedPerson">王值班员 · 晚班</strong><small>短信已验证 · 语音已验证</small></div><div class="resolve-item"><span>投递快照</span><strong>23:14:13 已固化</strong><small>收件人、通道、排班 v12;后续编辑不改写本次事实</small></div></div></article></div>
|
||
<div class="grid chain-grid" style="margin-top:12px"><article class="card"><div class="card-head"><h2>解析与版本规则</h2><span class="badge info">不可变历史</span></div><div class="card-body"><p class="muted">每次投递创建时按站点时区解析当时生效的排班版本,并保存实际收件人、通道与版本快照。未来排班版本只影响其生效时间后的新投递。</p></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="chain-panel" data-chain-panel="roster">
|
||
<div class="grid stats"><article class="card stat"><span>当前排班</span><strong id="scheduleVersion">v12</strong><span class="muted">已发布 · 08-01 生效</span></article><article class="card stat"><span>站点时区</span><strong style="font-size:18px">Asia/Shanghai</strong><span class="muted">跨午夜班次按站点时间</span></article><article class="card stat"><span>当前值班</span><strong style="font-size:18px">王值班员</strong><span class="muted">晚班 · 至明日 08:00</span></article><article class="card stat"><span>未来 14 天覆盖</span><strong>100%</strong><span class="muted">0 空档 · 0 重叠</span></article></div>
|
||
<article class="card"><div class="card-head"><div><h2>校园安保值班表</h2><span class="muted">周轮换 · 白班 08:00–20:00 / 晚班 20:00–08:00</span></div><div class="page-actions"><button class="btn" id="simulateRosterConflict">演示冲突检查</button><button class="btn" id="openSubstitute">临时替班</button><button class="btn btn-primary" id="openSchedulePublish">发布新版本</button></div></div><div class="inline-alert danger" id="rosterConflict" role="alert" hidden style="margin:12px 12px 0"></div><div class="roster-week">
|
||
<div class="roster-day current"><strong>今天 · 周二</strong><div class="shift-slot"><span>白班</span><strong>林值班员</strong><small>08:00–20:00</small></div><div class="shift-slot"><span>晚班</span><strong id="currentNightShift">王值班员</strong><small>20:00–08:00</small></div></div>
|
||
<div class="roster-day"><strong>周三</strong><div class="shift-slot"><span>白班</span><strong>赵值班员</strong><small>08:00–20:00</small></div><div class="shift-slot"><span>晚班</span><strong id="wednesdayNightShift">周值班员</strong><small>20:00–08:00</small></div></div>
|
||
<div class="roster-day"><strong>周四</strong><div class="shift-slot"><span>白班</span><strong>林值班员</strong><small>08:00–20:00</small></div><div class="shift-slot"><span>晚班</span><strong>王值班员</strong><small>20:00–08:00</small></div></div>
|
||
<div class="roster-day"><strong>周五</strong><div class="shift-slot"><span>白班</span><strong>赵值班员</strong><small>08:00–20:00</small></div><div class="shift-slot"><span>晚班</span><strong>周值班员</strong><small>20:00–08:00</small></div></div>
|
||
<div class="roster-day"><strong>周六</strong><div class="shift-slot"><span>白班</span><strong>林值班员</strong><small>08:00–20:00</small></div><div class="shift-slot"><span>晚班</span><strong>王值班员</strong><small>20:00–08:00</small></div></div>
|
||
<div class="roster-day"><strong>周日</strong><div class="shift-slot"><span>白班</span><strong>赵值班员</strong><small>08:00–20:00</small></div><div class="shift-slot"><span>晚班</span><strong>周值班员</strong><small>20:00–08:00</small></div></div>
|
||
<div class="roster-day"><strong>下周一</strong><div class="shift-slot"><span>白班</span><strong>林值班员</strong><small>08:00–20:00</small></div><div class="shift-slot"><span>晚班</span><strong>王值班员</strong><small>20:00–08:00</small></div></div>
|
||
</div><div class="object-note"><strong>边界:</strong>临时替班进入新排班草稿并经发布后影响未来班次;值班交接只转移进行中 Alert 的责任,不会暗改这里的轮换。</div></article>
|
||
<article class="card" style="margin-top:12px"><div class="card-head"><h2>版本与例外</h2><span class="badge info">全量审计</span></div><div class="table-wrap"><table><thead><tr><th>版本</th><th>生效时间</th><th>变更</th><th>状态</th><th>发布人</th></tr></thead><tbody><tr><td>v12</td><td>2026-08-01 00:00</td><td>暑期周轮换</td><td><span class="badge success">当前生效</span></td><td>林工</td></tr><tr><td>v11</td><td>2026-07-01 00:00</td><td>赵值班员加入白班组</td><td><span class="badge">历史</span></td><td>周管理员</td></tr></tbody></table></div></article>
|
||
</section>
|
||
|
||
<section class="chain-panel" data-chain-panel="contacts">
|
||
<div class="grid contact-summary"><article class="card stat"><span>联系人</span><strong id="contactCount">6</strong><span class="muted">4 名值班员 · 2 名负责人</span></article><article class="card stat"><span>已验证通道</span><strong>11 / 12</strong><span class="muted">1 个语音号码待验证</span></article><article class="card stat"><span>被排班引用</span><strong>4</strong><span class="muted">删除前必须解除引用</span></article></div>
|
||
<article class="card"><div class="card-head"><div><h2>联系人和通知通道</h2><span class="muted">只维护身份、角色、值班组和通道;班次与轮换在排班模块维护</span></div><button class="btn btn-primary" id="openContact">新建联系人</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><strong>王值班员</strong><br><span class="mono muted">USR-DUTY-WANG</span></td><td>值班员</td><td>校园安保组</td><td>短信 ✓ · 语音 ✓ · Web ✓</td><td>SCH-CAMPUS-SECURITY</td><td><span class="badge success">可用</span></td><td><button class="btn" data-edit-contact="wang">编辑</button></td></tr><tr><td><strong>林值班员</strong><br><span class="mono muted">USR-DUTY-LIN</span></td><td>值班员</td><td>校园安保组</td><td>短信 ✓ · Web ✓</td><td>SCH-CAMPUS-SECURITY</td><td><span class="badge success">可用</span></td><td><button class="btn" data-edit-contact="lin">编辑</button></td></tr><tr><td><strong>校级负责人</strong><br><span class="mono muted">USR-SCHOOL-LEAD</span></td><td>站点负责人</td><td>校级负责人组</td><td>短信 ✓ · 语音待验证</td><td>升级策略 v4</td><td><span class="badge warning">部分可用</span></td><td><button class="btn" data-edit-contact="lead">编辑</button></td></tr></tbody></table></div><div class="object-note">联系人通道变更只影响后续新投递;已创建投递保留当时的收件人和通道快照。联系人被排班或策略引用时不能直接删除。</div></article>
|
||
</section>
|
||
</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>
|
||
<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">
|
||
<div class="page-head"><div><h1>站点与 Area</h1><p>Bell 是 Tenant、Site、Area、配额与隐私准入策略的唯一真相源;Sense 只消费版本化投影。</p></div><div class="page-actions"><button class="btn">管理角色权限</button><button class="btn btn-primary" id="newSite">新建站点</button></div></div>
|
||
<div class="grid stats"><article class="card stat"><span>租户</span><strong>1</strong><span class="muted">青藤教育集团</span></article><article class="card stat"><span>站点</span><strong>2</strong><span class="muted">1 个试点中</span></article><article class="card stat"><span>视频配额</span><strong>16 / 128</strong><span class="muted">默认 16,不是硬上限</span></article><article class="card stat"><span>策略投影</span><strong>v42</strong><span class="muted">Sense 2 分钟前已同步</span></article></div>
|
||
<div class="grid site-grid"><article class="card"><div class="card-head"><h2>站点</h2><button class="btn btn-quiet">筛选</button></div><div class="site-list"><button class="site-item active"><span><strong>青藤寄宿学校</strong><small>site-ivy · S2 校园包</small></span><span class="badge success">启用</span></button><button class="site-item"><span><strong>城东试验点</strong><small>site-east · 未绑定场景包</small></span><span class="badge warning">准备中</span></button></div></article>
|
||
<article class="card"><div class="card-head"><div><h2>青藤寄宿学校 · Area</h2><span class="muted">层级、报表口径和设备准入共用同一逻辑 ID</span></div><button class="btn btn-primary" id="newArea">新建 Area</button></div><div class="policy-note"><strong>策略变更不会静默处置已有设备。</strong> 若改为“仅非成像”且 Area 内已有摄像头,必须显式迁移设备或取消变更。</div><div class="table-wrap"><table><thead><tr><th>Area</th><th>父级</th><th>capture_policy</th><th>设备</th><th>策略版本</th><th>投影状态</th><th>操作</th></tr></thead><tbody><tr><td><strong>北围墙</strong><br><span class="muted">area-north-wall</span></td><td>校园</td><td><span class="badge success">允许成像</span></td><td>4 视频</td><td>v42</td><td><span class="badge success">已同步</span></td><td><button class="btn" data-edit-area="north">编辑策略</button></td></tr><tr><td><strong>宿舍卧室</strong><br><span class="muted">area-dorm-room</span></td><td>宿舍楼</td><td><span class="badge warning">仅非成像</span></td><td>0 视频 · 1 雷达规划</td><td>v38</td><td><span class="badge success">已同步</span></td><td><button class="btn" data-edit-area="dorm">编辑策略</button></td></tr><tr><td><strong>实验楼入口</strong><br><span class="muted">area-lab-gate</span></td><td>实验楼</td><td><span class="badge success">允许成像</span></td><td>2 视频</td><td>v41</td><td><span class="badge warning">待 Sense 拉取</span></td><td><button class="btn" data-edit-area="lab">编辑策略</button></td></tr></tbody></table></div></article></div>
|
||
</section>
|
||
|
||
<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"></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>
|
||
</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 value="ZONE-011">北围墙方向警戒线 · 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&camera=CAM-S2-001&zone=ZONE-007&return_to=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>
|
||
<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 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>
|
||
|
||
<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>
|
||
|
||
<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">交接期间升级链继续计时,不暂停、不重置;只转移这些进行中 Alert 的处置责任,不修改未来排班。未来班次变化请在“值班与排班”创建替班版本。</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>
|
||
|
||
<dialog id="escalationDialog" aria-labelledby="escalationTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="escalationTitle">编辑升级步骤</h2><span class="muted">默认链 v5 草稿 · 不直接保存手机号</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="escalationTargetType">目标类型</label><select class="select" id="escalationTargetType"><option value="schedule">值班排班</option><option value="team">固定值班组</option><option value="person">指定人员</option></select></div><div class="form-field"><label for="escalationTargetRef">目标</label><select class="select" id="escalationTargetRef"></select></div><div class="form-field"><label for="escalationChannel">首选通道</label><select class="select" id="escalationChannel"><option>短信 + 语音兜底</option><option>Web + 本地声光</option><option>语音 + 短信兜底</option></select></div><div class="form-field"><label for="escalationTimeout">等待 ack</label><select class="select" id="escalationTimeout"><option>2 分钟</option><option>3 分钟</option><option>5 分钟</option></select></div><div class="form-field full"><label>当前解析预览</label><div class="resolve-item" id="escalationPreview" aria-live="polite"></div></div></div><p class="workflow-note">保存的是类型化目标引用与通道偏好。每次投递创建时按站点时区解析生效排班,再固化收件人、通道和排班版本快照。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="saveEscalation">保存策略草稿</button></div></form></dialog>
|
||
|
||
<dialog id="scheduleDialog" aria-labelledby="scheduleTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="scheduleTitle">发布值班排班</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="scheduleName">排班计划</label><input class="field" id="scheduleName" value="校园安保值班表" readonly></div><div class="form-field"><label for="scheduleNextVersion">新版本</label><input class="field" id="scheduleNextVersion" value="v13" readonly></div><div class="form-field"><label for="scheduleTimezone">站点时区</label><select class="select" id="scheduleTimezone"><option>Asia/Shanghai</option></select></div><div class="form-field"><label for="scheduleEffective">生效时间</label><input class="field" id="scheduleEffective" type="datetime-local" value="2026-08-05T20:00"></div><div class="form-field full"><label for="scheduleReason">发布说明</label><textarea class="textarea" id="scheduleReason" placeholder="必填,例如:王值班员临时替班,覆盖周三晚班"></textarea></div></div><p class="metric-note">发布前重新检查空档、重叠、联系人停用和通道验证。新版本只影响生效时间后的投递,不回写 v12 或既有投递快照。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="confirmSchedulePublish">确认发布 v13</button></div></form></dialog>
|
||
|
||
<dialog id="substituteDialog" aria-labelledby="substituteTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="substituteTitle">创建临时替班</h2><span class="muted">保存到 v13 草稿,发布后生效</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="substituteShift">被替换班次</label><select class="select" id="substituteShift"><option>周三晚班 · 周值班员 · 20:00–08:00</option><option>周四白班 · 林值班员 · 08:00–20:00</option></select></div><div class="form-field"><label for="substitutePerson">替班人</label><select class="select" id="substitutePerson"><option value="">请选择可用联系人</option><option>王值班员</option><option>赵值班员</option></select></div><div class="form-field full"><label for="substituteReason">替班原因</label><textarea class="textarea" id="substituteReason" placeholder="必填,写入排班版本审计"></textarea></div></div><p class="workflow-note">临时替班修改未来排班。若只是把正在处置的 Alert 交给下一班,请使用值班台“交接班”,不要改排班。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="saveSubstitute">保存到 v13 草稿</button></div></form></dialog>
|
||
|
||
<dialog id="contactDialog" aria-labelledby="contactTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="contactTitle">新建联系人</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="contactName">姓名</label><input class="field" id="contactName" placeholder="联系人显示名"></div><div class="form-field"><label for="contactRole">角色</label><select class="select" id="contactRole"><option>值班员</option><option>站点负责人</option><option>租户管理员</option></select></div><div class="form-field"><label for="contactTeam">值班组</label><select class="select" id="contactTeam"><option>校园安保组</option><option>校级负责人组</option></select></div><div class="form-field"><label for="contactPhone">新通知号码</label><input class="field" id="contactPhone" type="tel" placeholder="输入后发送验证码;不回显旧值"></div><div class="form-field full"><label>启用通道</label><label class="radio-card"><input type="checkbox" id="contactSms" checked> 短信(需验证)</label><label class="radio-card"><input type="checkbox" id="contactVoice"> 语音(需验证)</label><label class="radio-card"><input type="checkbox" id="contactWeb" checked> Web 站内通知</label></div></div><p class="metric-note">联系人保存身份、角色、值班组与已验证通道。加入哪个班次、何时轮换由“值班与排班”模块引用联系人 ID 完成。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button type="button" class="btn btn-primary" id="saveContact">保存并发起通道验证</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:'短信等待回执',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 = 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 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();
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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 的升级链继续运行。'); });
|
||
|
||
function switchChainSection(name) {
|
||
const target = ['strategy', 'roster', 'contacts'].includes(name) ? name : 'strategy';
|
||
$$('[data-chain-panel]').forEach(panel => panel.classList.toggle('active', panel.dataset.chainPanel === target));
|
||
$$('[data-chain-section]').forEach(button => {
|
||
const active = button.dataset.chainSection === target;
|
||
button.classList.toggle('active', active);
|
||
button.setAttribute('aria-pressed', String(active));
|
||
});
|
||
}
|
||
$$('[data-chain-section]').forEach(button => button.addEventListener('click', () => switchChainSection(button.dataset.chainSection)));
|
||
|
||
const escalationTargets = {
|
||
schedule: [
|
||
{value:'SCH-CAMPUS-SECURITY',label:'校园安保值班表 · v12',preview:'王值班员 · 晚班 · 短信/语音已验证'},
|
||
{value:'SCH-SCHOOL-LEAD',label:'校级负责人轮值表 · v6',preview:'陈负责人 · 当前轮值 · 语音已验证'}
|
||
],
|
||
team: [
|
||
{value:'DUTY-ROOM',label:'值班室固定组',preview:'4 人在线 · Web + 本地声光'},
|
||
{value:'SCHOOL-LEADS',label:'校级负责人组',preview:'3 名成员 · 顺序通知'}
|
||
],
|
||
person: [
|
||
{value:'USR-SCHOOL-LEAD',label:'校级负责人',preview:'指定人员 · 短信已验证 · 语音待验证'},
|
||
{value:'USR-DUTY-WANG',label:'王值班员',preview:'指定人员 · 短信/语音/Web 已验证'}
|
||
]
|
||
};
|
||
function renderEscalationTargets() {
|
||
const type = $('#escalationTargetType').value;
|
||
const select = $('#escalationTargetRef');
|
||
select.replaceChildren();
|
||
escalationTargets[type].forEach(target => {
|
||
const option = document.createElement('option');
|
||
option.value = target.value;
|
||
option.textContent = target.label;
|
||
select.append(option);
|
||
});
|
||
renderEscalationPreview();
|
||
}
|
||
function renderEscalationPreview() {
|
||
const type = $('#escalationTargetType').value;
|
||
const target = escalationTargets[type].find(item => item.value === $('#escalationTargetRef').value) || escalationTargets[type][0];
|
||
const typeLabel = type === 'schedule' ? '值班排班' : type === 'team' ? '固定值班组' : '指定人员';
|
||
$('#escalationPreview').textContent = `${typeLabel} · ${target.label} → ${target.preview}。实际投递时固化解析快照。`;
|
||
}
|
||
$('#editEscalation').addEventListener('click', () => { renderEscalationTargets(); $('#escalationDialog').showModal(); });
|
||
$('#escalationTargetType').addEventListener('change', renderEscalationTargets);
|
||
$('#escalationTargetRef').addEventListener('change', renderEscalationPreview);
|
||
$('#saveEscalation').addEventListener('click', () => { $('#escalationDialog').close(); notify('升级策略草稿已保存为类型化目标引用;未复制联系人号码。'); });
|
||
|
||
let rosterHasConflict = false;
|
||
$('#simulateRosterConflict').addEventListener('click', event => {
|
||
rosterHasConflict = !rosterHasConflict;
|
||
$('#rosterConflict').hidden = !rosterHasConflict;
|
||
$('#rosterConflict').textContent = rosterHasConflict ? '发布已阻止:周三 20:00–21:00 存在排班重叠,同时安排了周值班员和王值班员;周四 08:00–09:00 存在空档。请调整后重新检查。' : '';
|
||
event.currentTarget.textContent = rosterHasConflict ? '恢复无冲突排班' : '演示冲突检查';
|
||
$('#openSchedulePublish').disabled = rosterHasConflict;
|
||
notify(rosterHasConflict ? '发现 1 个重叠和 1 个空档,排班不能发布。' : '冲突已修复,未来 14 天覆盖完整。');
|
||
});
|
||
$('#openSchedulePublish').addEventListener('click', () => { if (rosterHasConflict) { notify('请先修复排班空档和重叠。'); $('#rosterConflict').focus(); return; } $('#scheduleDialog').showModal(); });
|
||
$('#confirmSchedulePublish').addEventListener('click', () => {
|
||
if (!$('#scheduleReason').value.trim()) { notify('请填写排班发布说明。'); $('#scheduleReason').focus(); return; }
|
||
$('#scheduleDialog').close();
|
||
$('#scheduleVersion').textContent = 'v13';
|
||
notify('排班 v13 已发布,将在设定时间生效;v12 与既有投递快照保持不变。');
|
||
});
|
||
$('#openSubstitute').addEventListener('click', () => $('#substituteDialog').showModal());
|
||
$('#saveSubstitute').addEventListener('click', () => {
|
||
if (!$('#substitutePerson').value) { notify('请选择替班人。'); $('#substitutePerson').focus(); return; }
|
||
if (!$('#substituteReason').value.trim()) { notify('请填写替班原因。'); $('#substituteReason').focus(); return; }
|
||
$('#substituteDialog').close();
|
||
$('#wednesdayNightShift').textContent = `${$('#substitutePerson').value}(替班草稿)`;
|
||
$('#scheduleVersion').textContent = 'v13 草稿';
|
||
notify('临时替班已保存到 v13 草稿,发布前不会影响实际通知。');
|
||
});
|
||
|
||
let editingContact = false;
|
||
function openContactEditor(key = '') {
|
||
editingContact = Boolean(key);
|
||
const names = {wang:'王值班员',lin:'林值班员',lead:'校级负责人'};
|
||
$('#contactTitle').textContent = editingContact ? `编辑联系人 · ${names[key]}` : '新建联系人';
|
||
$('#contactName').value = editingContact ? names[key] : '';
|
||
$('#contactPhone').value = '';
|
||
$('#contactPhone').placeholder = editingContact ? '留空则保留已验证号码;旧值不回显' : '输入后发送验证码;不回显旧值';
|
||
$('#contactDialog').showModal();
|
||
}
|
||
$('#openContact').addEventListener('click', () => openContactEditor());
|
||
$$('[data-edit-contact]').forEach(button => button.addEventListener('click', () => openContactEditor(button.dataset.editContact)));
|
||
$('#saveContact').addEventListener('click', () => {
|
||
if (!$('#contactName').value.trim()) { notify('请填写联系人姓名。'); $('#contactName').focus(); return; }
|
||
const needsPhone = $('#contactSms').checked || $('#contactVoice').checked;
|
||
if (!editingContact && needsPhone && !$('#contactPhone').value.trim()) { notify('短信或语音通道需要填写新号码并完成验证。'); $('#contactPhone').focus(); return; }
|
||
$('#contactDialog').close();
|
||
if (!editingContact) $('#contactCount').textContent = '7';
|
||
notify(editingContact ? '联系人主数据已更新;既有投递快照未改变。' : '联系人已保存,短信/语音通道进入验证流程;尚未加入任何班次。');
|
||
});
|
||
|
||
$('#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);
|
||
$('#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();
|
||
|
||
const routeParams = new URLSearchParams(location.search);
|
||
if (routeParams.get('view')) switchView(routeParams.get('view'));
|
||
if (routeParams.get('view') === 'chains' && routeParams.get('section')) switchChainSection(routeParams.get('section'));
|
||
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;
|
||
syncSenseZoneLink();
|
||
notify('已保留摄像头与检测区域上下文,可继续配置规则。');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|