docs(design): add Sense and Bell prototypes
Harness governance / validate (push) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled

This commit is contained in:
QiuSW
2026-08-04 00:23:55 +08:00
parent b643e42535
commit f43a4efa80
3 changed files with 970 additions and 0 deletions
+443
View File
@@ -0,0 +1,443 @@
<!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; 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); }
.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); }
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: 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; }
.content { padding: 12px; }
.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)); }
.rule-grid { grid-template-columns: 1fr 1fr; }
.fact-grid { grid-template-columns: repeat(2, 1fr); }
.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; }
.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">Operations</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>
<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>客户侧私有实例 · 当前仅匿名安全规则</span></div>
<div class="top-actions">
<select class="select" id="stateSelect" aria-label="原型状态演示"><option value="normal">正常状态</option><option value="loading">加载中</option><option value="weak">弱网 / 证据失败</option><option value="empty">队列为空</option><option value="denied">无权限</option></select>
<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="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" role="tablist"><button class="active" role="tab" aria-selected="true">全部 9</button><button role="tab">高危 3</button><button role="tab">我的 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" role="tablist"><button class="active" role="tab" aria-selected="true">视频片段</button><button role="tab">抓拍 2</button><button role="tab">结构化事实</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="privacy-note"><svg class="icon"><use href="#i-lock"/></svg><span>仅显示与本事件关联的 20 秒证据。常态录像仍在客户 NVR,本页不暴露流地址或摄像头凭据。</span></div>
</section>
<aside class="card action-panel" aria-label="预警确认与处置">
<div class="card-head"><h2>处置与升级</h2><span class="badge warning" id="ackState">等待 ack</span></div>
<div class="action-scroll">
<div class="ack-box"><button class="btn btn-primary" id="ackButton"><svg class="icon"><use href="#i-check"/></svg>确认接手</button><p id="ackHelp">一次点击确认;并发确认时会显示实际接手人,不覆盖对方结果。</p></div>
<div><div class="countdown"><svg class="icon" style="color:var(--warning);width:26px;height:26px"><use href="#i-clock"/></svg><div><strong id="countdown">01:42</strong><span>未 ack 将升级到校级负责人</span></div></div>
<div class="escalation"><h3>投递时间线</h3>
<div class="step done"><i class="step-dot"></i><strong>值班室 Web + 本地声光</strong><span>23:14:10 · 已送达</span></div>
<div class="step current"><i class="step-dot"></i><strong>值班员短信</strong><span>23:14:13 · 已送达,未确认</span></div>
<div class="step"><i class="step-dot"></i><strong>校级负责人语音</strong><span>预计 23:16:10 · 尚未发送</span></div>
</div>
</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>
<tr><td class="mono">EVT-01J8…7A2</td><td>23:14:08</td><td>北围墙 01</td><td>非授权人员进入</td><td><span class="badge danger">已确认</span></td><td>Alert #1842</td><td>处置中</td><td><span class="badge success">可用</span></td></tr>
<tr><td class="mono">EVT-01J8…69P</td><td>22:48:31</td><td>宿舍东通道</td><td>区域聚集</td><td><span class="badge info">已记录</span></td><td>未触发</td><td>试运行样本</td><td><span class="badge success">可用</span></td></tr>
<tr><td class="mono">EVT-01J8…52C</td><td>22:19:04</td><td>实验楼入口</td><td>危险区域闯入</td><td><span class="badge danger">已确认</span></td><td>Alert #1839</td><td>误报</td><td><span class="badge warning">重试中</span></td></tr>
</tbody></table></div></article>
</section>
<section class="view" data-page="rules">
<div class="page-head"><div><h1>规则策略</h1><p>继承来源、试运行和正式发布清晰分开;空间区域在 Sense 编辑。</p></div><div class="page-actions"><button class="btn btn-primary">新建规则</button></div></div>
<div class="grid rule-grid">
<article class="card rule-card"><div class="rule-card-top"><span class="badge success">正式生效</span><button class="btn btn-quiet icon-btn" aria-label="更多操作"><svg class="icon"><use href="#i-more"/></svg></button></div><h2>非授权人员进入</h2><p>危险区域 × 全天 × 持续 3 秒</p><dl><dt>覆盖范围</dt><dd>4 个点位</dd><dt>继承来源</dt><dd>站点模板</dd><dt>今日命中</dt><dd>8</dd></dl></article>
<article class="card rule-card"><div class="rule-card-top"><span class="badge warning">试运行</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>设备覆盖</dd><dt>样本命中</dt><dd>14</dd></dl></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>设备草稿</dd><dt>最近修改</dt><dd>2h 前</dd></dl></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="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('已创建限时静默,到期将自动恢复并通知值班组。');});
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);
</script>
</body>
</html>
+513
View File
@@ -0,0 +1,513 @@
<!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 Sense · 设备与视频接入原型</title>
<style>
:root {
--bg: #08111f;
--surface: #0d1929;
--surface-2: #111f31;
--surface-3: #17263a;
--text: #f2f7fb;
--muted: #9fb0c5;
--subtle: #71849c;
--line: #27384d;
--primary: #38bdf8;
--primary-strong: #0ea5e9;
--primary-soft: rgba(56, 189, 248, .14);
--success: #34d399;
--warning: #fbbf24;
--danger: #fb7185;
--violet: #a78bfa;
--shadow: 0 16px 40px rgba(0, 0, 0, .22);
--radius: 14px;
--sidebar: 224px;
--banner: 28px;
}
html[data-theme="light"] {
--bg: #eef4f8;
--surface: #ffffff;
--surface-2: #f7fafc;
--surface-3: #eaf1f6;
--text: #112033;
--muted: #50647b;
--subtle: #718096;
--line: #cfdae5;
--primary-soft: rgba(2, 132, 199, .11);
--shadow: 0 14px 34px rgba(38, 64, 92, .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(56, 189, 248, .45);
outline-offset: 2px;
}
.prototype-banner {
position: fixed; inset: 0 0 auto 0; z-index: 1000;
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); height: calc(100dvh - var(--banner));
border-right: 1px solid var(--line); background: var(--surface); padding: 18px 12px;
display: flex; flex-direction: column; z-index: 30;
}
.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, #38bdf8, #6366f1); color: white; font-weight: 900; box-shadow: 0 8px 20px rgba(14, 165, 233, .22); }
.brand strong { display: block; font-size: 16px; letter-spacing: .01em; }
.brand small { color: var(--muted); }
.nav-label { margin: 8px 10px 6px; color: var(--subtle); font-size: 11px; font-weight: 700; 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: background .2s ease, color .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; }
.sidebar-foot { margin-top: auto; padding: 12px 10px 2px; border-top: 1px solid var(--line); color: var(--muted); }
.online-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); box-shadow: 0 0 0 4px rgba(52, 211, 153, .12); }
.main { min-width: 0; }
.topbar {
position: sticky; top: var(--banner); z-index: 20; min-height: 64px; padding: 10px 22px;
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-select { display: flex; align-items: center; gap: 8px; min-width: 250px; }
.site-select strong { font-size: 15px; }
.site-select span { color: var(--muted); font-size: 12px; }
.top-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.select, .field, .textarea {
width: 100%; min-height: 40px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--text); padding: 8px 11px;
}
.textarea { min-height: 74px; resize: vertical; }
.top-actions .select { width: auto; min-width: 126px; }
.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: 700; transition: border-color .2s ease, background .2s ease, opacity .2s ease;
}
.btn:hover { border-color: var(--primary); }
.btn-primary { background: var(--primary-strong); border-color: var(--primary-strong); color: #fff; }
.btn-danger { color: #ffd8df; border-color: rgba(251, 113, 133, .45); background: rgba(251, 113, 133, .1); }
.btn-quiet { border-color: transparent; background: transparent; color: var(--muted); }
.icon-btn { width: 42px; padding: 0; }
.content { padding: 22px; }
.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 { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
.page-head h1 { margin: 0 0 4px; font-size: 24px; line-height: 1.25; }
.page-head p { margin: 0; color: var(--muted); }
.page-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.grid { display: grid; gap: 14px; }
.stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.card { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow); }
.stat { padding: 16px; min-height: 116px; display: flex; flex-direction: column; justify-content: space-between; }
.stat-top { display: flex; align-items: center; justify-content: space-between; color: var(--muted); }
.stat strong { font-size: 27px; font-variant-numeric: tabular-nums; }
.trend { font-size: 12px; color: var(--success); }
.split { grid-template-columns: minmax(0, 1.6fr) minmax(300px, .8fr); margin-top: 14px; }
.card-head { padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-bottom: 1px solid var(--line); }
.card-head h2, .card-head h3 { margin: 0; font-size: 15px; }
.card-body { padding: 16px; }
.health-list { display: grid; gap: 12px; }
.health-row { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; }
.health-row span { color: var(--muted); }
.health-row strong { font-variant-numeric: tabular-nums; }
.meter { grid-column: 1/-1; height: 7px; border-radius: 99px; background: var(--surface-3); overflow: hidden; }
.meter i { display: block; height: 100%; border-radius: inherit; background: var(--primary); }
.badge { display: inline-flex; align-items: center; gap: 5px; min-height: 24px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); font-size: 12px; white-space: nowrap; }
.badge.success { color: #74efc1; border-color: rgba(52, 211, 153, .35); background: rgba(52, 211, 153, .1); }
.badge.warning { color: #ffdc74; border-color: rgba(251, 191, 36, .35); background: rgba(251, 191, 36, .1); }
.badge.danger { color: #ff9aae; border-color: rgba(251, 113, 133, .4); background: rgba(251, 113, 133, .1); }
.badge.info { color: #8edaff; border-color: rgba(56, 189, 248, .35); background: var(--primary-soft); }
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.search { position: relative; min-width: 220px; }
.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: 820px; }
th, td { padding: 12px 14px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle; }
th { color: var(--muted); background: var(--surface-2); font-size: 12px; font-weight: 700; }
td { font-variant-numeric: tabular-nums; }
tr:last-child td { border-bottom: 0; }
tr:hover td { background: color-mix(in srgb, var(--primary-soft) 45%, transparent); }
.device-name { display: flex; align-items: center; gap: 10px; }
.camera-icon { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--surface-3); color: var(--primary); }
.device-name small { display: block; color: var(--muted); }
.camera-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.camera-card { overflow: hidden; }
.camera-stage { aspect-ratio: 16/9; position: relative; background: #020914; overflow: hidden; }
.camera-stage::before { content: ""; position: absolute; inset: 0; background: linear-gradient(160deg, transparent 45%, rgba(56,189,248,.08)), repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 4px); }
.scene { position: absolute; inset: 0; width: 100%; height: 100%; }
.camera-meta { padding: 12px 14px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.camera-meta small { color: var(--muted); }
.zone-layout { display: grid; grid-template-columns: 230px minmax(420px, 1fr) 310px; gap: 12px; min-height: calc(100dvh - 162px); }
.zone-camera-list, .zone-settings, .zone-workspace { min-width: 0; }
.compact-list { display: grid; gap: 6px; padding: 10px; }
.compact-list button { width: 100%; min-height: 54px; border: 1px solid transparent; border-radius: 9px; background: transparent; color: var(--text); display: flex; align-items: center; gap: 10px; padding: 8px; text-align: left; }
.compact-list button:hover, .compact-list button.selected { background: var(--primary-soft); border-color: rgba(56,189,248,.32); }
.compact-list small { display: block; color: var(--muted); }
.mini-thumb { width: 54px; height: 34px; border-radius: 6px; background: linear-gradient(145deg, #24364c, #0a101a); position: relative; overflow: hidden; }
.mini-thumb::after { content: ""; position: absolute; left: 8px; right: 8px; bottom: 8px; height: 1px; background: var(--primary); transform: rotate(-12deg); }
.editor-tools { padding: 10px; display: flex; gap: 6px; align-items: center; border-bottom: 1px solid var(--line); overflow-x: auto; }
.tool-btn { min-width: 42px; min-height: 40px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); color: var(--muted); display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; }
.tool-btn.active { background: var(--primary-soft); border-color: var(--primary); color: var(--text); }
.tool-divider { width: 1px; align-self: stretch; background: var(--line); margin: 0 3px; }
.editor-stage { position: relative; aspect-ratio: 16/9; min-height: 340px; background: #030a12; overflow: hidden; }
.editor-stage svg { width: 100%; height: 100%; display: block; touch-action: none; }
.editor-hint { position: absolute; left: 12px; bottom: 12px; padding: 7px 10px; border-radius: 8px; background: rgba(3, 10, 18, .78); color: #dceaff; font-size: 12px; pointer-events: none; }
.editor-status { display: flex; justify-content: space-between; gap: 10px; padding: 10px 12px; color: var(--muted); border-top: 1px solid var(--line); font-size: 12px; }
.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; }
.switch-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 42px; }
.switch { position: relative; width: 42px; height: 24px; }
.switch input { position: absolute; opacity: 0; }
.switch span { position: absolute; inset: 0; border-radius: 99px; background: var(--surface-3); border: 1px solid var(--line); transition: .2s ease; }
.switch span::after { content: ""; position: absolute; width: 18px; height: 18px; left: 2px; top: 2px; border-radius: 50%; background: var(--muted); transition: transform .2s ease, background .2s ease; }
.switch input:checked + span { background: var(--primary-strong); border-color: var(--primary-strong); }
.switch input:checked + span::after { transform: translateX(18px); background: white; }
.coordinate-box { margin-top: 12px; padding: 12px; border: 1px dashed var(--line); border-radius: 10px; background: var(--surface-2); }
.coordinate-box h3 { margin: 0 0 4px; font-size: 13px; }
.coordinate-box p { margin: 0 0 10px; color: var(--muted); font-size: 12px; }
.coordinate-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 6px; }
.settings-actions { position: sticky; bottom: 0; margin: 14px -16px -16px; padding: 12px 16px; display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--line); background: var(--surface); }
.drop-zone { padding: 28px; border: 1px dashed var(--line); border-radius: 12px; text-align: center; background: var(--surface-2); }
.drop-zone .icon { width: 28px; height: 28px; color: var(--primary); }
.drop-zone p { margin: 8px 0 12px; color: var(--muted); }
.system-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.system-tile { padding: 16px; }
.system-tile h3 { margin: 0 0 12px; font-size: 14px; }
.system-tile dl { margin: 0; display: grid; grid-template-columns: 1fr auto; gap: 8px; color: var(--muted); }
.system-tile dd { margin: 0; color: var(--text); font-variant-numeric: tabular-nums; }
.state-layer { display: none; min-height: 380px; place-items: center; border: 1px dashed var(--line); border-radius: var(--radius); background: var(--surface); text-align: center; padding: 24px; }
.state-layer.show { display: grid; }
.state-layer .icon { width: 38px; height: 38px; color: var(--primary); }
.state-layer h2 { margin: 12px 0 4px; font-size: 18px; }
.state-layer p { margin: 0 0 14px; color: var(--muted); max-width: 440px; }
.skeleton { width: min(420px, 90%); display: grid; gap: 10px; }
.skeleton i { display: block; height: 14px; 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: 70%; }
@keyframes shimmer { to { background-position: -200% 0; } }
dialog { width: min(680px, calc(100% - 28px)); max-height: 88dvh; border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--surface); color: var(--text); box-shadow: 0 24px 80px rgba(0,0,0,.5); }
dialog::backdrop { background: rgba(0, 8, 18, .64); backdrop-filter: blur(4px); }
.dialog-head, .dialog-foot { padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 10px; border-bottom: 1px solid var(--line); }
.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; }
.toast { position: fixed; right: 20px; bottom: 20px; z-index: 1100; max-width: min(390px, 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: 1180px) {
:root { --sidebar: 78px; }
.brand-copy, .nav span:not(.count), .nav-label, .sidebar-foot { 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; }
.zone-layout { grid-template-columns: 190px minmax(400px, 1fr); }
.zone-settings { grid-column: 1/-1; }
.settings-actions { position: static; margin-bottom: -16px; }
}
@media (max-width: 820px) {
:root { --sidebar: 0px; }
.app { display: block; padding-bottom: 66px; }
.sidebar { display: none; }
.topbar { padding: 8px 12px; }
.site-select { min-width: 0; }
.site-select span, .top-actions .select { display: none; }
.content { padding: 14px 12px; }
.stats, .system-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.split, .zone-layout { grid-template-columns: 1fr; }
.zone-camera-list { order: 2; }
.zone-workspace { order: 1; }
.zone-settings { order: 3; }
.camera-grid { grid-template-columns: 1fr; }
.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: var(--primary); background: var(--primary-soft); }
}
@media (max-width: 560px) {
.stats, .system-grid { grid-template-columns: 1fr; }
.page-head { display: grid; }
.page-actions { width: 100%; }
.page-actions .btn { flex: 1; }
.form-grid { grid-template-columns: 1fr; }
.form-field.full { grid-column: auto; }
.editor-stage { min-height: 230px; }
.coordinate-row { grid-template-columns: 1fr 1fr; }
.coordinate-row .btn { grid-column: 1/-1; }
}
@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-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></symbol>
<symbol id="i-video" viewBox="0 0 24 24"><rect x="3" y="5" width="14" height="14" rx="2"/><path d="m17 10 4-2v8l-4-2z"/></symbol>
<symbol id="i-camera" viewBox="0 0 24 24"><path d="M5 7h3l1.4-2h5.2L16 7h3a2 2 0 0 1 2 2v9H3V9a2 2 0 0 1 2-2Z"/><circle cx="12" cy="12.5" r="3"/></symbol>
<symbol id="i-zone" viewBox="0 0 24 24"><path d="m5 5 14 2-2 12-12-3Z"/><circle cx="5" cy="5" r="1.5"/><circle cx="19" cy="7" r="1.5"/><circle cx="17" cy="19" r="1.5"/><circle cx="5" cy="16" r="1.5"/></symbol>
<symbol id="i-upload" viewBox="0 0 24 24"><path d="M12 16V4m0 0L7 9m5-5 5 5"/><path d="M4 15v5h16v-5"/></symbol>
<symbol id="i-pulse" viewBox="0 0 24 24"><path d="M3 12h4l2-6 4 12 2-6h6"/></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-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></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-chevron" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6"/></symbol>
<symbol id="i-pointer" viewBox="0 0 24 24"><path d="m5 3 14 8-6 2-3 6Z"/></symbol>
<symbol id="i-line" viewBox="0 0 24 24"><path d="m4 18 16-12"/><circle cx="4" cy="18" r="2"/><circle cx="20" cy="6" r="2"/></symbol>
<symbol id="i-undo" viewBox="0 0 24 24"><path d="M9 7 4 12l5 5"/><path d="M5 12h8a6 6 0 0 1 6 6"/></symbol>
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 7h16M9 7V4h6v3m-9 0 1 14h10l1-14"/></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-check" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M20 6v5h-5M4 18v-5h5"/><path d="M18 9a7 7 0 0 0-12-2M6 15a7 7 0 0 0 12 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>
<symbol id="i-wifi" viewBox="0 0 24 24"><path d="M4 9a12 12 0 0 1 16 0M7 13a8 8 0 0 1 10 0m-7 4a3 3 0 0 1 4 0M12 20v.01"/></symbol>
</svg>
<div class="prototype-banner">PROTOTYPE - 仅供枚举交互,非实现依据</div>
<div class="app">
<aside class="sidebar" aria-label="Sense 主导航">
<div class="brand"><div class="brand-mark">Y</div><div class="brand-copy"><strong>YoVision</strong><small>Sense · Edge Console</small></div></div>
<div class="nav-label">Workspace</div>
<nav class="nav">
<button data-view="overview"><svg class="icon"><use href="#i-grid"/></svg><span>运行总览</span></button>
<button data-view="monitor"><svg class="icon"><use href="#i-video"/></svg><span>实时监控</span><span class="count">4</span></button>
<button data-view="devices"><svg class="icon"><use href="#i-camera"/></svg><span>摄像头</span><span class="count">16</span></button>
<button data-view="zones" aria-current="page"><svg class="icon"><use href="#i-zone"/></svg><span>区域规则</span></button>
<button data-view="onboarding"><svg class="icon"><use href="#i-upload"/></svg><span>接入任务</span><span class="count">1</span></button>
<button data-view="system"><svg class="icon"><use href="#i-pulse"/></svg><span>系统状态</span></button>
</nav>
<div class="sidebar-foot"><div class="online-row"><span class="dot"></span><strong>边缘节点在线</strong></div><div class="copy">sense-edge-01<br><span class="mono">v0.1-prototype</span></div></div>
</aside>
<main class="main" id="main-content" tabindex="-1">
<header class="topbar">
<div class="site-select"><div><strong>青藤寄宿学校</strong><span> / 主校区 · 16 / 16 路</span></div><svg class="icon"><use href="#i-chevron"/></svg></div>
<div class="top-actions">
<select class="select" id="stateSelect" aria-label="原型状态演示">
<option value="normal">正常状态</option><option value="loading">加载中</option><option value="empty">空数据</option><option value="error">连接失败</option>
</select>
<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" data-page="overview">
<div class="page-head"><div><h1>运行总览</h1><p>先判断是否影响业务,再进入设备或分片定位原因。</p></div><div class="page-actions"><button class="btn"><svg class="icon"><use href="#i-refresh"/></svg>立即对账</button></div></div>
<div class="grid stats">
<article class="card stat"><div class="stat-top"><span>在线设备</span><svg class="icon"><use href="#i-camera"/></svg></div><strong>15 / 16</strong><span class="trend">93.8% · 1 路重连中</span></article>
<article class="card stat"><div class="stat-top"><span>活跃媒体流</span><svg class="icon"><use href="#i-video"/></svg></div><strong>15</strong><span class="trend">主/子码流均衡</span></article>
<article class="card stat"><div class="stat-top"><span>待收敛项</span><svg class="icon"><use href="#i-refresh"/></svg></div><strong>2</strong><span class="muted">配置 1 · 时间漂移 1</span></article>
<article class="card stat"><div class="stat-top"><span>今日设备告警</span><svg class="icon"><use href="#i-alert"/></svg></div><strong>3</strong><span class="muted">与业务预警分开</span></article>
</div>
<div class="grid split">
<article class="card"><div class="card-head"><h2>分片健康</h2><span class="badge success">全部可用</span></div><div class="card-body health-list">
<div class="health-row"><span>media-01</span><strong>8 / 32</strong><div class="meter"><i style="width:25%"></i></div></div>
<div class="health-row"><span>media-02</span><strong>8 / 32</strong><div class="meter"><i style="width:25%"></i></div></div>
<div class="health-row"><span>推理绑定</span><strong>16 / 16</strong><div class="meter"><i style="width:100%;background:var(--violet)"></i></div></div>
</div></article>
<article class="card"><div class="card-head"><h2>需要处理</h2><span class="badge warning">2 项</span></div><div class="card-body">
<p><strong>北围墙 03</strong><br><span class="muted">断线 02:14 · 正在第 4 次重连</span></p>
<p><strong>实验楼入口 02</strong><br><span class="muted">时间漂移 +3.8s · 建议重新校时</span></p>
</div></article>
</div>
</section>
<section class="view" data-page="monitor">
<div class="page-head"><div><h1>实时监控</h1><p>默认只并发预览 4 路子码流;切页会释放上一页播放器。</p></div><div class="page-actions"><span class="badge info">第 1 / 4 页</span><button class="btn">布局 2 × 2</button></div></div>
<div class="grid camera-grid" id="cameraGrid"></div>
</section>
<section class="view" data-page="devices">
<div class="page-head"><div><h1>摄像头</h1><p>期望态与实际态分开展示;连接地址和凭据始终脱敏。</p></div><div class="page-actions"><button class="btn">扫描设备</button><button class="btn btn-primary" id="openCameraDialog"><svg class="icon"><use href="#i-plus"/></svg>添加摄像头</button></div></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>异常</option><option>重连中</option></select></div><span class="muted">已用 16 / 128</span></div>
<div class="table-wrap"><table><thead><tr><th><input type="checkbox" aria-label="选择全部设备"></th><th>设备</th><th>期望态</th><th>实际态</th><th>最后在线</th><th>时间漂移</th><th>媒体分片</th><th>操作</th></tr></thead><tbody id="deviceRows"></tbody></table></div>
</article>
</section>
<section class="view active" data-page="zones">
<div class="page-head"><div><h1>区域与警戒线</h1><p>在子码流快照上定义规则空间;保存草稿不会立即影响正式策略。</p></div><div class="page-actions"><span class="badge warning" id="draftBadge">草稿未发布</span><button class="btn">版本记录</button><button class="btn btn-primary" id="publishRule">发布规则</button></div></div>
<div class="zone-layout">
<aside class="card zone-camera-list" aria-label="摄像头选择"><div class="card-head"><h2>点位</h2><span class="badge">16</span></div><div style="padding:10px"><div class="search"><svg class="icon"><use href="#i-search"/></svg><input class="field" aria-label="搜索点位" placeholder="搜索点位"></div></div><div class="compact-list">
<button class="selected"><span class="mini-thumb"></span><span><strong>北围墙 01</strong><small>在线 · 2 条规则</small></span></button>
<button><span class="mini-thumb"></span><span><strong>实验楼入口</strong><small>在线 · 1 条规则</small></span></button>
<button><span class="mini-thumb"></span><span><strong>宿舍东通道</strong><small>在线 · 3 条规则</small></span></button>
<button><span class="mini-thumb"></span><span><strong>配电房门口</strong><small>在线 · 1 条规则</small></span></button>
</div></aside>
<section class="card zone-workspace" aria-label="区域绘制工作区">
<div class="editor-tools" role="toolbar" aria-label="区域绘制工具">
<button class="tool-btn" data-tool="pointer" aria-pressed="false" title="选择对象"><svg class="icon"><use href="#i-pointer"/></svg><span>选择</span></button>
<button class="tool-btn active" data-tool="polygon" aria-pressed="true" title="绘制多边形"><svg class="icon"><use href="#i-zone"/></svg><span>多边形</span></button>
<button class="tool-btn" data-tool="line" aria-pressed="false" title="绘制方向警戒线"><svg class="icon"><use href="#i-line"/></svg><span>警戒线</span></button>
<span class="tool-divider"></span>
<button class="tool-btn" id="undoPoint" title="撤销最后一个点"><svg class="icon"><use href="#i-undo"/></svg><span>撤销</span></button>
<button class="tool-btn" id="clearPoints" title="清空当前草稿"><svg class="icon"><use href="#i-trash"/></svg><span>清空</span></button>
</div>
<div class="editor-stage">
<svg id="zoneCanvas" viewBox="0 0 960 540" role="img" aria-label="北围墙 01 子码流快照及区域叠加;可点击添加多边形顶点">
<defs><linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#10283b"/><stop offset="1" stop-color="#07111d"/></linearGradient><pattern id="scan" width="8" height="8" patternUnits="userSpaceOnUse"><path d="M0 8h8" stroke="#ffffff" stroke-opacity=".035"/></pattern></defs>
<rect width="960" height="540" fill="url(#sky)"/><path d="M0 330 160 250 330 315 510 235 690 310 820 225 960 282V540H0Z" fill="#13283a"/><rect y="410" width="960" height="130" fill="#101a22"/><path d="M0 414h960M0 472h960" stroke="#5c7489" stroke-opacity=".45"/><path d="M170 300v240m230-245v245m250-250v250m190-280v280" stroke="#6f879b" stroke-width="4"/><path d="M0 345h960" stroke="#7890a4" stroke-width="5"/><rect width="960" height="540" fill="url(#scan)"/>
<polygon points="105,365 285,310 535,335 505,466 135,472" fill="#38bdf8" fill-opacity=".18" stroke="#38bdf8" stroke-width="4" stroke-dasharray="10 7"/>
<g fill="#07111d" stroke="#7dd3fc" stroke-width="4"><circle cx="105" cy="365" r="7"/><circle cx="285" cy="310" r="7"/><circle cx="535" cy="335" r="7"/><circle cx="505" cy="466" r="7"/><circle cx="135" cy="472" r="7"/></g>
<line x1="650" y1="340" x2="855" y2="430" stroke="#fbbf24" stroke-width="5"/><path d="m760 382 30-2-18 24" fill="none" stroke="#fbbf24" stroke-width="5"/><g id="draftLayer"></g>
<text x="26" y="38" fill="#d6e8f5" font-size="18" font-family="sans-serif">北围墙 01 · 子码流预览</text><text x="26" y="66" fill="#88a4b8" font-size="14" font-family="sans-serif">CAM-S2-001 · 704 × 576 · 5 FPS</text>
</svg>
<div class="editor-hint" id="editorHint">多边形:点击画面添加顶点,双击闭合;也可在右侧输入坐标。</div>
</div>
<div class="editor-status"><span id="pointStatus">当前草稿:0 个点</span><span>缩放 100% · 网格吸附关闭</span></div>
</section>
<aside class="card zone-settings"><div class="card-head"><h2>规则属性</h2><button class="btn btn-quiet icon-btn" aria-label="更多规则操作"><svg class="icon"><use href="#i-more"/></svg></button></div><div class="card-body">
<div class="form-grid">
<div class="form-field full"><label for="zoneName">名称</label><input id="zoneName" class="field" value="北围墙危险区域"></div>
<div class="form-field"><label for="zoneType">空间类型</label><select id="zoneType" class="select"><option>禁入区域</option><option>方向警戒线</option><option>关注区域</option></select></div>
<div class="form-field"><label for="zoneColor">叠加颜色</label><select id="zoneColor" class="select"><option>蓝色</option><option>琥珀色</option><option>红色</option></select></div>
<div class="form-field full"><label for="zoneRule">绑定规则</label><select id="zoneRule" class="select"><option>非授权人员进入</option><option>翻越围墙</option><option>区域内聚集</option></select></div>
<div class="form-field"><label for="schedule">生效时段</label><select id="schedule" class="select"><option>全天</option><option>夜间 22:00–06:00</option><option>自定义课表</option></select></div>
<div class="form-field"><label for="duration">持续时间</label><select id="duration" class="select"><option>立即</option><option>超过 3 秒</option><option>超过 10 秒</option></select></div>
<div class="form-field full"><div class="switch-row"><span><strong>试运行</strong><br><small class="muted">命中只记录,不触发正式预警</small></span><label class="switch"><input id="dryRun" type="checkbox" checked aria-label="启用试运行"><span></span></label></div></div>
</div>
<div class="coordinate-box"><h3>键盘坐标编辑</h3><p>坐标范围 0–100%,为鼠标绘制提供等效操作。</p><div class="coordinate-row"><input class="field" id="coordX" type="number" min="0" max="100" value="50" aria-label="横坐标百分比"><input class="field" id="coordY" type="number" min="0" max="100" value="50" aria-label="纵坐标百分比"><button class="btn" id="addCoordinate">添加点</button></div><ol id="pointList" class="muted" aria-live="polite"><li>尚未添加草稿点</li></ol></div>
<div class="settings-actions"><button class="btn" id="discardDraft">放弃</button><button class="btn" id="saveDraft">保存草稿</button></div>
</div></aside>
</div>
</section>
<section class="view" data-page="onboarding">
<div class="page-head"><div><h1>接入任务</h1><p>默认 16 路也支持批量导入;128 路时不依赖逐路手工操作。</p></div><div class="page-actions"><button class="btn">下载 CSV 模板</button></div></div>
<div class="grid split">
<article class="card"><div class="card-head"><h2>导入摄像头</h2><span class="badge info">配额 16 / 128</span></div><div class="card-body"><div class="drop-zone"><svg class="icon"><use href="#i-upload"/></svg><strong>选择 CSV 文件或拖到这里</strong><p>先本地校验列、重复序列号和配额,不上传明文密码。</p><button class="btn btn-primary" id="fakeUpload">选择文件</button></div></div></article>
<article class="card"><div class="card-head"><h2>当前任务</h2><span class="badge warning">进行中</span></div><div class="card-body"><p><strong>宿舍区第二批 · 8 路</strong></p><div class="meter"><i style="width:62%"></i></div><p class="muted">已验证 5 / 8 · 2 路待补凭据 · 1 路重试中</p><button class="btn">查看逐项结果</button></div></article>
</div>
</section>
<section class="view" data-page="system">
<div class="page-head"><div><h1>系统状态</h1><p>MediaMTX、数据库和对账器只是可观察组件,不替代设备台账真相。</p></div><div class="page-actions"><button class="btn"><svg class="icon"><use href="#i-refresh"/></svg>刷新</button></div></div>
<div class="grid system-grid">
<article class="card system-tile"><h3>Sense 服务</h3><dl><dt>状态</dt><dd><span class="badge success">健康</span></dd><dt>运行时间</dt><dd>12d 06h</dd><dt>待处理队列</dt><dd>2</dd></dl></article>
<article class="card system-tile"><h3>MediaMTX</h3><dl><dt>实例</dt><dd>2 / 2</dd><dt>Readers</dt><dd>15</dd><dt>重连</dt><dd>1</dd></dl></article>
<article class="card system-tile"><h3>本地资源</h3><dl><dt>CPU</dt><dd>22%</dd><dt>内存</dt><dd>4.8 / 16 GB</dd><dt>事件缓存</dt><dd>86 MB</dd></dl></article>
</div>
</section>
</div>
</main>
</div>
<nav class="mobile-nav" aria-label="移动端主导航">
<button data-view="overview">总览</button><button data-view="monitor">监控</button><button data-view="devices">设备</button><button class="active" data-view="zones">区域</button><button data-view="system">系统</button>
</nav>
<dialog id="cameraDialog" aria-labelledby="cameraDialogTitle">
<form method="dialog">
<div class="dialog-head"><h2 id="cameraDialogTitle">添加摄像头</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="cameraName">名称</label><input class="field" id="cameraName" required value="北围墙 04"></div>
<div class="form-field"><label for="cameraProtocol">协议</label><select class="select" id="cameraProtocol"><option>ONVIF</option><option>RTSP</option></select></div>
<div class="form-field full"><label for="cameraEndpoint">设备地址</label><input class="field" id="cameraEndpoint" value="192.0.2.24:80" aria-describedby="endpointHelp"><small id="endpointHelp" class="muted">原型使用保留地址;保存后列表仅显示脱敏端点。</small></div>
<div class="form-field"><label for="cameraUser">用户名</label><input class="field" id="cameraUser" value="operator"></div>
<div class="form-field"><label for="cameraPassword">密码</label><input class="field" id="cameraPassword" type="password" value="prototype-only"></div>
<div class="form-field"><label for="cameraArea">区域</label><select class="select" id="cameraArea"><option>北围墙</option><option>实验楼</option><option>宿舍区</option></select></div>
<div class="form-field"><label for="cameraSerial">序列号</label><input class="field" id="cameraSerial" value="CAM-DEMO-004"></div>
</div><p><button type="button" class="btn" id="testConnection">测试连接</button> <span id="testResult" class="muted" aria-live="polite">保存前先验证 Profiles、StreamUri 与校时能力。</span></p></div>
<div class="dialog-foot"><button class="btn" value="cancel">取消</button><button class="btn btn-primary" id="saveCamera" 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'), 3600); }
const deviceData = [
['北围墙 01','CAM-S2-001','启用','在线','刚刚','+0.2s','media-01','success'],
['北围墙 03','CAM-S2-003','启用','重连中','2 分钟前','—','media-01','warning'],
['实验楼入口 02','CAM-S2-006','启用','在线','12 秒前','+3.8s','media-02','warning'],
['宿舍东通道','CAM-S2-011','启用','在线','4 秒前','+0.1s','media-02','success'],
['配电房门口','CAM-S2-015','停用','已停止','1 天前','—','未绑定','info']
];
$('#deviceRows').innerHTML = deviceData.map(d => `<tr><td><input type="checkbox" aria-label="选择 ${d[0]}"></td><td><div class="device-name"><span class="camera-icon"><svg class="icon"><use href="#i-camera"/></svg></span><span><strong>${d[0]}</strong><small>${d[1]}</small></span></div></td><td>${d[2]}</td><td><span class="badge ${d[7]}">${d[3]}</span></td><td>${d[4]}</td><td>${d[5]}</td><td>${d[6]}</td><td><button class="btn btn-quiet icon-btn" aria-label="${d[0]} 更多操作"><svg class="icon"><use href="#i-more"/></svg></button></td></tr>`).join('');
const cameraNames = ['北围墙 01','实验楼入口 02','宿舍东通道','配电房门口'];
$('#cameraGrid').innerHTML = cameraNames.map((name, i) => `<article class="card camera-card"><div class="camera-stage"><svg class="scene" viewBox="0 0 640 360" role="img" aria-label="${name} 模拟子码流画面"><rect width="640" height="360" fill="#07111d"/><path d="M0 ${250-i*12} 140 180 310 230 460 150 640 210V360H0Z" fill="#14283a"/><path d="M0 290h640M100 210v150m210-165v165m220-190v190" stroke="#647b8e" stroke-width="4"/><rect width="640" height="360" fill="none" stroke="#ffffff" stroke-opacity=".08"/><text x="18" y="28" fill="#d7e7f3" font-size="16">${name} · 子码流</text><circle cx="598" cy="27" r="5" fill="#34d399"/></svg></div><div class="camera-meta"><span><strong>${name}</strong><small>704 × 576 · 5 FPS</small></span><span class="badge success">在线</span></div></article>`).join('');
function switchView(name) {
$$('.view').forEach(v => v.classList.toggle('active', v.dataset.page === name));
$$('[data-view]').forEach(b => { const current = b.dataset.view === name; b.toggleAttribute('aria-current', current); b.classList.toggle('active', current); });
$('#stateSelect').value = 'normal'; renderState('normal');
$('#main-content').focus({preventScroll:true});
}
$$('[data-view]').forEach(b => b.addEventListener('click', () => switchView(b.dataset.view)));
function renderState(state) {
const layer = $('#stateLayer'); const views = $$('.view');
layer.classList.toggle('show', state !== 'normal'); views.forEach(v => v.style.display = state === 'normal' ? '' : 'none');
if (state === 'normal') return;
const configs = {
loading: `<div><div class="skeleton" aria-label="正在加载"><i></i><i></i><i></i><i></i></div><h2>正在同步边缘状态</h2><p>超过 300ms 时显示进度;已有视频流不因控制面加载而中断。</p></div>`,
empty: `<div><svg class="icon"><use href="#i-camera"/></svg><h2>这个站点还没有摄像头</h2><p>先扫描 ONVIF 设备,或下载 CSV 模板进行批量导入。</p><button class="btn btn-primary" onclick="switchView('onboarding')">开始接入</button></div>`,
error: `<div><svg class="icon" style="color:var(--danger)"><use href="#i-alert"/></svg><h2>无法读取最新状态</h2><p>边缘节点暂时不可达。当前显示可能已过期,系统不会把未知状态伪装为在线。</p><button class="btn" onclick="document.querySelector('#stateSelect').value='normal';renderState('normal')">重试</button></div>`
}; layer.innerHTML = configs[state];
}
$('#stateSelect').addEventListener('change', e => renderState(e.target.value));
$('#themeToggle').addEventListener('click', () => { const html = document.documentElement; html.dataset.theme = html.dataset.theme === 'dark' ? 'light' : 'dark'; notify(`已切换为${html.dataset.theme === 'dark' ? '深色' : '浅色'}主题`); });
const zoneCanvas = $('#zoneCanvas'), draftLayer = $('#draftLayer'), points = [];
let activeTool = 'polygon';
function drawDraft() {
const coords = points.map(p => `${p.x},${p.y}`).join(' ');
const shape = activeTool === 'line' && points.length > 1 ? `<line x1="${points[0].x}" y1="${points[0].y}" x2="${points[1].x}" y2="${points[1].y}" stroke="#fbbf24" stroke-width="5"/>` : `<polyline points="${coords}" fill="${points.length > 2 ? 'rgba(167,139,250,.18)' : 'none'}" stroke="#a78bfa" stroke-width="4" stroke-dasharray="8 6"/>`;
draftLayer.innerHTML = shape + points.map((p,i) => `<circle cx="${p.x}" cy="${p.y}" r="8" fill="#08111f" stroke="#c4b5fd" stroke-width="4"><title>点 ${i+1}</title></circle>`).join('');
$('#pointStatus').textContent = `当前草稿:${points.length} 个点`;
$('#pointList').innerHTML = points.length ? points.map((p,i) => `<li>点 ${i+1}:${(p.x/9.6).toFixed(1)}%, ${(p.y/5.4).toFixed(1)}%</li>`).join('') : '<li>尚未添加草稿点</li>';
$('#draftBadge').textContent = points.length ? '草稿有未保存修改' : '草稿未发布';
}
function addPoint(x,y) { if (activeTool === 'pointer') return; if (activeTool === 'line' && points.length >= 2) points.length = 0; points.push({x:Math.max(0,Math.min(960,x)),y:Math.max(0,Math.min(540,y))}); drawDraft(); }
zoneCanvas.addEventListener('click', e => { const r = zoneCanvas.getBoundingClientRect(); addPoint((e.clientX-r.left)/r.width*960,(e.clientY-r.top)/r.height*540); });
zoneCanvas.addEventListener('dblclick', e => { e.preventDefault(); if (activeTool === 'polygon' && points.length >= 3) notify(`多边形已闭合,共 ${points.length} 个顶点;请保存草稿。`); });
$$('.tool-btn[data-tool]').forEach(btn => btn.addEventListener('click', () => { activeTool = btn.dataset.tool; $$('.tool-btn[data-tool]').forEach(b => { const on = b === btn; b.classList.toggle('active', on); b.setAttribute('aria-pressed', on); }); points.length = 0; drawDraft(); $('#editorHint').textContent = activeTool === 'line' ? '警戒线:依次添加起点与终点;箭头方向由起点指向终点。' : activeTool === 'pointer' ? '选择:查看已有区域;切换绘制工具可新建草稿。' : '多边形:点击画面添加顶点,双击闭合;也可在右侧输入坐标。'; }));
$('#undoPoint').addEventListener('click', () => { points.pop(); drawDraft(); });
$('#clearPoints').addEventListener('click', () => { if (points.length && confirm('清空当前未保存的绘制草稿?')) { points.length=0; drawDraft(); } });
$('#addCoordinate').addEventListener('click', () => { const x=Number($('#coordX').value), y=Number($('#coordY').value); if (!Number.isFinite(x)||!Number.isFinite(y)||x<0||x>100||y<0||y>100) { notify('坐标必须在 0–100% 之间。'); return; } addPoint(x*9.6,y*5.4); });
$('#discardDraft').addEventListener('click', () => { if (!points.length || confirm('放弃未保存的区域修改?')) { points.length=0; drawDraft(); notify('已放弃本次修改,正式规则未受影响。'); } });
$('#saveDraft').addEventListener('click', () => { if ((activeTool==='polygon'&&points.length<3)||(activeTool==='line'&&points.length<2)) { notify(activeTool==='line'?'警戒线至少需要 2 个点。':'多边形至少需要 3 个点。'); return; } notify('草稿已保存,尚未正式发布。'); $('#draftBadge').textContent='草稿已保存'; });
$('#publishRule').addEventListener('click', () => { if (confirm('发布后将影响“北围墙 01”的正式规则。确认发布当前已保存草稿?')) { $('#draftBadge').className='badge success'; $('#draftBadge').textContent='规则已发布'; notify('规则版本 v7 已发布,可在版本记录中回滚。'); } });
$('#dryRun').addEventListener('change', e => notify(e.target.checked?'已启用试运行:命中不会触发正式预警。':'已关闭试运行:发布后将进入正式链路。'));
const dialog = $('#cameraDialog');
$('#openCameraDialog').addEventListener('click', () => dialog.showModal());
$('#testConnection').addEventListener('click', async e => { const b=e.currentTarget; b.disabled=true; b.textContent='测试中…'; $('#testResult').textContent='正在读取 Profiles 与设备时间'; await new Promise(r=>setTimeout(r,700)); b.disabled=false; b.textContent='重新测试'; $('#testResult').innerHTML='<span style="color:var(--success)">连接成功:2 个 Profile,时间漂移 +0.4s</span>'; });
$('#saveCamera').addEventListener('click', () => notify('摄像头已保存为“待激活”,凭据不会显示在列表中。'));
$('#fakeUpload').addEventListener('click', () => notify('演示:已选择 cameras-demo.csv,检测到 1 个重复序列号。'));
drawDraft();
</script>
</body>
</html>
+14
View File
@@ -71,3 +71,17 @@ Sense 与 Bell 尚无可供产品确认的页面结构。生产 UI 开工前,
- dispatcher `ila` 已将 Issue #6 分配给 `codex`,claim 与工作分支均从精确 `context_ref` 创建并读回一致。
- 在独立 worktree `D:\OPC\yovision-T-004` 开始工作;写入范围仅限本任务声明的三个路径。
### 2026-08-04 原型实现
- 使用 `ui-ux-pro-max` 生成并复核统一设计系统:深色专业安防控制台、高信息密度、系统字体、语义状态色、150–300ms 微交互、明确焦点和 reduced-motion;Sense 使用青蓝强调接入/画面,Bell 使用紫色强调处置/状态机。
- `docs/design/sense/index.html` 已覆盖运行总览、4 路按需监控、摄像头期望态/实际态、批量接入、系统健康,以及多边形/方向警戒线的鼠标绘制、坐标键盘替代、撤销、草稿、试运行和发布确认。
- `docs/design/bell/index.html` 已覆盖预警队列、事件事实与证据、一次点击 ack、升级倒计时和投递事实、处置结果、误报反馈、规则、升级链、报表和审计;弱网时先显示结构化事实并允许证据后台重试。
- 两个文件均为单文件内联 CSS/JS,无外部依赖、真实视频、客户数据、连接串或人脸入口;顶部均固定显示原型用途提示。
### 2026-08-04 自测结果
- 标准库 HTML 结构审计通过:两文件均有 viewport、原型横幅、reduced-motion,无重复 `id`、无未命名按钮、无不可识别表单控件和外部 HTTP 资源;Sense 统计 40 个按钮/21 个控件,Bell 统计 42 个按钮/14 个控件。
- Playwright 在 1440×900 与 375×812 视口实测:两页页面级 `scrollWidth == clientWidth`,桌面三栏和移动底部导航正常,浏览器控制台 0 error / 0 warning;桌面与移动关键状态截图已人工检查,临时 QA 图片未写入仓库。
- Sense 实测坐标点添加、草稿状态、保存反馈、设备导航、添加摄像头对话框和 Esc 退出;Bell 实测一键 ack、升级计时停止、弱网状态、误报必填校验、错误焦点和 Esc 退出。
- 尚待项目负责人分别打开两个 HTML,确认页面结构、控件集合和主要工作流;在收到确认前任务保持 `DOING`。