docs: add light Windows UI prototype
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
- 旧生产基线:`demo/main.py`、`demo/fall_detection_gui.py`、`demo/detect_fall.py`、`demo/best.pt`。
|
||||
- V1 代码:`v1/` 目录存在但尚无实现。
|
||||
- V2 代码:`v2/` 目录存在但尚无实现。
|
||||
- 非代码设计工件:docs/ui/silver-pose-ui-ux-spec.md、docs/ui/2026-07-20-html-prototype-plan.md 与 docs/ui/silver-pose-v1-prototype.html 已建立。UI 文档已将正式默认主题定为浅灰蓝底、白色卡片,红色只表示确认摔倒。当前 HTML 仍为历史深色版本,等待单独授权后再按文档重绘;其顶部双 Tab、设置草稿与状态交互仍可作为结构参考。画面、事件和时间均为模拟数据,不连接真实摄像头、模型或网络,也不改变 Phase 1 任务顺序。
|
||||
- 非代码设计工件:docs/ui/silver-pose-ui-ux-spec.md、docs/ui/2026-07-20-html-prototype-plan.md、docs/ui/silver-pose-v1-prototype.html 与 docs/ui/silver-pose-v2-prototype.html 已建立。v2 HTML 是符合正式浅色 Windows 规范的当前视觉参考:浅灰蓝底、白色卡片,红色只表示确认摔倒、其弹窗和事件证据;文件名中的 v2 只表示原型设计修订,不能理解为 Go V2 实现已开始。v1 HTML 保留为历史深色对照。两者均使用顶部双 Tab、设置草稿与状态交互,且画面、事件和时间都是模拟数据,不连接真实摄像头、模型或网络,也不改变 Phase 1 任务顺序。
|
||||
- 测试:暂无 pytest 测试;`python -m compileall -q demo` 已通过。
|
||||
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
||||
- 当前标准启动:`./init.ps1`。
|
||||
|
||||
@@ -104,6 +104,10 @@ The revised prototype has a horizontal tablist with monitoring and settings tabs
|
||||
|
||||
Use the approved light Windows palette from the UI specification: a pale blue-gray page background, white cards and toolbars, and semantic borders. Red is reserved exclusively for a confirmed fall and the associated dialog or event evidence. The existing dark HTML prototype must be recolored in a separate approved implementation task; it is not the visual source of truth after this revision.
|
||||
|
||||
## Light theme implementation
|
||||
|
||||
The approved repaint is delivered as `docs/ui/silver-pose-v2-prototype.html`. This is a standalone, offline design revision only; the `v2` filename does not begin the Go V2 implementation or alter its V1 acceptance gate. The historical dark `silver-pose-v1-prototype.html` remains unchanged for comparison. The new artifact supports `#settings`, `#confirmed`, and `#offline` hashes for deterministic local visual review of the settings, confirmed-event dialog, and reconnect states.
|
||||
|
||||
- Spec coverage: Task 1 implements all specified views, dialog behavior, responsive breakpoints, keyboard paths, state text, offline limitation, and validation conditions.
|
||||
- Placeholder scan: no unresolved placeholder markers or file paths appear in this plan.
|
||||
- Consistency: the three controller state names are consistently `normal`, `confirmed`, and `offline`; the dialog and acknowledgement element IDs are fixed across layout and behavior.
|
||||
|
||||
@@ -0,0 +1,688 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Silver Pose · 浅色 Windows 演示原型</title>
|
||||
<style>
|
||||
:root {
|
||||
--surface-base: #eaf1f8;
|
||||
--surface-layer: #ffffff;
|
||||
--surface-overlay: #f6f9fc;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #52657c;
|
||||
--text-muted: #72839a;
|
||||
--stroke: #c8d4e3;
|
||||
--stroke-subtle: #dce5ef;
|
||||
--accent: #2563eb;
|
||||
--accent-hover: #1d4ed8;
|
||||
--success: #15803d;
|
||||
--success-fill: #e9f7ee;
|
||||
--caution: #b45309;
|
||||
--caution-fill: #fff7e6;
|
||||
--critical: #c62828;
|
||||
--critical-fill: #fff0f0;
|
||||
--offline: #64748b;
|
||||
--offline-fill: #f1f5f9;
|
||||
--focus: #2563eb;
|
||||
--radius-card: 10px;
|
||||
--radius-control: 7px;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--font-ui: "Segoe UI Variable", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
|
||||
--font-number: "Cascadia Mono", Consolas, monospace;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html { background: var(--surface-base); font-family: var(--font-ui); }
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
background:
|
||||
radial-gradient(circle at 92% -10%, rgba(183, 211, 242, .72), transparent 34rem),
|
||||
linear-gradient(145deg, #f3f7fb 0%, var(--surface-base) 60%, #e5eef8 100%);
|
||||
}
|
||||
|
||||
button, input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 40px;
|
||||
border: 1px solid var(--stroke);
|
||||
border-radius: var(--radius-control);
|
||||
padding: 8px 13px;
|
||||
color: var(--text-primary);
|
||||
background: var(--surface-layer);
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
cursor: pointer;
|
||||
transition: background 130ms ease, border-color 130ms ease, box-shadow 130ms ease, transform 130ms ease;
|
||||
}
|
||||
|
||||
button:hover { border-color: #9cb1c9; background: #f8fbff; }
|
||||
button:active { transform: translateY(1px); }
|
||||
button:focus-visible, input:focus-visible, [tabindex]:focus-visible {
|
||||
outline: 3px solid rgba(37, 99, 235, .35);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
width: min(1440px, calc(100% - 40px));
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: 20px 0 28px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
gap: var(--space-4);
|
||||
padding: 8px 12px 8px 16px;
|
||||
border: 1px solid rgba(186, 202, 220, .95);
|
||||
border-radius: var(--radius-card);
|
||||
background: rgba(255, 255, 255, .92);
|
||||
box-shadow: 0 8px 24px rgba(41, 66, 99, .08);
|
||||
}
|
||||
|
||||
.brand, .source-summary, .topbar-right, .section-heading, .toolbar, .chip, .state-title, .meta-line, .event-row, .setting-label, .setting-actions, .dialog-heading, .footer-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.brand { min-width: 238px; gap: 10px; }
|
||||
.brand-mark {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3473e6, #1e4fae);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .28);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
.brand strong { display: block; font-size: 16px; letter-spacing: .01em; }
|
||||
.brand small { display: block; margin-top: 1px; color: var(--text-secondary); font-size: 12px; }
|
||||
|
||||
.source-summary { flex: 1; justify-content: center; min-width: 0; gap: 9px; color: var(--text-secondary); font-size: 13px; }
|
||||
.source-indicator { width: 9px; height: 9px; border: 2px solid var(--success); border-radius: 50%; background: #fff; }
|
||||
.source-name { overflow: hidden; color: var(--text-primary); font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.topbar-right { gap: 12px; color: var(--text-secondary); font-family: var(--font-number); font-size: 12px; }
|
||||
.demo-tag { padding: 4px 8px; border: 1px solid #b7c9df; border-radius: 99px; color: #315274; background: #f4f8fc; font-family: var(--font-ui); font-size: 11px; font-weight: 650; }
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
margin: 14px 0 0;
|
||||
padding: 4px;
|
||||
gap: 2px;
|
||||
border: 1px solid var(--stroke);
|
||||
border-radius: 9px;
|
||||
background: rgba(255, 255, 255, .76);
|
||||
}
|
||||
.tabs button { min-height: 36px; border: 0; border-radius: 6px; background: transparent; color: var(--text-secondary); }
|
||||
.tabs button[aria-selected="true"] { color: #173e7b; background: #eaf2ff; box-shadow: inset 0 -2px 0 var(--accent); }
|
||||
|
||||
main { padding-top: var(--space-4); }
|
||||
[hidden] { display: none !important; }
|
||||
.page-intro { margin: 0 0 var(--space-3); }
|
||||
.page-intro h1 { margin: 0; font-size: 21px; line-height: 1.3; }
|
||||
.page-intro p { margin: 5px 0 0; color: var(--text-secondary); font-size: 13px; line-height: 1.55; }
|
||||
.inline-note { color: #315274; font-weight: 600; }
|
||||
|
||||
.toolbar {
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: var(--space-4);
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--stroke);
|
||||
border-radius: var(--radius-card);
|
||||
background: rgba(255, 255, 255, .88);
|
||||
}
|
||||
.state-switches, .tool-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.state-switches button { min-height: 38px; color: var(--text-secondary); }
|
||||
.state-switches button[aria-pressed="true"] { border-color: currentColor; font-weight: 700; }
|
||||
.state-switches button[data-state-target="normal"][aria-pressed="true"] { color: var(--success); background: var(--success-fill); }
|
||||
.state-switches button[data-state-target="confirmed"][aria-pressed="true"] { color: var(--critical); background: var(--critical-fill); }
|
||||
.state-switches button[data-state-target="offline"][aria-pressed="true"] { color: var(--offline); background: var(--offline-fill); }
|
||||
.primary-action { border-color: var(--accent); color: #fff; background: var(--accent); }
|
||||
.primary-action:hover { border-color: var(--accent-hover); color: #fff; background: var(--accent-hover); }
|
||||
.secondary-action { color: #24486f; background: #f7fbff; }
|
||||
|
||||
.monitor-layout { display: grid; grid-template-columns: minmax(0, 8fr) minmax(292px, 4fr); gap: var(--space-4); }
|
||||
.card { overflow: hidden; border: 1px solid var(--stroke); border-radius: var(--radius-card); background: var(--surface-layer); box-shadow: 0 6px 18px rgba(50, 77, 111, .06); }
|
||||
.card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3); padding: 15px 16px 12px; border-bottom: 1px solid var(--stroke-subtle); }
|
||||
.card-header h2 { margin: 0; font-size: 15px; line-height: 1.35; }
|
||||
.card-header p { margin: 3px 0 0; color: var(--text-secondary); font-size: 12px; line-height: 1.45; }
|
||||
|
||||
.scene-wrap { position: relative; aspect-ratio: 16 / 9; min-height: 300px; overflow: hidden; background: #dfeaf5; }
|
||||
.scene-wrap::after { position: absolute; inset: 0; border: 2px solid transparent; content: ""; pointer-events: none; }
|
||||
.scene-svg { display: block; width: 100%; height: 100%; }
|
||||
.scene-topline { position: absolute; z-index: 2; top: 14px; right: 14px; left: 14px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.scene-badge { display: inline-flex; align-items: center; gap: 7px; padding: 6px 9px; border: 1px solid #9fc7ae; border-radius: 6px; color: var(--success); background: rgba(255, 255, 255, .93); font-family: var(--font-number); font-size: 12px; font-weight: 700; }
|
||||
.badge-dot { width: 8px; height: 8px; border: 2px solid currentColor; border-radius: 50%; background: #fff; }
|
||||
.scene-time { padding: 5px 7px; border: 1px solid rgba(136, 160, 188, .75); border-radius: 5px; color: #315274; background: rgba(255, 255, 255, .9); font-family: var(--font-number); font-size: 11px; }
|
||||
.track-label { position: absolute; z-index: 2; top: 47%; left: 47%; padding: 5px 7px; border: 1px solid #63a77e; border-radius: 5px; color: #0f6f33; background: rgba(255, 255, 255, .93); font-family: var(--font-number); font-size: 11px; font-weight: 700; }
|
||||
.reconnect-overlay { position: absolute; z-index: 3; inset: 0; display: grid; place-items: center; padding: 24px; color: #334155; background: rgba(241, 245, 249, .82); opacity: 0; pointer-events: none; text-align: center; transition: opacity 130ms ease; }
|
||||
.reconnect-overlay strong { display: block; font-size: 18px; }
|
||||
.reconnect-overlay span { display: block; margin-top: 6px; color: var(--offline); font-size: 13px; }
|
||||
.scene-caption { padding: 11px 16px; border-top: 1px solid var(--stroke-subtle); color: var(--text-secondary); background: #fbfdff; font-size: 12px; line-height: 1.5; }
|
||||
|
||||
.normal-person, .fall-person { transform-origin: 50% 50%; transition: opacity 160ms ease; }
|
||||
.fall-person { opacity: 0; }
|
||||
.skeleton { fill: none; stroke: #15803d; stroke-linecap: round; stroke-linejoin: round; stroke-width: 6; }
|
||||
.joint { fill: #fff; stroke: #15803d; stroke-width: 3; }
|
||||
.person-box { fill: none; stroke: #15803d; stroke-dasharray: 9 6; stroke-width: 2; }
|
||||
|
||||
.side-stack { display: grid; align-content: start; gap: var(--space-4); }
|
||||
.state-body, .evidence-body { padding: 15px 16px; }
|
||||
.state-title { gap: 10px; }
|
||||
.state-mark { display: grid; width: 32px; height: 32px; place-items: center; border: 1px solid #9fc7ae; border-radius: 8px; color: var(--success); background: var(--success-fill); font-size: 15px; font-weight: 750; }
|
||||
.state-title h2 { margin: 0; color: var(--success); font-size: 15px; }
|
||||
.state-title p { margin: 2px 0 0; color: var(--text-secondary); font-size: 12px; }
|
||||
.state-description { margin: 13px 0 0; padding: 10px 11px; border: 1px solid #cfe2d6; border-radius: 7px; color: #2d5540; background: #f5fbf7; font-size: 12px; line-height: 1.52; }
|
||||
.metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 13px; }
|
||||
.metric { padding: 9px 10px; border: 1px solid var(--stroke-subtle); border-radius: 7px; background: #fbfdff; }
|
||||
.metric span { display: block; color: var(--text-muted); font-size: 11px; }
|
||||
.metric strong { display: block; margin-top: 4px; color: #293c54; font-family: var(--font-number); font-size: 12px; }
|
||||
.metric strong.status-success { color: var(--success); }
|
||||
|
||||
.evidence-list { display: grid; gap: 1px; }
|
||||
.evidence-row { display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 9px 0; border-bottom: 1px solid var(--stroke-subtle); font-size: 12px; }
|
||||
.evidence-row:last-child { border-bottom: 0; }
|
||||
.evidence-shape { width: 13px; height: 13px; border: 2px solid var(--success); border-radius: 50%; }
|
||||
.evidence-row strong { font-weight: 650; }
|
||||
.evidence-row span:last-child { color: var(--text-secondary); text-align: right; }
|
||||
.artifact-box { margin-top: 12px; padding: 10px; border: 1px solid #cfe2d6; border-radius: 7px; background: #f5fbf7; }
|
||||
.artifact-box strong { display: block; color: var(--success); font-size: 12px; }
|
||||
.artifact-box span { display: block; margin-top: 4px; color: var(--text-secondary); font-family: var(--font-number); font-size: 11px; line-height: 1.45; word-break: break-word; }
|
||||
|
||||
.event-card { margin-top: var(--space-4); }
|
||||
.event-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.event-row { min-width: 0; gap: 10px; padding: 13px 15px; border-right: 1px solid var(--stroke-subtle); }
|
||||
.event-row:last-child { border-right: 0; }
|
||||
.event-symbol { flex: 0 0 auto; width: 11px; height: 11px; border: 2px solid var(--success); border-radius: 50%; }
|
||||
.event-symbol.caution { border-color: var(--caution); }
|
||||
.event-symbol.offline { border-color: var(--offline); }
|
||||
.event-copy { min-width: 0; }
|
||||
.event-copy strong { display: block; overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.event-copy span { display: block; margin-top: 3px; color: var(--text-secondary); font-size: 11px; line-height: 1.35; }
|
||||
|
||||
.settings-status { display: inline-flex; align-items: center; gap: 7px; padding: 7px 9px; border: 1px solid #b9cce2; border-radius: 7px; color: #315274; background: #f4f8fc; font-size: 12px; font-weight: 650; }
|
||||
.settings-status-dot { width: 8px; height: 8px; border: 2px solid currentColor; border-radius: 50%; }
|
||||
.settings-status[data-config-state="dirty"] { border-color: #ecd09a; color: var(--caution); background: var(--caution-fill); }
|
||||
.settings-status[data-config-state="saved"] { border-color: #b9cce2; color: #315274; background: #f4f8fc; }
|
||||
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
|
||||
.settings-card.wide { grid-column: 1 / -1; }
|
||||
.settings-body { padding: 16px; }
|
||||
.key-value { display: grid; grid-template-columns: minmax(108px, .75fr) minmax(0, 1.25fr); gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--stroke-subtle); font-size: 13px; }
|
||||
.key-value:last-child { border-bottom: 0; }
|
||||
.key-value span { color: var(--text-secondary); }
|
||||
.key-value strong { color: #293c54; font-weight: 650; }
|
||||
.readiness { color: var(--success) !important; }
|
||||
.setting-form { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 16px; }
|
||||
.setting-label { align-items: flex-start; flex-direction: column; gap: 6px; color: #384b63; font-size: 12px; font-weight: 650; }
|
||||
.setting-label small { color: var(--text-muted); font-weight: 400; }
|
||||
.setting-label input { width: 100%; min-height: 38px; border: 1px solid var(--stroke); border-radius: 6px; padding: 7px 9px; color: var(--text-primary); background: #fff; font-family: var(--font-number); }
|
||||
.setting-actions { justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-top: 16px; padding-top: 15px; border-top: 1px solid var(--stroke-subtle); }
|
||||
.config-versions { color: var(--text-secondary); font-size: 12px; line-height: 1.5; }
|
||||
.config-versions strong { color: #315274; font-family: var(--font-number); }
|
||||
.settings-buttons { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.settings-note { margin: 10px 0 0; color: var(--text-secondary); font-size: 12px; line-height: 1.55; }
|
||||
|
||||
.footer-note { justify-content: space-between; gap: 12px; padding: 15px 2px 0; color: var(--text-secondary); font-size: 11px; line-height: 1.5; }
|
||||
.footer-note strong { color: #315274; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||
.toast { position: fixed; z-index: 10; right: 24px; bottom: 24px; max-width: min(430px, calc(100% - 32px)); padding: 11px 13px; border: 1px solid #9bb8d8; border-radius: 8px; color: #24486f; background: #f4f8fc; box-shadow: 0 12px 30px rgba(45, 72, 104, .18); font-size: 13px; opacity: 0; pointer-events: none; transform: translateY(8px); transition: opacity 150ms ease, transform 150ms ease; }
|
||||
.toast.is-visible { opacity: 1; transform: translateY(0); }
|
||||
|
||||
dialog { width: min(440px, calc(100% - 32px)); border: 1px solid #e1aaaa; border-radius: 10px; padding: 0; color: var(--text-primary); background: #fff; box-shadow: 0 22px 68px rgba(68, 25, 25, .23); }
|
||||
dialog::backdrop { background: rgba(30, 41, 59, .28); }
|
||||
.dialog-heading { gap: 11px; padding: 18px 20px 14px; border-bottom: 1px solid #f0cdcd; background: var(--critical-fill); }
|
||||
.dialog-symbol { display: grid; width: 30px; height: 30px; place-items: center; border: 1px solid #e2a2a2; border-radius: 50%; color: var(--critical); background: #fff; font-size: 16px; font-weight: 800; }
|
||||
.dialog-heading h2 { margin: 0; color: var(--critical); font-size: 18px; }
|
||||
.dialog-heading p { margin: 3px 0 0; color: #7a3535; font-size: 12px; }
|
||||
.dialog-body { padding: 16px 20px; }
|
||||
.dialog-body p { margin: 0; color: #384b63; font-size: 13px; line-height: 1.6; }
|
||||
.dialog-details { display: grid; gap: 8px; margin-top: 13px; padding: 11px; border: 1px solid #f0d0d0; border-radius: 7px; background: #fffafa; font-size: 12px; }
|
||||
.dialog-details div { display: flex; justify-content: space-between; gap: 10px; }
|
||||
.dialog-details span { color: var(--text-secondary); }
|
||||
.dialog-details strong { color: #703232; font-family: var(--font-number); text-align: right; }
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 0 20px 18px; }
|
||||
.dialog-actions button { border-color: var(--critical); color: #fff; background: var(--critical); }
|
||||
.dialog-actions button:hover { border-color: #a91f1f; background: #a91f1f; }
|
||||
|
||||
.app-shell[data-state="confirmed"] .scene-wrap::after { border-color: var(--critical); box-shadow: inset 0 0 0 1px rgba(198, 40, 40, .12); }
|
||||
.app-shell[data-state="confirmed"] .scene-badge, .app-shell[data-state="confirmed"] .track-label { border-color: #df9b9b; color: var(--critical); background: #fff8f8; }
|
||||
.app-shell[data-state="confirmed"] .badge-dot { background: var(--critical); }
|
||||
.app-shell[data-state="confirmed"] .normal-person { opacity: 0; }
|
||||
.app-shell[data-state="confirmed"] .fall-person { opacity: 1; }
|
||||
.app-shell[data-state="confirmed"] .state-mark { border-color: #dfa1a1; color: var(--critical); background: var(--critical-fill); }
|
||||
.app-shell[data-state="confirmed"] .state-title h2 { color: var(--critical); }
|
||||
.app-shell[data-state="confirmed"] .state-description, .app-shell[data-state="confirmed"] .artifact-box { border-color: #efc5c5; color: #763a3a; background: #fff8f8; }
|
||||
.app-shell[data-state="confirmed"] .artifact-box strong, .app-shell[data-state="confirmed"] .metric strong.status-success, .app-shell[data-state="confirmed"] .evidence-shape { color: var(--critical); border-color: var(--critical); }
|
||||
.app-shell[data-state="confirmed"] .event-symbol { border-color: var(--critical); }
|
||||
.app-shell[data-state="confirmed"] .skeleton { stroke: var(--critical); }
|
||||
.app-shell[data-state="confirmed"] .joint { stroke: var(--critical); }
|
||||
.app-shell[data-state="confirmed"] .person-box { stroke: var(--critical); }
|
||||
|
||||
.app-shell[data-state="offline"] .scene-wrap::after { border-color: #9aaabd; }
|
||||
.app-shell[data-state="offline"] .source-indicator { border-color: var(--offline); }
|
||||
.app-shell[data-state="offline"] .scene-svg { filter: grayscale(.75) opacity(.64); }
|
||||
.app-shell[data-state="offline"] .reconnect-overlay { opacity: 1; }
|
||||
.app-shell[data-state="offline"] .scene-badge, .app-shell[data-state="offline"] .track-label { border-color: #b5c1cf; color: var(--offline); background: #f8fafc; }
|
||||
.app-shell[data-state="offline"] .state-mark { border-color: #bdc8d4; color: var(--offline); background: var(--offline-fill); }
|
||||
.app-shell[data-state="offline"] .state-title h2, .app-shell[data-state="offline"] .metric strong.status-success { color: var(--offline); }
|
||||
.app-shell[data-state="offline"] .state-description, .app-shell[data-state="offline"] .artifact-box { border-color: #d6dee7; color: #52657c; background: #f8fafc; }
|
||||
.app-shell[data-state="offline"] .artifact-box strong, .app-shell[data-state="offline"] .evidence-shape { color: var(--offline); border-color: var(--offline); }
|
||||
|
||||
@media (max-width: 1007px) {
|
||||
.app-shell { width: min(100%, calc(100% - 28px)); padding-top: 14px; }
|
||||
.monitor-layout { grid-template-columns: minmax(0, 1.25fr) minmax(270px, .75fr); gap: 12px; }
|
||||
.event-list { grid-template-columns: 1fr; }
|
||||
.event-row { border-right: 0; border-bottom: 1px solid var(--stroke-subtle); }
|
||||
.event-row:last-child { border-bottom: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.topbar, .toolbar, .setting-actions, .footer-note { align-items: flex-start; flex-wrap: wrap; }
|
||||
.source-summary { order: 3; flex-basis: 100%; justify-content: flex-start; }
|
||||
.topbar-right { margin-left: auto; }
|
||||
.monitor-layout { grid-template-columns: 1fr; }
|
||||
.settings-grid { grid-template-columns: 1fr; }
|
||||
.settings-card.wide { grid-column: auto; }
|
||||
.setting-form { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-shell { width: min(100%, calc(100% - 20px)); padding-top: 10px; }
|
||||
.topbar { padding: 10px 12px; }
|
||||
.brand { min-width: 0; }
|
||||
.topbar-right { display: none; }
|
||||
.tabs { width: 100%; }
|
||||
.tabs button { flex: 1; }
|
||||
.toolbar { padding: 10px; }
|
||||
.state-switches, .tool-actions { width: 100%; }
|
||||
.state-switches button { flex: 1 1 132px; }
|
||||
.tool-actions button { flex: 1; }
|
||||
.scene-wrap { min-height: 220px; }
|
||||
.scene-topline { top: 10px; right: 10px; left: 10px; }
|
||||
.card-header, .state-body, .evidence-body, .settings-body { padding-right: 12px; padding-left: 12px; }
|
||||
.setting-form { grid-template-columns: 1fr; }
|
||||
.settings-buttons { width: 100%; }
|
||||
.settings-buttons button { flex: 1; }
|
||||
.key-value { grid-template-columns: 1fr; gap: 4px; }
|
||||
.footer-note { padding-bottom: 8px; }
|
||||
.toast { right: 16px; bottom: 16px; }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.source-summary { font-size: 12px; }
|
||||
.scene-time { display: none; }
|
||||
.scene-badge { max-width: 100%; }
|
||||
.metric-grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell" id="app" data-state="normal" data-running="true">
|
||||
<header class="topbar">
|
||||
<div class="brand" aria-label="Silver Pose 跌倒事件演示">
|
||||
<span class="brand-mark" aria-hidden="true">SP</span>
|
||||
<div><strong>Silver Pose</strong><small>摔倒事件演示 · 单路本地监控</small></div>
|
||||
</div>
|
||||
<div class="source-summary" aria-label="当前演示来源状态">
|
||||
<span class="source-indicator" aria-hidden="true"></span>
|
||||
<span class="source-name">大厅东侧 · 固定俯视演示机位</span>
|
||||
<span id="source-status">来源在线 · 模拟画面</span>
|
||||
</div>
|
||||
<div class="topbar-right"><span class="demo-tag">演示数据</span><time id="clock">12:08:42</time></div>
|
||||
</header>
|
||||
|
||||
<nav class="tabs" role="tablist" aria-label="主视图">
|
||||
<button id="monitor-tab" type="button" role="tab" aria-selected="true" aria-controls="monitor-panel" data-tab-target="monitor">实时监控</button>
|
||||
<button id="settings-tab" type="button" role="tab" aria-selected="false" aria-controls="settings-panel" tabindex="-1" data-tab-target="settings">设置</button>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<section id="monitor-panel" role="tabpanel" aria-labelledby="monitor-tab">
|
||||
<div class="page-intro">
|
||||
<h1>实时监控</h1>
|
||||
<p>展示同一人员的姿态和事件状态。<span class="inline-note">演示数据,未连接真实摄像头或模型。</span></p>
|
||||
</div>
|
||||
|
||||
<div class="toolbar" aria-label="原型状态和操作">
|
||||
<div class="state-switches" aria-label="切换演示状态">
|
||||
<button type="button" data-state-target="normal" aria-pressed="true">1 正常监控</button>
|
||||
<button type="button" data-state-target="confirmed" aria-pressed="false">2 确认摔倒</button>
|
||||
<button type="button" data-state-target="offline" aria-pressed="false">3 连接异常</button>
|
||||
</div>
|
||||
<div class="tool-actions">
|
||||
<button id="monitor-toggle" type="button" class="primary-action" aria-pressed="true"><span>停止模拟监控</span></button>
|
||||
<button id="artifact-button" type="button" class="secondary-action">查看截图位置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="monitor-layout">
|
||||
<article class="card" aria-label="实时监控画面">
|
||||
<div class="card-header"><div><h2>大厅东侧 · 画面预览</h2><p id="scene-caption">全身大部分可见,姿态质量满足监控条件。</p></div><span class="demo-tag">16:9 示例画面</span></div>
|
||||
<div class="scene-wrap">
|
||||
<div class="scene-topline"><div class="scene-badge"><span class="badge-dot" aria-hidden="true"></span><span id="scene-state">NORMAL</span></div><time class="scene-time" id="scene-time">12:08:42</time></div>
|
||||
<span class="track-label" id="track-label">P-017 · 姿态可用</span>
|
||||
<svg class="scene-svg" viewBox="0 0 960 540" role="img" aria-label="大厅俯视演示画面,含人员姿态示意">
|
||||
<defs>
|
||||
<linearGradient id="floor" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#edf4fb"/><stop offset="1" stop-color="#d7e5f2"/></linearGradient>
|
||||
<linearGradient id="corridor" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f8fbff"/><stop offset="1" stop-color="#e8f1f8"/></linearGradient>
|
||||
</defs>
|
||||
<rect width="960" height="540" fill="url(#floor)"/>
|
||||
<path d="M0 82H960M0 386H960" stroke="#c5d7e8" stroke-width="3"/>
|
||||
<path d="M95 0V540M315 0V540M645 0V540M865 0V540" stroke="#d3e0ec" stroke-width="2"/>
|
||||
<path d="M0 150 250 105 710 105 960 150V350L710 400H250L0 350Z" fill="url(#corridor)" stroke="#bdcfdf" stroke-width="3"/>
|
||||
<path d="M160 105 160 400M800 105 800 400" stroke="#d1dfec" stroke-width="3"/>
|
||||
<rect x="42" y="42" width="147" height="51" rx="8" fill="#dbe9f5" stroke="#b9ccdf"/>
|
||||
<rect x="770" y="440" width="150" height="55" rx="8" fill="#dbe9f5" stroke="#b9ccdf"/>
|
||||
<text x="65" y="74" fill="#607a95" font-family="Segoe UI, Microsoft YaHei" font-size="17">入口区域</text>
|
||||
<text x="796" y="474" fill="#607a95" font-family="Segoe UI, Microsoft YaHei" font-size="17">服务台</text>
|
||||
<g class="normal-person">
|
||||
<rect class="person-box" x="443" y="138" width="118" height="262" rx="8"/>
|
||||
<path class="skeleton" d="M500 184 500 237 469 284M500 237 530 284M500 237 482 327M500 237 520 327"/>
|
||||
<circle class="joint" cx="500" cy="163" r="18"/>
|
||||
<circle class="joint" cx="500" cy="184" r="5"/><circle class="joint" cx="500" cy="237" r="5"/>
|
||||
<circle class="joint" cx="469" cy="284" r="5"/><circle class="joint" cx="530" cy="284" r="5"/>
|
||||
<circle class="joint" cx="482" cy="327" r="5"/><circle class="joint" cx="520" cy="327" r="5"/>
|
||||
</g>
|
||||
<g class="fall-person">
|
||||
<rect class="person-box" x="378" y="274" width="266" height="104" rx="8"/>
|
||||
<path class="skeleton" d="M427 323 498 323 558 302M498 323 553 354M498 323 454 350M498 323 590 331"/>
|
||||
<circle class="joint" cx="406" cy="323" r="17"/>
|
||||
<circle class="joint" cx="427" cy="323" r="5"/><circle class="joint" cx="498" cy="323" r="5"/>
|
||||
<circle class="joint" cx="558" cy="302" r="5"/><circle class="joint" cx="553" cy="354" r="5"/>
|
||||
<circle class="joint" cx="454" cy="350" r="5"/><circle class="joint" cx="590" cy="331" r="5"/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="reconnect-overlay"><div><strong>视频源正在重连</strong><span>连接异常不生成摔倒事件,也不播放报警。</span></div></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="side-stack" aria-label="状态与事件证据">
|
||||
<section class="card">
|
||||
<div class="state-body">
|
||||
<div class="state-title"><span class="state-mark" id="state-mark" aria-hidden="true">✓</span><div><h2 id="state-heading">NORMAL · 正常监控</h2><p id="state-time">稳定观察中</p></div></div>
|
||||
<p class="state-description" id="state-description">当前人员姿态稳定,没有生成摔倒候选或报警副作用。</p>
|
||||
<div class="metric-grid" aria-label="当前状态指标">
|
||||
<div class="metric"><span>人员</span><strong id="metric-people">1 · P-017</strong></div>
|
||||
<div class="metric"><span>姿态质量</span><strong id="metric-quality">良好</strong></div>
|
||||
<div class="metric"><span>事件延迟</span><strong id="metric-latency">—</strong></div>
|
||||
<div class="metric"><span>报警</span><strong class="status-success" id="metric-alert">未触发</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-header"><div><h2>事件证据</h2><p id="evidence-status">当前无待处理事件</p></div><span class="demo-tag" id="artifact-status">监控中</span></div>
|
||||
<div class="evidence-body">
|
||||
<div class="evidence-list" id="evidence-panel"></div>
|
||||
<div class="artifact-box"><strong id="artifact-title">本地声音:未触发</strong><span id="artifact-path">当前没有已保存截图。</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<section class="card event-card" aria-label="最近事件">
|
||||
<div class="card-header"><div><h2>最近事件</h2><p>原型仅展示摘要;正式版由本地 JSONL 事件记录提供。</p></div><span class="demo-tag" id="event-count">无新增确认事件</span></div>
|
||||
<div class="event-list" id="event-list"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="settings-panel" role="tabpanel" aria-labelledby="settings-tab" hidden data-config-state="saved">
|
||||
<div class="page-intro">
|
||||
<h1>设置</h1>
|
||||
<p>编辑的是非敏感草稿配置。保存后仅在下一次开始监控时生效,当前运行配置保持不变。</p>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<div class="card-header"><div><h2>配置应用状态</h2><p>用于区分正在监控的快照和下一次启动将采用的草稿。</p></div><span class="settings-status" id="config-state"><span class="settings-status-dot" aria-hidden="true"></span><span id="config-state-text">草稿已保存,下一次启动生效</span></span></div>
|
||||
<div class="settings-body"><div class="key-value"><span>当前运行配置</span><strong>cfg-20260720-01 · 监控开始时冻结</strong></div><div class="key-value"><span>草稿版本</span><strong id="draft-version">cfg-20260720-02 · 已保存待应用</strong></div></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
<article class="card settings-card"><div class="card-header"><div><h2>视频来源</h2><p>真实来源只由本地环境变量或未跟踪配置提供。</p></div></div><div class="settings-body"><div class="key-value"><span>来源变量</span><strong>SILVER_POSE_RTSP_URL</strong></div><div class="key-value"><span>就绪状态</span><strong class="readiness">已检测 · 不显示具体地址</strong></div><p class="settings-note">此原型不读取、保存或回显来源地址及其凭证。</p></div></article>
|
||||
<article class="card settings-card"><div class="card-header"><div><h2>姿态模型</h2><p>只读模型来源信息;原型不会执行推理。</p></div></div><div class="settings-body"><div class="key-value"><span>能力</span><strong>person Pose · 17 关键点</strong></div><div class="key-value"><span>锁定状态</span><strong>演示基线已记录</strong></div><p class="settings-note">姿态输出不等同于摔倒概率;事件判断由时序状态机负责。</p></div></article>
|
||||
<article class="card settings-card wide"><div class="card-header"><div><h2>事件与重连草稿</h2><p>这些值为原型演示字段。修改后会标为“编辑中”,不会热修改实时监控。</p></div></div><div class="settings-body"><form id="settings-form"><div class="setting-form"><label class="setting-label">确认持续时间(秒)<small>目标范围 1–3 秒</small><input id="confirm-seconds" type="number" min="1" max="3" step="0.1" value="1.8"></label><label class="setting-label">恢复冷却(秒)<small>确认后再允许新事件</small><input id="cooldown-seconds" type="number" min="1" max="120" step="1" value="18"></label><label class="setting-label">关键点质量阈值<small>低质量姿态不产生候选</small><input id="quality-threshold" type="number" min="0" max="1" step="0.01" value="0.65"></label><label class="setting-label">重连初始等待(秒)<small>断流期间不报警</small><input id="reconnect-seconds" type="number" min="1" max="60" step="1" value="4"></label><label class="setting-label">最大重连等待(秒)<small>用于指数退避上限</small><input id="reconnect-max-seconds" type="number" min="1" max="300" step="1" value="30"></label><label class="setting-label">事件冷却复核(秒)<small>避免重复副作用</small><input id="event-debounce-seconds" type="number" min="1" max="120" step="1" value="12"></label></div><div class="setting-actions"><div class="config-versions">运行版本:<strong>cfg-20260720-01</strong><br>草稿仅在下一次开始监控时成为新的运行配置。</div><div class="settings-buttons"><button id="reset-settings" type="button">恢复演示默认值</button><button type="submit" class="primary-action">保存草稿</button></div></div></form></div></article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer-note"><span><strong>适用边界:</strong>固定俯视大厅或走廊、单路、全身大部分可见的演示条件。</span><span>HTML 交互原型 · 不替代事件级验收</span></footer>
|
||||
</div>
|
||||
|
||||
<div id="live-status" aria-live="polite" class="sr-only"></div>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<dialog id="fall-dialog" aria-labelledby="dialog-title" aria-describedby="dialog-description">
|
||||
<div class="dialog-heading"><span class="dialog-symbol" aria-hidden="true">!</span><div><h2 id="dialog-title">确认摔倒</h2><p>已生成一次本地报警副作用</p></div></div>
|
||||
<div class="dialog-body"><p id="dialog-description">人员 P-017 的倒地证据已持续满足确认条件。请按现场流程确认情况。</p><div class="dialog-details"><div><span>确认时间</span><strong id="dialog-time">12:08:42</strong></div><div><span>确认延迟</span><strong>1.8 秒</strong></div><div><span>事件编号</span><strong>FALL-00017</strong></div><div><span>截图状态</span><strong>已保存(演示)</strong></div></div></div>
|
||||
<div class="dialog-actions"><button id="acknowledge-button" type="button">我已知晓</button></div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
var app = document.getElementById("app");
|
||||
var dialog = document.getElementById("fall-dialog");
|
||||
var acknowledgeButton = document.getElementById("acknowledge-button");
|
||||
var stateButtons = Array.prototype.slice.call(document.querySelectorAll("[data-state-target]"));
|
||||
var topTabs = Array.prototype.slice.call(document.querySelectorAll("[data-tab-target]"));
|
||||
var monitorPanel = document.getElementById("monitor-panel");
|
||||
var settingsPanel = document.getElementById("settings-panel");
|
||||
var liveStatus = document.getElementById("live-status");
|
||||
var toast = document.getElementById("toast");
|
||||
var monitorToggle = document.getElementById("monitor-toggle");
|
||||
var settingsForm = document.getElementById("settings-form");
|
||||
var resetSettingsButton = document.getElementById("reset-settings");
|
||||
var settingInputs = Array.prototype.slice.call(settingsForm.querySelectorAll("input"));
|
||||
var configState = document.getElementById("config-state");
|
||||
var configStateText = document.getElementById("config-state-text");
|
||||
var draftVersion = document.getElementById("draft-version");
|
||||
var activeTab = "monitor";
|
||||
var lastInvoker = null;
|
||||
var confirmedDialogShown = false;
|
||||
var toastTimer = null;
|
||||
var draftRevision = 2;
|
||||
var defaults = {};
|
||||
|
||||
settingInputs.forEach(function (input) { defaults[input.id] = input.value; });
|
||||
|
||||
var views = {
|
||||
normal: {
|
||||
sceneState: "NORMAL",
|
||||
source: "来源在线 · 模拟画面",
|
||||
track: "P-017 · 姿态可用",
|
||||
title: "NORMAL · 正常监控",
|
||||
mark: "✓",
|
||||
time: "稳定观察中",
|
||||
description: "当前人员姿态稳定,没有生成摔倒候选或报警副作用。",
|
||||
caption: "全身大部分可见,姿态质量满足监控条件。",
|
||||
people: "1 · P-017", quality: "良好", latency: "—", alert: "未触发",
|
||||
evidenceStatus: "当前无待处理事件", artifactStatus: "监控中", artifactTitle: "本地声音:未触发", artifactPath: "当前没有已保存截图。",
|
||||
evidence: [["视频来源", "在线"], ["姿态质量", "通过"], ["状态机", "NORMAL"]],
|
||||
events: [["持续监控", "P-017 姿态稳定,未生成候选", "normal"], ["质量门控通过", "全身关键点可用于后续判断", "normal"], ["报警保持静默", "无确认事件,不播放声音", "normal"]],
|
||||
announcement: "已切换到正常监控。当前没有生成摔倒事件或报警。"
|
||||
},
|
||||
confirmed: {
|
||||
sceneState: "CONFIRMED · FALL-00017",
|
||||
source: "来源在线 · 事件已确认",
|
||||
track: "P-017 · 确认倒地",
|
||||
title: "CONFIRMED · 已确认摔倒",
|
||||
mark: "!",
|
||||
time: "12:08:42 · 已确认",
|
||||
description: "倒地证据持续 1.8 秒,已创建单个事件并完成声音、弹窗与截图演示。",
|
||||
caption: "红色仅表示确认摔倒事件及其证据;关闭弹窗后,事件证据保持可追溯。",
|
||||
people: "1 · P-017", quality: "良好", latency: "1.8 秒", alert: "已触发一次",
|
||||
evidenceStatus: "确认事件 · 证据已冻结", artifactStatus: "FALL-00017", artifactTitle: "声音与截图:已触发(演示)", artifactPath: "events/20260720/FALL-00017.png · 原型不创建文件",
|
||||
evidence: [["事件编号", "FALL-00017"], ["确认延迟", "1.8 秒"], ["报警副作用", "一次"], ["配置版本", "cfg-20260720-01"]],
|
||||
events: [["确认摔倒", "12:08:42 · P-017 · FALL-00017", "confirmed"], ["截图已保存", "仅展示演示路径,不写入文件", "confirmed"], ["声音已播放", "同一事件不重复播放", "confirmed"]],
|
||||
announcement: "已确认摔倒事件 FALL-00017。声音、弹窗和截图证据均只触发一次。"
|
||||
},
|
||||
offline: {
|
||||
sceneState: "SOURCE RETRYING",
|
||||
source: "视频源异常 · 正在重连",
|
||||
track: "上一帧姿态已冻结",
|
||||
title: "SOURCE ERROR · 连接异常",
|
||||
mark: "—",
|
||||
time: "重试第 2 次",
|
||||
description: "视频源暂不可用。系统停止推进人员和倒地证据;断流本身不代表摔倒。",
|
||||
caption: "正在重连期间保留最后一帧演示画面,不生成新的人员或摔倒结论。",
|
||||
people: "—", quality: "不可用", latency: "—", alert: "未触发",
|
||||
evidenceStatus: "事件处理已暂停", artifactStatus: "重连中", artifactTitle: "本地声音:未触发", artifactPath: "等待视频源恢复后继续监控。",
|
||||
evidence: [["视频源状态", "重连中"], ["人员更新", "已暂停"], ["摔倒事件", "不触发"]],
|
||||
events: [["视频源不可用", "12:08:42 · 已保留最后一帧", "offline"], ["第 2 次重连等待中", "4 秒后再次尝试", "offline"], ["报警保持静默", "连接异常不生成摔倒事件", "offline"]],
|
||||
announcement: "视频源连接异常,正在重连。没有生成摔倒事件或报警。"
|
||||
}
|
||||
};
|
||||
|
||||
function setText(id, value) { document.querySelector(id).textContent = value; }
|
||||
function eventMarkup(rows) {
|
||||
return rows.map(function (row) {
|
||||
return "<div class='event-row'><span class='event-symbol " + row[2] + "' aria-hidden='true'></span><div class='event-copy'><strong>" + row[0] + "</strong><span>" + row[1] + "</span></div></div>";
|
||||
}).join("");
|
||||
}
|
||||
function evidenceMarkup(rows) {
|
||||
return rows.map(function (row) {
|
||||
return "<div class='evidence-row'><span class='evidence-shape' aria-hidden='true'></span><strong>" + row[0] + "</strong><span>" + row[1] + "</span></div>";
|
||||
}).join("");
|
||||
}
|
||||
function showToast(message) {
|
||||
toast.textContent = message;
|
||||
toast.classList.add("is-visible");
|
||||
window.clearTimeout(toastTimer);
|
||||
toastTimer = window.setTimeout(function () { toast.classList.remove("is-visible"); }, 3600);
|
||||
}
|
||||
function setActiveTab(nextTab, moveFocus) {
|
||||
if (nextTab !== "monitor" && nextTab !== "settings") return;
|
||||
activeTab = nextTab;
|
||||
topTabs.forEach(function (tab) {
|
||||
var selected = tab.dataset.tabTarget === nextTab;
|
||||
tab.setAttribute("aria-selected", String(selected));
|
||||
tab.setAttribute("tabindex", selected ? "0" : "-1");
|
||||
if (selected && moveFocus) tab.focus();
|
||||
});
|
||||
monitorPanel.hidden = nextTab !== "monitor";
|
||||
settingsPanel.hidden = nextTab !== "settings";
|
||||
liveStatus.textContent = nextTab === "settings" ? "已切换至设置。草稿保存后在下一次开始监控时生效。" : "已切换至实时监控。当前运行配置保持不变。";
|
||||
}
|
||||
function setDemoState(nextState, invoker) {
|
||||
var view = views[nextState];
|
||||
if (!view) return;
|
||||
if (invoker) lastInvoker = invoker;
|
||||
app.dataset.state = nextState;
|
||||
stateButtons.forEach(function (button) { button.setAttribute("aria-pressed", String(button.dataset.stateTarget === nextState)); });
|
||||
setText("#scene-state", view.sceneState); setText("#source-status", view.source); setText("#track-label", view.track);
|
||||
setText("#state-heading", view.title); setText("#state-mark", view.mark); setText("#state-time", view.time);
|
||||
setText("#state-description", view.description); setText("#scene-caption", view.caption);
|
||||
setText("#metric-people", view.people); setText("#metric-quality", view.quality); setText("#metric-latency", view.latency); setText("#metric-alert", view.alert);
|
||||
setText("#evidence-status", view.evidenceStatus); setText("#artifact-status", view.artifactStatus); setText("#artifact-title", view.artifactTitle); setText("#artifact-path", view.artifactPath);
|
||||
setText("#event-count", nextState === "confirmed" ? "1 个确认事件" : view.artifactStatus);
|
||||
document.getElementById("evidence-panel").innerHTML = evidenceMarkup(view.evidence);
|
||||
document.getElementById("event-list").innerHTML = eventMarkup(view.events);
|
||||
liveStatus.textContent = view.announcement;
|
||||
if (nextState === "confirmed" && !confirmedDialogShown) {
|
||||
confirmedDialogShown = true;
|
||||
setText("#dialog-time", document.getElementById("scene-time").textContent);
|
||||
if (typeof dialog.showModal === "function" && !dialog.open) {
|
||||
dialog.showModal();
|
||||
window.setTimeout(function () { acknowledgeButton.focus(); }, 0);
|
||||
}
|
||||
}
|
||||
if (nextState !== "confirmed") {
|
||||
confirmedDialogShown = false;
|
||||
if (dialog.open) dialog.close();
|
||||
}
|
||||
}
|
||||
function markConfigDirty() {
|
||||
settingsPanel.dataset.configState = "dirty";
|
||||
configState.dataset.configState = "dirty";
|
||||
configStateText.textContent = "设置已修改,尚未保存";
|
||||
draftVersion.textContent = "cfg-20260720-0" + (draftRevision + 1) + " · 编辑中";
|
||||
}
|
||||
function resetSettings() {
|
||||
settingInputs.forEach(function (input) { input.value = defaults[input.id]; });
|
||||
markConfigDirty();
|
||||
showToast("已恢复演示默认值;请保存后在下一次启动监控时应用。");
|
||||
}
|
||||
function saveSettings() {
|
||||
draftRevision += 1;
|
||||
settingsPanel.dataset.configState = "saved";
|
||||
configState.dataset.configState = "saved";
|
||||
configStateText.textContent = "草稿已保存,下一次启动生效";
|
||||
draftVersion.textContent = "cfg-20260720-0" + draftRevision + " · 已保存待应用";
|
||||
liveStatus.textContent = "设置草稿已保存。当前监控仍使用 cfg-20260720-01,下一次开始监控时才使用新版本。";
|
||||
showToast("演示草稿已保存;运行配置未被热修改。");
|
||||
}
|
||||
function updateClock() {
|
||||
var time = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
||||
document.getElementById("clock").textContent = time;
|
||||
document.getElementById("scene-time").textContent = time;
|
||||
}
|
||||
|
||||
topTabs.forEach(function (tab) { tab.addEventListener("click", function () { setActiveTab(tab.dataset.tabTarget, false); }); });
|
||||
stateButtons.forEach(function (button) { button.addEventListener("click", function () { setDemoState(button.dataset.stateTarget, button); }); });
|
||||
settingInputs.forEach(function (input) { input.addEventListener("input", markConfigDirty); });
|
||||
resetSettingsButton.addEventListener("click", resetSettings);
|
||||
settingsForm.addEventListener("submit", function (event) { event.preventDefault(); saveSettings(); });
|
||||
acknowledgeButton.addEventListener("click", function () { dialog.close(); });
|
||||
dialog.addEventListener("close", function () { if (lastInvoker && typeof lastInvoker.focus === "function") lastInvoker.focus(); });
|
||||
monitorToggle.addEventListener("click", function () {
|
||||
var running = app.dataset.running === "true";
|
||||
app.dataset.running = String(!running);
|
||||
monitorToggle.setAttribute("aria-pressed", String(!running));
|
||||
monitorToggle.querySelector("span").textContent = running ? "开始模拟监控" : "停止模拟监控";
|
||||
showToast(running ? "原型已暂停更新:未停止任何真实视频源。" : "原型已恢复更新:仍未连接真实视频源。");
|
||||
});
|
||||
document.getElementById("artifact-button").addEventListener("click", function () {
|
||||
showToast(app.dataset.state === "confirmed" ? "演示截图位置:events/20260720/FALL-00017.png(原型不创建文件)。" : "当前没有已保存截图;切换到“确认摔倒”可查看报警工件状态。");
|
||||
});
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
||||
var tabIndex = topTabs.indexOf(document.activeElement);
|
||||
if (tabIndex !== -1 && (event.key === "ArrowLeft" || event.key === "ArrowRight")) {
|
||||
event.preventDefault();
|
||||
var direction = event.key === "ArrowRight" ? 1 : -1;
|
||||
var nextIndex = (tabIndex + direction + topTabs.length) % topTabs.length;
|
||||
setActiveTab(topTabs[nextIndex].dataset.tabTarget, true);
|
||||
return;
|
||||
}
|
||||
if (document.activeElement && document.activeElement.matches("input")) return;
|
||||
var stateMap = { "1": "normal", "2": "confirmed", "3": "offline" };
|
||||
if (activeTab === "monitor" && stateMap[event.key]) setDemoState(stateMap[event.key], document.querySelector("[data-state-target='" + stateMap[event.key] + "']"));
|
||||
if (event.key === "Escape" && dialog.open) dialog.close();
|
||||
});
|
||||
|
||||
if (window.location.hash === "#settings") setActiveTab("settings", false);
|
||||
var initialState = window.location.hash === "#confirmed" ? "confirmed" : window.location.hash === "#offline" ? "offline" : "normal";
|
||||
setDemoState(initialState);
|
||||
updateClock();
|
||||
window.setInterval(updateClock, 1000);
|
||||
}());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user