341 lines
39 KiB
HTML
341 lines
39 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; }
|
||
html { min-width: 320px; background: var(--bg); }
|
||
body { margin: 0; min-height: 100dvh; background: var(--bg); color: var(--text); font: 14px/1.5 Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||
button, input, select, textarea { font: inherit; }
|
||
button, select, input[type="checkbox"], input[type="radio"] { cursor: pointer; }
|
||
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible { outline: 3px solid rgba(139, 124, 246, .45); outline-offset: 2px; }
|
||
.prototype-banner { position: fixed; z-index: 1000; inset: 0 0 auto 0; height: var(--banner); display: flex; align-items: center; justify-content: center; background: #f6c344; color: #211900; font-size: 12px; font-weight: 800; letter-spacing: .07em; }
|
||
.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto; }
|
||
.app { min-height: 100dvh; padding-top: var(--banner); display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); }
|
||
.sidebar { position: sticky; top: var(--banner); z-index: 30; height: calc(100dvh - var(--banner)); padding: 18px 12px; border-right: 1px solid var(--line); background: var(--surface); display: flex; flex-direction: column; }
|
||
.brand { display: flex; align-items: center; gap: 10px; padding: 2px 10px 20px; }
|
||
.brand-mark { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 10px; background: linear-gradient(145deg, #8b7cf6, #38bdf8); color: white; font-weight: 900; box-shadow: 0 8px 22px rgba(113, 97, 232, .25); }
|
||
.brand strong { display: block; font-size: 16px; }
|
||
.brand small { color: var(--muted); }
|
||
.nav-label { margin: 8px 10px 6px; color: var(--subtle); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .12em; }
|
||
.nav { display: grid; gap: 4px; }
|
||
.nav button { min-height: 44px; padding: 0 12px; border: 0; border-radius: 10px; background: transparent; color: var(--muted); display: flex; align-items: center; gap: 10px; text-align: left; transition: .2s ease; }
|
||
.nav button:hover { color: var(--text); background: var(--surface-2); }
|
||
.nav button[aria-current="page"] { color: var(--text); background: var(--primary-soft); box-shadow: inset 3px 0 var(--primary); }
|
||
.nav .count { margin-left: auto; min-width: 22px; padding: 1px 6px; border-radius: 999px; background: var(--surface-3); color: var(--text); text-align: center; font-size: 12px; }
|
||
.shift-card { margin-top: auto; padding: 12px; border: 1px solid var(--line); border-radius: 11px; background: var(--surface-2); }
|
||
.shift-card strong { display: block; }
|
||
.shift-card span { color: var(--muted); font-size: 12px; }
|
||
.shift-bar { margin-top: 9px; height: 5px; border-radius: 99px; background: var(--surface-3); overflow: hidden; }
|
||
.shift-bar i { display: block; width: 63%; height: 100%; background: var(--success); }
|
||
.main { min-width: 0; }
|
||
.topbar { position: sticky; top: var(--banner); z-index: 20; min-height: 64px; padding: 10px 20px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--surface) 94%, transparent); backdrop-filter: blur(12px); }
|
||
.site strong { display: block; font-size: 15px; }
|
||
.site span { color: var(--muted); font-size: 12px; }
|
||
.top-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
|
||
.btn { min-height: 40px; border: 1px solid var(--line); border-radius: 9px; padding: 8px 13px; background: var(--surface-2); color: var(--text); display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-weight: 750; text-decoration: none; transition: .2s ease; }
|
||
.btn:hover { border-color: var(--primary); }
|
||
.btn:disabled { cursor: not-allowed; opacity: .5; }
|
||
.btn-primary { background: var(--primary-strong); border-color: var(--primary-strong); color: white; }
|
||
.btn-success { background: #117a5d; border-color: #159271; color: white; }
|
||
.btn-danger { background: rgba(251,113,133,.1); border-color: rgba(251,113,133,.38); color: #ffb4c2; }
|
||
.btn-quiet { border-color: transparent; background: transparent; color: var(--muted); }
|
||
.icon-btn { width: 42px; padding: 0; }
|
||
.select, .field, .textarea { width: 100%; min-height: 40px; padding: 8px 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); }
|
||
.textarea { min-height: 80px; resize: vertical; }
|
||
.top-actions .select { width: auto; min-width: 150px; }
|
||
.content { padding: 18px; }
|
||
.view { display: none; animation: reveal .22s ease-out; }
|
||
.view.active { display: block; }
|
||
@keyframes reveal { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||
.page-head { margin-bottom: 15px; display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||
.page-head h1 { margin: 0 0 4px; font-size: 24px; line-height: 1.25; }
|
||
.page-head p { margin: 0; color: var(--muted); }
|
||
.page-actions, .toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||
.card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
|
||
.card-head { min-height: 52px; padding: 11px 14px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||
.card-head h2, .card-head h3 { margin: 0; font-size: 15px; }
|
||
.card-body { padding: 14px; }
|
||
.badge { display: inline-flex; align-items: center; gap: 5px; min-height: 24px; padding: 2px 8px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); font-size: 12px; white-space: nowrap; }
|
||
.badge.success { color: #78edc2; border-color: rgba(52,211,153,.34); background: rgba(52,211,153,.1); }
|
||
.badge.warning { color: #ffe18a; border-color: rgba(251,191,36,.36); background: rgba(251,191,36,.1); }
|
||
.badge.danger { color: #ff9eb0; border-color: rgba(251,113,133,.4); background: rgba(251,113,133,.1); }
|
||
.badge.info { color: #a5e2ff; border-color: rgba(56,189,248,.35); background: rgba(56,189,248,.1); }
|
||
.badge.violet { color: #c9c1ff; border-color: rgba(139,124,246,.38); background: var(--primary-soft); }
|
||
.duty-grid { display: grid; grid-template-columns: 320px minmax(420px, 1fr) 330px; gap: 12px; min-height: calc(100dvh - 156px); }
|
||
.queue, .evidence, .action-panel { min-width: 0; overflow: hidden; }
|
||
.queue-head { padding: 10px; border-bottom: 1px solid var(--line); }
|
||
.queue-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; padding: 4px; border-radius: 9px; background: var(--surface-2); }
|
||
.queue-tabs button { min-height: 34px; border: 0; border-radius: 7px; background: transparent; color: var(--muted); }
|
||
.queue-tabs button.active { background: var(--surface-3); color: var(--text); }
|
||
.alert-list { padding: 8px; display: grid; gap: 7px; max-height: calc(100dvh - 265px); overflow-y: auto; }
|
||
.alert-card { width: 100%; padding: 11px; border: 1px solid transparent; border-radius: 10px; background: transparent; color: var(--text); text-align: left; transition: .18s ease; }
|
||
.alert-card:hover { background: var(--surface-2); }
|
||
.alert-card.selected { background: var(--primary-soft); border-color: rgba(139,124,246,.45); }
|
||
.alert-card-top, .alert-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||
.alert-card h3 { margin: 8px 0 3px; font-size: 14px; }
|
||
.alert-card p { margin: 0; color: var(--muted); font-size: 12px; }
|
||
.alert-card-foot { margin-top: 9px; color: var(--subtle); font-size: 12px; }
|
||
.severity-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
|
||
.evidence-summary { padding: 12px 14px; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--line); }
|
||
.evidence-summary h2 { margin: 0 0 5px; font-size: 18px; }
|
||
.evidence-summary p { margin: 0; color: var(--muted); }
|
||
.evidence-tabs { display: flex; gap: 4px; padding: 8px 12px 0; }
|
||
.evidence-tabs button { min-height: 36px; padding: 0 12px; border: 0; border-radius: 8px 8px 0 0; background: transparent; color: var(--muted); }
|
||
.evidence-tabs button.active { background: var(--surface-2); color: var(--text); }
|
||
.video-stage { position: relative; aspect-ratio: 16/9; margin: 0 12px 12px; background: #02060d; overflow: hidden; border: 1px solid var(--line); border-radius: 10px; }
|
||
.video-stage svg { width: 100%; height: 100%; display: block; }
|
||
.video-controls { position: absolute; inset: auto 0 0; min-height: 44px; padding: 6px 10px; display: flex; align-items: center; gap: 9px; color: white; background: linear-gradient(transparent, rgba(0,0,0,.88)); }
|
||
.video-controls button { width: 34px; height: 34px; border: 0; border-radius: 7px; background: rgba(255,255,255,.08); color: white; }
|
||
.timeline-bar { flex: 1; height: 5px; border-radius: 99px; background: rgba(255,255,255,.22); overflow: hidden; }
|
||
.timeline-bar i { display: block; width: 54%; height: 100%; background: var(--primary); }
|
||
.fact-grid { margin: 0 12px 12px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
|
||
.fact { padding: 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); }
|
||
.fact span { display: block; color: var(--muted); font-size: 11px; }
|
||
.fact strong { display: block; margin-top: 3px; font-size: 13px; font-variant-numeric: tabular-nums; }
|
||
.privacy-note { margin: 0 12px 12px; padding: 9px 10px; display: flex; gap: 8px; border-left: 3px solid var(--cyan); background: rgba(56,189,248,.08); color: var(--muted); font-size: 12px; }
|
||
.action-scroll { max-height: calc(100dvh - 210px); overflow-y: auto; }
|
||
.ack-box { padding: 14px; border-bottom: 1px solid var(--line); }
|
||
.ack-box .btn { width: 100%; min-height: 48px; }
|
||
.ack-box p { margin: 9px 0 0; color: var(--muted); font-size: 12px; }
|
||
.countdown { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center; padding: 12px; margin: 12px; border: 1px solid rgba(251,191,36,.28); border-radius: 10px; background: rgba(251,191,36,.07); }
|
||
.countdown strong { display: block; font-size: 20px; color: #ffe18a; font-variant-numeric: tabular-nums; }
|
||
.countdown span { color: var(--muted); font-size: 12px; }
|
||
.escalation { padding: 0 14px 14px; }
|
||
.escalation h3 { margin: 0 0 10px; font-size: 14px; }
|
||
.step { position: relative; padding: 0 0 16px 27px; }
|
||
.step::before { content: ""; position: absolute; left: 7px; top: 17px; bottom: 0; width: 1px; background: var(--line); }
|
||
.step:last-child::before { display: none; }
|
||
.step-dot { position: absolute; left: 0; top: 3px; width: 15px; height: 15px; border-radius: 50%; border: 3px solid var(--surface); background: var(--subtle); box-shadow: 0 0 0 1px var(--line); }
|
||
.step.done .step-dot { background: var(--success); }
|
||
.step.current .step-dot { background: var(--warning); }
|
||
.step strong { display: block; }
|
||
.step span { color: var(--muted); font-size: 12px; }
|
||
.disposition { padding: 14px; border-top: 1px solid var(--line); display: grid; gap: 9px; }
|
||
.disposition label { color: var(--muted); font-size: 12px; font-weight: 700; }
|
||
.disposition-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||
.grid { display: grid; gap: 12px; }
|
||
.stats { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: 12px; }
|
||
.stat { padding: 15px; min-height: 110px; display: flex; flex-direction: column; justify-content: space-between; }
|
||
.stat span { color: var(--muted); }
|
||
.stat strong { font-size: 27px; font-variant-numeric: tabular-nums; }
|
||
.search { position: relative; min-width: 230px; }
|
||
.search .icon { position: absolute; left: 11px; top: 11px; color: var(--subtle); pointer-events: none; }
|
||
.search input { padding-left: 38px; }
|
||
.table-wrap { overflow-x: auto; }
|
||
table { width: 100%; border-collapse: collapse; min-width: 870px; }
|
||
th, td { padding: 12px 14px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; }
|
||
th { background: var(--surface-2); color: var(--muted); font-size: 12px; }
|
||
td { font-variant-numeric: tabular-nums; }
|
||
tr:last-child td { border-bottom: 0; }
|
||
tr:hover td { background: color-mix(in srgb, var(--primary-soft) 50%, transparent); }
|
||
.rule-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||
.rule-card { padding: 15px; }
|
||
.rule-card-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||
.rule-card h2 { margin: 12px 0 5px; font-size: 16px; }
|
||
.rule-card p { margin: 0 0 14px; color: var(--muted); }
|
||
.rule-card dl { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 7px; color: var(--muted); }
|
||
.rule-card dd { margin: 0; color: var(--text); }
|
||
.rule-card-actions { margin-top: 14px; padding-top: 12px; display: flex; flex-wrap: wrap; gap: 7px; border-top: 1px solid var(--line); }
|
||
.rule-context { margin-bottom: 12px; padding: 11px 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid rgba(56,189,248,.34); border-radius: 10px; background: rgba(56,189,248,.08); }
|
||
.rule-context[hidden] { display: none; }
|
||
.rule-context p { margin: 0; color: var(--muted); }
|
||
.linked-zone { grid-column: 1/-1; padding: 11px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); }
|
||
.linked-zone strong, .linked-zone small { display: block; }
|
||
.linked-zone small { color: var(--muted); }
|
||
.workflow-note { grid-column: 1/-1; margin: 0; padding: 10px 11px; border-left: 3px solid var(--primary); background: var(--primary-soft); color: var(--muted); font-size: 12px; }
|
||
.chain-grid { grid-template-columns: minmax(0, 1.4fr) minmax(300px, .8fr); }
|
||
.chain-row { padding: 13px 14px; display: grid; grid-template-columns: 40px 1fr auto; gap: 11px; align-items: center; border-bottom: 1px solid var(--line); }
|
||
.chain-row:last-child { border-bottom: 0; }
|
||
.chain-num { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; background: var(--primary-soft); color: #cfc8ff; font-weight: 800; }
|
||
.chain-row small { color: var(--muted); }
|
||
.chart-grid { grid-template-columns: 1.35fr .65fr; }
|
||
.chart { padding: 16px; }
|
||
.chart h2 { margin: 0; font-size: 15px; }
|
||
.chart p { margin: 4px 0 14px; color: var(--muted); }
|
||
.bars { height: 220px; display: flex; align-items: end; gap: 16px; padding: 10px 6px 26px; border-bottom: 1px solid var(--line); }
|
||
.bar { position: relative; flex: 1; min-width: 24px; height: var(--h); border-radius: 7px 7px 0 0; background: linear-gradient(180deg, var(--primary), #4f46a8); }
|
||
.bar span { position: absolute; inset: calc(100% + 5px) 0 auto; text-align: center; color: var(--muted); font-size: 11px; }
|
||
.donut { width: 170px; height: 170px; margin: 18px auto; border-radius: 50%; background: conic-gradient(var(--success) 0 61%, var(--warning) 61% 83%, var(--danger) 83%); position: relative; }
|
||
.donut::after { content: "83%\A 24h 内闭环"; white-space: pre; position: absolute; inset: 27px; border-radius: 50%; display: grid; place-items: center; text-align: center; background: var(--surface); color: var(--text); font-weight: 800; }
|
||
.legend { display: flex; flex-wrap: wrap; gap: 10px; color: var(--muted); font-size: 12px; }
|
||
.legend i { display: inline-block; width: 9px; height: 9px; margin-right: 5px; border-radius: 2px; }
|
||
.state-layer { display: none; min-height: 420px; place-items: center; padding: 24px; border: 1px dashed var(--line); border-radius: var(--radius); background: var(--surface); text-align: center; }
|
||
.state-layer.show { display: grid; }
|
||
.state-layer .icon { width: 40px; height: 40px; color: var(--primary); }
|
||
.state-layer h2 { margin: 12px 0 4px; font-size: 18px; }
|
||
.state-layer p { margin: 0 0 14px; max-width: 460px; color: var(--muted); }
|
||
.skeleton { width: min(460px, 90%); display: grid; gap: 10px; }
|
||
.skeleton i { height: 15px; border-radius: 6px; background: linear-gradient(90deg, var(--surface-3), var(--line), var(--surface-3)); background-size: 200% 100%; animation: shimmer 1.4s infinite; }
|
||
.skeleton i:nth-child(2) { width: 72%; }
|
||
@keyframes shimmer { to { background-position: -200% 0; } }
|
||
dialog { width: min(610px, calc(100% - 28px)); max-height: 88dvh; padding: 0; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); color: var(--text); box-shadow: 0 24px 80px rgba(0,0,0,.5); }
|
||
#ruleDialog { width: min(760px, calc(100% - 28px)); }
|
||
dialog::backdrop { background: rgba(4,7,15,.68); backdrop-filter: blur(4px); }
|
||
.dialog-head, .dialog-foot { padding: 14px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||
.dialog-head h2 { margin: 0; font-size: 17px; }
|
||
.dialog-body { padding: 18px; overflow: auto; }
|
||
.dialog-foot { border: 0; border-top: 1px solid var(--line); justify-content: flex-end; }
|
||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.form-field { display: grid; gap: 6px; }
|
||
.form-field.full { grid-column: 1/-1; }
|
||
.form-field label { color: var(--muted); font-size: 12px; font-weight: 700; }
|
||
.toast { position: fixed; right: 20px; bottom: 20px; z-index: 1100; max-width: min(420px, calc(100% - 40px)); padding: 12px 14px; border: 1px solid rgba(52,211,153,.35); border-radius: 10px; background: #0b2c25; color: #d9fff3; box-shadow: var(--shadow); transform: translateY(20px); opacity: 0; pointer-events: none; transition: .2s ease; }
|
||
.toast.show { transform: none; opacity: 1; }
|
||
.muted { color: var(--muted); }
|
||
.mono { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-variant-numeric: tabular-nums; }
|
||
.mobile-nav { display: none; }
|
||
@media (max-width: 1240px) {
|
||
:root { --sidebar: 78px; }
|
||
.brand-copy, .nav span:not(.count), .nav-label, .shift-card { display: none; }
|
||
.brand { justify-content: center; padding-inline: 0; }
|
||
.nav button { justify-content: center; padding: 0; }
|
||
.nav .count { position: absolute; margin: -24px 0 0 25px; }
|
||
.duty-grid { grid-template-columns: 285px minmax(390px, 1fr); }
|
||
.action-panel { grid-column: 1/-1; }
|
||
.action-scroll { max-height: none; display: grid; grid-template-columns: rep…4257 tokens truncated…t>覆盖范围</dt><dd>3 个点位</dd><dt>检测区域</dt><dd>3 个区域版本</dd><dt>继承来源</dt><dd>设备覆盖</dd><dt>样本命中</dt><dd>14</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>查看试运行</button><button class="btn btn-primary" id="publishTrialRule">正式发布</button></div></article>
|
||
<article class="card rule-card"><div class="rule-card-top"><span class="badge violet">草稿</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>方向越线</h2><p>警戒线 A → B × 上学时段</p><dl><dt>覆盖范围</dt><dd>1 个点位</dd><dt>检测区域</dt><dd>宿舍东通道 · 警戒线 v3</dd><dt>继承来源</dt><dd>设备草稿</dd><dt>最近修改</dt><dd>2h 前</dd></dl><div class="rule-card-actions"><button class="btn" data-edit-rule>继续编辑</button><a class="btn" href="../sense/index.html?view=device-detail&camera=CAM-S2-011&return=bell-rule">查看警戒线</a></div></article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="view" data-page="chains">
|
||
<div class="page-head"><div><h1>升级链</h1><p>生产前至少两条独立路径;无回执不能当作成功。</p></div><div class="page-actions"><button class="btn btn-primary">编辑升级链</button></div></div>
|
||
<div class="grid chain-grid"><article class="card"><div class="card-head"><h2>校园高危事件 · 默认链</h2><span class="badge success">已发布 v4</span></div>
|
||
<div class="chain-row"><div class="chain-num">1</div><div><strong>值班室 Web + 本地声光</strong><small>立即 · 站点局域网内</small></div><span class="badge success">主路径</span></div>
|
||
<div class="chain-row"><div class="chain-num">2</div><div><strong>当班值班员短信</strong><small>立即 · 送达后等待 2 分钟</small></div><span class="badge info">独立路径</span></div>
|
||
<div class="chain-row"><div class="chain-num">3</div><div><strong>校级负责人语音</strong><small>未 ack 时升级 · 等待 3 分钟</small></div><span class="badge warning">兜底</span></div>
|
||
</article><article class="card"><div class="card-head"><h2>静默规则</h2><span class="badge info">无永久选项</span></div><div class="card-body"><label for="muteDuration" class="muted">维护窗口</label><select id="muteDuration" class="select" style="margin-top:6px"><option>30 分钟</option><option>1 小时</option><option>2 小时</option><option>4 小时(上限)</option></select><p class="muted">静默仅影响选定规则和点位,到时自动恢复;S1 事件不可静默。</p><button class="btn">创建维护窗口</button></div></article></div>
|
||
</section>
|
||
|
||
<section class="view" data-page="reports">
|
||
<div class="page-head"><div><h1>运营报表</h1><p>算法效果和系统 SLA 分开统计;图表同时提供表格导出。</p></div><div class="page-actions"><button class="btn"><svg class="icon"><use href="#i-download"/></svg>导出 CSV</button></div></div>
|
||
<div class="grid chart-grid"><article class="card chart"><h2>近 7 日预警量</h2><p>按站点日期汇总;可切换到每路每天误报数。</p><div class="bars" role="img" aria-label="近七日预警量依次为 18、22、15、31、27、24、19"><div class="bar" style="--h:58%"><span>周一</span></div><div class="bar" style="--h:70%"><span>周二</span></div><div class="bar" style="--h:48%"><span>周三</span></div><div class="bar" style="--h:100%"><span>周四</span></div><div class="bar" style="--h:87%"><span>周五</span></div><div class="bar" style="--h:77%"><span>周六</span></div><div class="bar" style="--h:61%"><span>周日</span></div></div></article><article class="card chart"><h2>处置结果</h2><p>本月已闭环事件。</p><div class="donut" role="img" aria-label="正常处置 61%,误报 22%,升级未闭环 17%"></div><div class="legend"><span><i style="background:var(--success)"></i>正常处置 61%</span><span><i style="background:var(--warning)"></i>误报 22%</span><span><i style="background:var(--danger)"></i>未闭环 17%</span></div></article></div>
|
||
</section>
|
||
|
||
<section class="view" data-page="audit">
|
||
<div class="page-head"><div><h1>审计日志</h1><p>查询、ack、规则发布和证据下载均可追溯;不记录密码与可复用连接串。</p></div><div class="page-actions"><button class="btn">导出审计记录</button></div></div>
|
||
<article class="card"><div class="card-head"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" aria-label="搜索审计日志" placeholder="操作、角色或对象编号"></div><span class="badge info">只读</span></div><div class="table-wrap"><table><thead><tr><th>时间</th><th>角色</th><th>动作</th><th>对象</th><th>结果</th><th>来源</th></tr></thead><tbody>
|
||
<tr><td>23:14:18</td><td>站点值班员</td><td>ACK_ALERT</td><td class="mono">ALT-1842</td><td><span class="badge success">成功</span></td><td>值班室 Web</td></tr><tr><td>22:52:06</td><td>站点管理员</td><td>PUBLISH_RULE</td><td class="mono">RULE-007</td><td><span class="badge success">成功</span></td><td>管理端</td></tr><tr><td>22:20:31</td><td>只读用户</td><td>VIEW_EVIDENCE</td><td class="mono">EVT-01J8…52C</td><td><span class="badge danger">拒绝</span></td><td>响应式 H5</td></tr>
|
||
</tbody></table></div></article>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<nav class="mobile-nav" aria-label="移动端主导航"><button class="active" data-view="duty">值班</button><button data-view="events">事件</button><button data-view="rules">规则</button><button data-view="chains">升级</button><button data-view="reports">报表</button></nav>
|
||
|
||
<dialog id="ruleDialog" aria-labelledby="ruleTitle"><form method="dialog"><div class="dialog-head"><div><h2 id="ruleTitle">配置告警规则</h2><span class="muted">规则草稿 · 尚未正式生效</span></div><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><div class="form-grid">
|
||
<div class="form-field"><label for="ruleScene">场景模板</label><select id="ruleScene" class="select"><option>非授权人员进入</option><option>区域内聚集</option><option>方向越线</option></select></div>
|
||
<div class="form-field"><label for="ruleScope">继承与覆盖</label><select id="ruleScope" class="select"><option>当前设备覆盖</option><option>继承站点模板</option><option>覆盖站点模板</option></select></div>
|
||
<div class="form-field"><label for="ruleCamera">摄像头</label><select id="ruleCamera" class="select"><option value="CAM-S2-001">北围墙 01</option><option value="CAM-S2-006">实验楼入口 02</option><option value="CAM-S2-011">宿舍东通道</option></select></div>
|
||
<div class="form-field"><label for="ruleZone">检测区域</label><select id="ruleZone" class="select"><option value="ZONE-007">北围墙危险区域 · v7</option><option>北围墙方向警戒线 · v3</option></select></div>
|
||
<div class="linked-zone"><span><strong>空间坐标由摄像头详情维护</strong><small>区域版本变化不会绕过规则试运行或自动正式发布。</small></span><a class="btn" id="editRuleZone" href="../sense/index.html?view=device-detail&camera=CAM-S2-001&return=bell-rule">编辑检测区域</a></div>
|
||
<div class="form-field"><label for="ruleSchedule">生效时段</label><select id="ruleSchedule" class="select"><option>全天</option><option>夜间 22:00–06:00</option><option>自定义课表</option></select></div>
|
||
<div class="form-field"><label for="ruleDuration">持续时间</label><select id="ruleDuration" class="select"><option>超过 3 秒</option><option>立即</option><option>超过 10 秒</option><option>超过 20 秒</option></select></div>
|
||
<div class="form-field full"><label><input id="startDryRun" type="checkbox" checked aria-label="保存后开始试运行"> 保存后开始试运行,不触发正式预警</label></div>
|
||
<p class="workflow-note">产品流程:先确认摄像头和检测区域,再配置业务条件;试运行样本达到现场评审要求后,才允许正式发布。</p>
|
||
</div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-primary" id="saveRuleDraft" value="default">保存并开始试运行</button></div></form></dialog>
|
||
|
||
<dialog id="falseDialog" aria-labelledby="falseTitle"><form method="dialog"><div class="dialog-head"><h2 id="falseTitle">标记为误报</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><p>这只会追加事件 outcome,不会修改原始事件事实和证据。</p><div class="form-grid"><div class="form-field full"><label for="falseReason">误报原因</label><select id="falseReason" class="select" required><option value="">请选择</option><option>区域配置不准确</option><option>目标分类错误</option><option>时段策略不准确</option><option>其他</option></select></div><div class="form-field full"><label for="falseNote">补充说明(可选)</label><textarea id="falseNote" class="textarea" placeholder="便于算法团队归因,不填写真实个人信息"></textarea></div><div class="form-field full"><label><input type="checkbox" checked> 进入标注复核队列</label></div></div></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-danger" id="confirmFalse" value="default">确认标记</button></div></form></dialog>
|
||
|
||
<dialog id="muteDialog" aria-labelledby="muteTitle"><form method="dialog"><div class="dialog-head"><h2 id="muteTitle">创建限时静默</h2><button class="btn btn-quiet icon-btn" value="cancel" aria-label="关闭对话框">×</button></div><div class="dialog-body"><div class="form-grid"><div class="form-field"><label for="muteScope">影响范围</label><select id="muteScope" class="select"><option>北围墙 01 · 当前规则</option><option>北围墙区域</option></select></div><div class="form-field"><label for="muteTime">持续时间</label><select id="muteTime" class="select"><option>30 分钟</option><option>1 小时</option><option>2 小时</option><option>4 小时(上限)</option></select></div><div class="form-field full"><label for="muteCause">原因</label><textarea id="muteCause" class="textarea" required placeholder="例如:现场施工维护,已安排人工巡查"></textarea></div></div><p class="muted">到期自动恢复;S1 高危事件与设备离线运维告警不受此静默影响。</p></div><div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-primary" id="confirmMute" value="default">确认并记录审计</button></div></form></dialog>
|
||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||
|
||
<script>
|
||
const $ = (selector, root = document) => root.querySelector(selector);
|
||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||
const toast = $('#toast'); let toastTimer;
|
||
function notify(message) { toast.textContent = message; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 3800); }
|
||
|
||
const alerts = [
|
||
{id:'ALT-1842',severity:'S1 高危',badge:'danger',title:'危险区域闯入',site:'北围墙 01',time:'23:14:08',age:'1 分钟',rule:'非授权人员进入',delivery:'短信已送达'},
|
||
{id:'ALT-1841',severity:'S1 高危',badge:'danger',title:'方向越线',site:'宿舍东通道',time:'23:12:46',age:'3 分钟',rule:'夜间越线 A → B',delivery:'声光已触发'},
|
||
{id:'ALT-1839',severity:'S2 重要',badge:'warning',title:'区域聚集',site:'操场西北角',time:'23:08:21',age:'7 分钟',rule:'人数 ≥ 6 持续 20 秒',delivery:'Web 已看到'},
|
||
{id:'ALT-1836',severity:'S2 重要',badge:'warning',title:'危险区域滞留',site:'配电房门口',time:'22:57:03',age:'18 分钟',rule:'区域内停留 ≥ 10 秒',delivery:'等待短信回执'}
|
||
];
|
||
const alertList = $('#alertList');
|
||
function renderAlerts(selected=0) { alertList.innerHTML = alerts.map((a,i)=>`<button class="alert-card ${i===selected?'selected':''}" data-index="${i}"><div class="alert-card-top"><span class="badge ${a.badge}"><i class="severity-dot"></i>${a.severity}</span><span class="muted">${a.age}</span></div><h3>${a.title}</h3><p>${a.site} · ${a.time}</p><div class="alert-card-foot"><span>${a.delivery}</span><span class="mono">${a.id}</span></div></button>`).join(''); $$('.alert-card', alertList).forEach(b=>b.addEventListener('click',()=>selectAlert(Number(b.dataset.index)))); }
|
||
function selectAlert(index) { renderAlerts(index); const a=alerts[index]; $('#selectedSeverity').className=`badge ${a.badge}`; $('#selectedSeverity').textContent=a.severity; $('#selectedTitle').textContent=a.title; $('#selectedMeta').textContent=`${a.site} · ${a.time} · ${a.id}`; $('#factRule').textContent=a.rule; $('#ackState').className='badge warning'; $('#ackState').textContent='等待 ack'; $('#ackButton').disabled=false; $('#ackButton').innerHTML='<svg class="icon"><use href="#i-check"/></svg>确认接手'; $('#ackHelp').textContent='一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。'; }
|
||
renderAlerts();
|
||
|
||
function switchView(name) { $$('.view').forEach(v=>v.classList.toggle('active',v.dataset.page===name)); $$('[data-view]').forEach(b=>{const on=b.dataset.view===name;b.toggleAttribute('aria-current',on);b.classList.toggle('active',on);}); $('#stateSelect').value='normal'; renderState('normal'); $('#main-content').focus({preventScroll:true}); }
|
||
$$('[data-view]').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));
|
||
$$('.queue-tabs button').forEach(b=>b.addEventListener('click',()=>{$$('.queue-tabs button').forEach(x=>{x.classList.toggle('active',x===b);x.setAttribute('aria-selected',x===b)});notify(`队列已切换到“${b.textContent}”`);}));
|
||
$$('.evidence-tabs button').forEach(b=>b.addEventListener('click',()=>{$$('.evidence-tabs button').forEach(x=>{x.classList.toggle('active',x===b);x.setAttribute('aria-selected',x===b)});notify(`正在演示“${b.textContent}”标签结构`);}));
|
||
|
||
function renderState(state) { const layer=$('#stateLayer'), views=$$('.view'); layer.classList.toggle('show',state!=='normal'); views.forEach(v=>v.style.display=state==='normal'?'':'none'); if(state==='normal')return; const states={
|
||
loading:`<div><div class="skeleton" aria-label="正在加载"><i></i><i></i><i></i><i></i></div><h2>正在读取最新预警状态</h2><p>超过 300ms 显示进度;ack 和升级事实始终从服务端重新读取。</p></div>`,
|
||
weak:`<div><svg class="icon" style="color:var(--warning)"><use href="#i-alert"/></svg><h2>结构化事件已到达,视频证据暂不可用</h2><p>弱网下先显示时间、点位、规则和升级链。视频片段可单独重试,不阻塞确认接手。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal');notify('证据重试任务已加入后台队列')">后台重试证据</button></div>`,
|
||
empty:`<div><svg class="icon"><use href="#i-check"/></svg><h2>当前没有待处置预警</h2><p>系统在线,新的 Alert 会实时进入队列。历史事件仍可从事件中心查询。</p><button class="btn" onclick="switchView('events')">查看事件中心</button></div>`,
|
||
denied:`<div><svg class="icon" style="color:var(--danger)"><use href="#i-lock"/></svg><h2>没有查看此站点证据的权限</h2><p>当前账号只能查看被授权站点。系统不会泄露事件是否存在、流地址或设备信息。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal')">返回可访问范围</button></div>`}; layer.innerHTML=states[state]; }
|
||
$('#stateSelect').addEventListener('change',e=>renderState(e.target.value));
|
||
$('#themeToggle').addEventListener('click',()=>{const h=document.documentElement;h.dataset.theme=h.dataset.theme==='dark'?'light':'dark';notify(`已切换为${h.dataset.theme==='dark'?'深色':'浅色'}主题`);});
|
||
|
||
$('#ackButton').addEventListener('click',async e=>{const b=e.currentTarget;b.disabled=true;b.textContent='确认中…';await new Promise(r=>setTimeout(r,650));b.innerHTML='<svg class="icon"><use href="#i-check"/></svg>已由当前值班员接手';$('#ackState').className='badge success';$('#ackState').textContent='已 ack';$('#ackHelp').textContent='23:15:02 · 当前值班员 · 服务端确认成功';notify('确认成功,升级计时已停止并记录审计。');});
|
||
$('#completeAlert').addEventListener('click',()=>{if(!$('#outcome').value){notify('请先选择处置结果。');$('#outcome').focus();return;}if($('#ackState').textContent!=='已 ack'){notify('请先确认接手,再完成处置。');$('#ackButton').focus();return;}notify('处置结果已追加,原始事件事实未被修改。');});
|
||
$('#falsePositive').addEventListener('click',()=>$('#falseDialog').showModal());
|
||
$('#confirmFalse').addEventListener('click',e=>{if(!$('#falseReason').value){e.preventDefault();notify('请选择误报原因。');$('#falseReason').focus();return;}notify('已标记误报并进入复核队列,原始事件保持不变。');});
|
||
$('#muteButton').addEventListener('click',()=>$('#muteDialog').showModal());
|
||
$('#confirmMute').addEventListener('click',e=>{if(!$('#muteCause').value.trim()){e.preventDefault();notify('请填写静默原因。');$('#muteCause').focus();return;}notify('已创建限时静默,到期将自动恢复并通知值班组。');});
|
||
|
||
const ruleDialog = $('#ruleDialog');
|
||
function openRuleEditor() { ruleDialog.showModal(); }
|
||
$('#openRuleDialog').addEventListener('click', openRuleEditor);
|
||
$('#continueRuleDraft').addEventListener('click', openRuleEditor);
|
||
$$('[data-edit-rule]').forEach(button=>button.addEventListener('click', openRuleEditor));
|
||
$('#ruleCamera').addEventListener('change',e=>{ $('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent(e.target.value)}&return=bell-rule`; notify('已切换摄像头,请确认检测区域版本与画面仍匹配。'); });
|
||
$('#saveRuleDraft').addEventListener('click',()=>notify($('#startDryRun').checked?'规则草稿已保存并进入试运行,不会触发正式预警。':'规则草稿已保存,尚未开始试运行。'));
|
||
$('#publishTrialRule').addEventListener('click',()=>{if(confirm('正式发布后将影响 3 个点位并启动预警链。确认发布当前试运行版本?')){$('#trialBadge').className='badge success';$('#trialBadge').textContent='正式生效';notify('规则版本已正式发布,变更已记录审计并可回滚。');}});
|
||
|
||
let remain=102;
|
||
setInterval(()=>{if($('#ackState').textContent==='已 ack'||remain<=0)return;remain--;const m=String(Math.floor(remain/60)).padStart(2,'0'),s=String(remain%60).padStart(2,'0');$('#countdown').textContent=`${m}:${s}`;},1000);
|
||
const routeParams = new URLSearchParams(location.search);
|
||
if (routeParams.get('view')) switchView(routeParams.get('view'));
|
||
if (routeParams.has('camera')) {
|
||
switchView('rules'); $('#ruleContext').hidden=false;
|
||
const camera=routeParams.get('camera'); if([...$('#ruleCamera').options].some(option=>option.value===camera)) $('#ruleCamera').value=camera;
|
||
$('#editRuleZone').href=`../sense/index.html?view=device-detail&camera=${encodeURIComponent(camera)}&return=bell-rule`;
|
||
notify('已保留摄像头与检测区域上下文,可继续配置规则。');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|