feat: add current MVP interactive prototypes (#28)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# YoVision 当前 MVP 交互原型
|
||||
|
||||
本目录是工单 #28 生成的版本绑定设计资产,用于确认 MVP #8 的信息架构、页面状态和交互流程,不代表产品功能已经实现。
|
||||
|
||||
## 打开方式
|
||||
|
||||
可直接双击 `index.html`,或在仓库根目录启动本地静态服务:
|
||||
|
||||
```powershell
|
||||
python -m http.server 4173 --directory prototypes
|
||||
```
|
||||
|
||||
然后访问 `http://127.0.0.1:4173/`。
|
||||
|
||||
## 原型范围
|
||||
|
||||
- `sense/index.html`:设备接入、ONVIF/RTSP、MediaMTX、实时监看和区域配置。
|
||||
- `brain/index.html`:合成输入到项目内匿名事件的内部流水线;Brain 无客户管理 UI。
|
||||
- `bell/index.html`:合成事件、不可变 Event、规则/Alert、ack/close 和审计时间线。
|
||||
- `assets/`:三套原型复用的样式和交互脚本。
|
||||
- `design-system/yovision/MASTER.md`:由 UI/UX Pro Max 生成的设计系统基线。
|
||||
|
||||
## 交互提示
|
||||
|
||||
- 左侧导航可切换页面;移动端使用顶部菜单按钮。
|
||||
- Sense 可筛选设备、添加待激活设备并查看区域配置。
|
||||
- Brain 可模拟处理下一帧并生成项目内事件。
|
||||
- Bell 可注入合成事件,并演示 Alert 的 ack 和 close。
|
||||
- 所有弹窗支持 `Esc` 关闭,交互元素支持键盘焦点。
|
||||
|
||||
## 事实来源边界
|
||||
|
||||
- 产品范围以 Gitea Wiki 的 Product Requirements、Product Roadmap 和对应工单为准。
|
||||
- 本原型不定义 API、数据库或跨项目契约。
|
||||
- 旧仓库原型仅用于只读参考;本目录没有复制旧原型代码。
|
||||
- 页面数据均为虚构演示数据,不得放入真实账号、摄像头凭据、客户数据或生产地址。
|
||||
Vendored
+199
@@ -0,0 +1,199 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const qs = (selector, root = document) => root.querySelector(selector);
|
||||
const qsa = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const toastRegion = qs("[data-toast-region]");
|
||||
let lastFocused = null;
|
||||
|
||||
function toast(message) {
|
||||
if (!toastRegion) return;
|
||||
const item = document.createElement("div");
|
||||
item.className = "toast";
|
||||
item.setAttribute("role", "status");
|
||||
item.textContent = message;
|
||||
toastRegion.appendChild(item);
|
||||
window.setTimeout(() => item.remove(), 3600);
|
||||
}
|
||||
|
||||
function setView(name) {
|
||||
qsa("[data-view]").forEach((view) => {
|
||||
const active = view.dataset.view === name;
|
||||
view.classList.toggle("active", active);
|
||||
view.hidden = !active;
|
||||
});
|
||||
qsa("[data-view-target]").forEach((button) => {
|
||||
const active = button.dataset.viewTarget === name;
|
||||
button.classList.toggle("active", active);
|
||||
if (active) button.setAttribute("aria-current", "page");
|
||||
else button.removeAttribute("aria-current");
|
||||
});
|
||||
const selected = qs(`[data-view-target="${name}"]`);
|
||||
const title = selected?.dataset.title || selected?.textContent.trim();
|
||||
const pageName = qs("[data-page-name]");
|
||||
if (pageName && title) pageName.textContent = title;
|
||||
qs("#main-content")?.focus({ preventScroll: true });
|
||||
qs("[data-sidebar]")?.classList.remove("open");
|
||||
qs("[data-mobile-menu]")?.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
qsa("[data-view-target]").forEach((button) => {
|
||||
button.addEventListener("click", () => setView(button.dataset.viewTarget));
|
||||
});
|
||||
|
||||
qs("[data-mobile-menu]")?.addEventListener("click", () => {
|
||||
const sidebar = qs("[data-sidebar]");
|
||||
const open = sidebar.classList.toggle("open");
|
||||
qs("[data-mobile-menu]").setAttribute("aria-expanded", String(open));
|
||||
});
|
||||
|
||||
function openModal(id, trigger) {
|
||||
const backdrop = qs(`#${id}`);
|
||||
if (!backdrop) return;
|
||||
lastFocused = trigger || document.activeElement;
|
||||
backdrop.classList.add("open");
|
||||
backdrop.setAttribute("aria-hidden", "false");
|
||||
document.body.style.overflow = "hidden";
|
||||
qs("input, select, textarea, button", qs(".modal", backdrop))?.focus();
|
||||
}
|
||||
|
||||
function closeModal(backdrop) {
|
||||
if (!backdrop) return;
|
||||
backdrop.classList.remove("open");
|
||||
backdrop.setAttribute("aria-hidden", "true");
|
||||
document.body.style.overflow = "";
|
||||
lastFocused?.focus();
|
||||
}
|
||||
|
||||
qsa("[data-open-modal]").forEach((trigger) => {
|
||||
trigger.addEventListener("click", () => openModal(trigger.dataset.openModal, trigger));
|
||||
});
|
||||
qsa("[data-close-modal]").forEach((trigger) => {
|
||||
trigger.addEventListener("click", () => closeModal(trigger.closest(".modal-backdrop")));
|
||||
});
|
||||
qsa(".modal-backdrop").forEach((backdrop) => {
|
||||
backdrop.addEventListener("mousedown", (event) => {
|
||||
if (event.target === backdrop) closeModal(backdrop);
|
||||
});
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") closeModal(qs(".modal-backdrop.open"));
|
||||
});
|
||||
|
||||
qsa("[role='tab'][data-tab-target]").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const group = tab.closest("[data-tabs]");
|
||||
qsa("[role='tab']", group).forEach((item) => {
|
||||
item.setAttribute("aria-selected", String(item === tab));
|
||||
item.tabIndex = item === tab ? 0 : -1;
|
||||
});
|
||||
qsa(".tab-panel", group.parentElement).forEach((panel) => {
|
||||
const active = panel.id === tab.dataset.tabTarget;
|
||||
panel.classList.toggle("active", active);
|
||||
panel.hidden = !active;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
qsa("[data-filter-input]").forEach((input) => {
|
||||
input.addEventListener("input", () => {
|
||||
const table = qs(input.dataset.filterInput);
|
||||
const query = input.value.trim().toLocaleLowerCase("zh-CN");
|
||||
qsa("tbody tr", table).forEach((row) => {
|
||||
row.hidden = query && !row.textContent.toLocaleLowerCase("zh-CN").includes(query);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
qsa("[data-toast]").forEach((button) => {
|
||||
button.addEventListener("click", () => toast(button.dataset.toast));
|
||||
});
|
||||
|
||||
const addDevice = qs("[data-action='add-device']");
|
||||
addDevice?.addEventListener("click", () => {
|
||||
const name = qs("#device-name")?.value.trim() || "新摄像头";
|
||||
const address = qs("#device-address")?.value.trim() || "192.168.10.88";
|
||||
const tbody = qs("#device-table tbody");
|
||||
if (tbody) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `<td><span class="table-title"></span><span class="table-subtitle">待探测 · video</span></td><td></td><td><span class="status status-warning">待激活</span></td><td>尚未选择</td><td><button class="btn btn-quiet" data-toast="已打开设备探测流程">查看</button></td>`;
|
||||
qs(".table-title", row).textContent = name;
|
||||
row.children[1].textContent = address;
|
||||
qs("button", row).addEventListener("click", () => toast("已打开设备探测流程"));
|
||||
tbody.prepend(row);
|
||||
}
|
||||
closeModal(addDevice.closest(".modal-backdrop"));
|
||||
toast(`${name} 已保存为待激活设备`);
|
||||
});
|
||||
|
||||
let frameCount = 12842;
|
||||
qs("[data-action='run-frame']")?.addEventListener("click", (event) => {
|
||||
event.currentTarget.disabled = true;
|
||||
event.currentTarget.textContent = "处理中…";
|
||||
window.setTimeout(() => {
|
||||
frameCount += 1;
|
||||
const counter = qs("[data-frame-count]");
|
||||
if (counter) counter.textContent = frameCount.toLocaleString("zh-CN");
|
||||
const events = qs("[data-brain-events]");
|
||||
if (events) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "activity-item";
|
||||
item.innerHTML = `<div class="activity-icon"><svg aria-hidden="true"><use href="#icon-activity"></use></svg></div><div><div class="activity-title">方向越线候选</div><div class="activity-meta">synthetic-camera-01 · 置信度 0.91 · 项目内事件</div></div><div class="activity-time">刚刚</div>`;
|
||||
events.prepend(item);
|
||||
}
|
||||
event.currentTarget.disabled = false;
|
||||
event.currentTarget.textContent = "处理下一帧";
|
||||
toast("已完成一帧推理并生成项目内候选事件");
|
||||
}, 520);
|
||||
});
|
||||
|
||||
qs("[data-action='inject-event']")?.addEventListener("click", (event) => {
|
||||
event.currentTarget.disabled = true;
|
||||
event.currentTarget.textContent = "注入中…";
|
||||
window.setTimeout(() => {
|
||||
const count = qs("[data-alert-count]");
|
||||
if (count) count.textContent = String(Number(count.textContent) + 1);
|
||||
event.currentTarget.disabled = false;
|
||||
event.currentTarget.textContent = "注入合成事件";
|
||||
closeModal(event.currentTarget.closest(".modal-backdrop"));
|
||||
toast("合成事件已校验并生成新的 Alert");
|
||||
}, 520);
|
||||
});
|
||||
|
||||
const alertState = qs("[data-alert-state]");
|
||||
const ackButton = qs("[data-action='ack-alert']");
|
||||
const closeButton = qs("[data-action='close-alert']");
|
||||
ackButton?.addEventListener("click", () => {
|
||||
alertState.textContent = "已确认";
|
||||
alertState.className = "status status-warning";
|
||||
ackButton.disabled = true;
|
||||
closeButton.disabled = false;
|
||||
const timeline = qs("[data-alert-timeline]");
|
||||
timeline?.insertAdjacentHTML("beforeend", `<div class="timeline-item"><div class="timeline-title">王值班员确认预警</div><div class="timeline-meta">刚刚 · 首个成功 ack · 已写入审计</div></div>`);
|
||||
toast("预警已由王值班员确认");
|
||||
});
|
||||
closeButton?.addEventListener("click", () => {
|
||||
alertState.textContent = "已关闭";
|
||||
alertState.className = "status status-success";
|
||||
closeButton.disabled = true;
|
||||
qs("[data-alert-timeline]")?.insertAdjacentHTML("beforeend", `<div class="timeline-item"><div class="timeline-title">处置完成并关闭</div><div class="timeline-meta">刚刚 · 原始 Event 保持不变</div></div>`);
|
||||
toast("Alert 已关闭,原始 Event 未修改");
|
||||
});
|
||||
|
||||
qsa("[data-select-alert]").forEach((row) => {
|
||||
const select = () => {
|
||||
qsa("[data-select-alert]").forEach((item) => item.classList.toggle("selected", item === row));
|
||||
toast(`已选择:${row.dataset.selectAlert}`);
|
||||
};
|
||||
row.addEventListener("click", select);
|
||||
row.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
select();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const initial = qs("[data-view-target].active")?.dataset.viewTarget || qs("[data-view]")?.dataset.view;
|
||||
if (initial) setView(initial);
|
||||
})();
|
||||
@@ -0,0 +1,244 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f5f7fb;
|
||||
--surface: #ffffff;
|
||||
--surface-muted: #f8fafc;
|
||||
--text: #172033;
|
||||
--text-muted: #5d687c;
|
||||
--border: #dce2ec;
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--primary-soft: #e8f0ff;
|
||||
--success: #16794a;
|
||||
--success-soft: #e9f7ef;
|
||||
--warning: #a15c00;
|
||||
--warning-soft: #fff5df;
|
||||
--danger: #c62828;
|
||||
--danger-soft: #fff0f0;
|
||||
--neutral-soft: #eef2f7;
|
||||
--sidebar: #111b2e;
|
||||
--sidebar-muted: #91a0b8;
|
||||
--focus: #0b6bcb;
|
||||
--radius-sm: 6px;
|
||||
--radius: 10px;
|
||||
--radius-lg: 16px;
|
||||
--shadow-sm: 0 1px 2px rgba(18, 32, 56, 0.06);
|
||||
--shadow-md: 0 8px 24px rgba(18, 32, 56, 0.09);
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--sidebar-width: 240px;
|
||||
font-family: Inter, "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { min-width: 320px; background: var(--bg); }
|
||||
body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.5; }
|
||||
button, input, select, textarea { font: inherit; }
|
||||
button, a, [role="button"] { touch-action: manipulation; }
|
||||
button { cursor: pointer; }
|
||||
a { color: inherit; }
|
||||
svg { width: 20px; height: 20px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
:focus-visible { outline: 3px solid color-mix(in srgb, var(--focus) 42%, transparent); outline-offset: 2px; }
|
||||
#main-content:focus { outline: none; }
|
||||
.skip-link { position: fixed; left: 12px; top: -80px; z-index: 1000; background: var(--surface); padding: 10px 14px; border-radius: var(--radius-sm); box-shadow: var(--shadow-md); }
|
||||
.skip-link:focus { top: 12px; }
|
||||
.sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||
|
||||
.app-shell { min-height: 100vh; display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); }
|
||||
.sidebar { position: sticky; top: 0; height: 100vh; overflow-y: auto; padding: 18px 14px; background: var(--sidebar); color: #fff; z-index: 40; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 4px 8px 18px; }
|
||||
.brand-mark { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 9px; background: linear-gradient(135deg, #3b82f6, #1d4ed8); color: #fff; font-weight: 800; letter-spacing: -0.04em; }
|
||||
.brand-name { font-size: 16px; font-weight: 700; letter-spacing: 0.01em; }
|
||||
.brand-subtitle { display: block; color: var(--sidebar-muted); font-size: 11px; font-weight: 500; }
|
||||
.side-label { margin: 12px 10px 6px; color: var(--sidebar-muted); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
.side-nav { display: grid; gap: 4px; }
|
||||
.side-nav button, .side-nav a { width: 100%; min-height: 44px; border: 0; border-radius: 8px; padding: 10px 12px; display: flex; align-items: center; gap: 10px; background: transparent; color: #c8d1e0; text-align: left; text-decoration: none; transition: background 180ms ease, color 180ms ease; }
|
||||
.side-nav button:hover, .side-nav a:hover { background: rgba(255,255,255,.07); color: #fff; }
|
||||
.side-nav button.active, .side-nav a.active { background: #243653; color: #fff; box-shadow: inset 3px 0 0 #60a5fa; }
|
||||
.side-note { margin: 18px 4px 0; padding: 12px; border: 1px solid rgba(255,255,255,.12); border-radius: 10px; background: rgba(255,255,255,.04); color: #c8d1e0; font-size: 12px; }
|
||||
.side-note strong { color: #fff; display: block; margin-bottom: 4px; }
|
||||
|
||||
.main-column { min-width: 0; }
|
||||
.topbar { position: sticky; top: 0; z-index: 30; min-height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 24px; border-bottom: 1px solid var(--border); background: rgba(255,255,255,.94); backdrop-filter: blur(10px); }
|
||||
.topbar-left, .topbar-actions, .inline { display: flex; align-items: center; gap: 10px; }
|
||||
.btn.mobile-menu { display: none; }
|
||||
.page-eyebrow { color: var(--text-muted); font-size: 12px; }
|
||||
.page-name { margin: 0; font-size: 18px; line-height: 1.25; }
|
||||
.prototype-pill { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 4px 9px; border: 1px solid #b8cdf8; border-radius: 999px; background: var(--primary-soft); color: #194fae; font-size: 12px; font-weight: 650; white-space: nowrap; }
|
||||
.prototype-pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--primary); }
|
||||
.content { width: min(1540px, 100%); margin: 0 auto; padding: 24px; }
|
||||
.view { display: none; animation: fade-in 180ms ease-out; }
|
||||
.view.active { display: block; }
|
||||
@keyframes fade-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 20px; }
|
||||
.page-header h1 { margin: 0 0 4px; font-size: clamp(22px, 2vw, 30px); line-height: 1.25; letter-spacing: -0.02em; }
|
||||
.page-header p { margin: 0; color: var(--text-muted); max-width: 760px; }
|
||||
.page-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
||||
.btn { min-height: 40px; padding: 8px 13px; border: 1px solid var(--border); border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; background: var(--surface); color: var(--text); font-weight: 650; text-decoration: none; transition: border-color 180ms ease, background 180ms ease, color 180ms ease, box-shadow 180ms ease; }
|
||||
.btn:hover { border-color: #a9b5c8; background: #f8fafc; }
|
||||
.btn-primary { border-color: var(--primary); background: var(--primary); color: #fff; }
|
||||
.btn-primary:hover { border-color: var(--primary-hover); background: var(--primary-hover); }
|
||||
.btn-danger { border-color: #e7b8b8; background: var(--danger-soft); color: var(--danger); }
|
||||
.btn-quiet { border-color: transparent; background: transparent; color: var(--text-muted); }
|
||||
.btn-icon { width: 42px; padding: 8px; }
|
||||
.btn:disabled { cursor: not-allowed; opacity: .48; }
|
||||
|
||||
.metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||
.metric { min-width: 0; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-sm); }
|
||||
.metric-label { color: var(--text-muted); font-size: 12px; font-weight: 650; }
|
||||
.metric-value { margin: 5px 0 3px; font-size: 26px; font-weight: 760; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
|
||||
.metric-foot { display: flex; align-items: center; gap: 6px; color: var(--text-muted); font-size: 12px; }
|
||||
.metric-foot strong { color: var(--success); }
|
||||
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-2 { grid-template-columns: minmax(0, 1.6fr) minmax(280px, .9fr); }
|
||||
.grid-even { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.card { min-width: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-sm); }
|
||||
.card-header { min-height: 54px; padding: 13px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--border); }
|
||||
.card-header h2, .card-header h3 { margin: 0; font-size: 15px; }
|
||||
.card-body { padding: 16px; }
|
||||
.card-subtitle { margin: 2px 0 0; color: var(--text-muted); font-size: 12px; }
|
||||
.stack { display: grid; gap: 12px; align-content: start; }
|
||||
|
||||
.status { min-height: 25px; padding: 3px 8px; display: inline-flex; align-items: center; gap: 6px; border-radius: 999px; font-size: 12px; font-weight: 680; white-space: nowrap; }
|
||||
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
||||
.status-success { background: var(--success-soft); color: var(--success); }
|
||||
.status-warning { background: var(--warning-soft); color: var(--warning); }
|
||||
.status-danger { background: var(--danger-soft); color: var(--danger); }
|
||||
.status-neutral { background: var(--neutral-soft); color: #4c5a70; }
|
||||
|
||||
.toolbar { display: flex; flex-wrap: wrap; align-items: end; gap: 10px; margin-bottom: 12px; }
|
||||
.field { display: grid; gap: 5px; min-width: 170px; }
|
||||
.field.grow { flex: 1 1 240px; }
|
||||
.field label { font-size: 12px; font-weight: 680; color: #465268; }
|
||||
.input, select.input, textarea.input { width: 100%; min-height: 42px; border: 1px solid #cfd7e4; border-radius: 8px; padding: 8px 11px; background: #fff; color: var(--text); }
|
||||
.input:focus { border-color: var(--focus); outline: 3px solid color-mix(in srgb, var(--focus) 18%, transparent); outline-offset: 0; }
|
||||
.help { color: var(--text-muted); font-size: 12px; }
|
||||
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; min-width: 680px; }
|
||||
th, td { padding: 11px 12px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: middle; }
|
||||
th { background: var(--surface-muted); color: #465268; font-size: 12px; font-weight: 720; }
|
||||
td { font-variant-numeric: tabular-nums; }
|
||||
tbody tr { transition: background 160ms ease; }
|
||||
tbody tr:hover { background: #f8fbff; }
|
||||
.table-title { font-weight: 680; }
|
||||
.table-subtitle { display: block; margin-top: 2px; color: var(--text-muted); font-size: 12px; }
|
||||
.empty-row { text-align: center; color: var(--text-muted); padding: 30px; }
|
||||
|
||||
.video-stage { position: relative; min-height: 360px; overflow: hidden; border-radius: 9px; background: radial-gradient(circle at 45% 38%, #31445d, #172337 55%, #0d1524); color: #dfe8f4; }
|
||||
.video-stage::before { content: ""; position: absolute; inset: 18% 12%; border: 1px solid rgba(255,255,255,.12); background: linear-gradient(145deg, transparent 45%, rgba(255,255,255,.04)); }
|
||||
.camera-caption { position: absolute; left: 14px; top: 14px; display: flex; gap: 8px; align-items: center; padding: 7px 10px; border-radius: 7px; background: rgba(7,13,24,.72); font-size: 12px; }
|
||||
.video-time { position: absolute; right: 14px; bottom: 12px; font: 12px ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||
.zone-shape { position: absolute; inset: 24% 22% 18% 18%; border: 2px solid #60a5fa; background: rgba(37,99,235,.16); clip-path: polygon(8% 14%, 88% 0, 100% 72%, 42% 100%, 0 78%); }
|
||||
.zone-label { position: absolute; left: 23%; top: 30%; padding: 4px 7px; border-radius: 5px; background: #1d4ed8; color: #fff; font-size: 11px; font-weight: 700; }
|
||||
|
||||
.activity-list { display: grid; gap: 10px; }
|
||||
.activity-item { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: start; padding: 10px 0; border-bottom: 1px solid var(--border); }
|
||||
.activity-item:last-child { border-bottom: 0; }
|
||||
.activity-icon { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 8px; background: var(--primary-soft); color: var(--primary); }
|
||||
.activity-title { font-weight: 680; }
|
||||
.activity-meta { color: var(--text-muted); font-size: 12px; }
|
||||
.activity-time { color: var(--text-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.pipeline { display: grid; grid-template-columns: repeat(6, minmax(112px, 1fr)); gap: 8px; overflow-x: auto; padding-bottom: 4px; }
|
||||
.pipeline-step { position: relative; min-height: 116px; padding: 13px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); }
|
||||
.pipeline-step:not(:last-child)::after { content: ""; position: absolute; right: -9px; top: 50%; width: 9px; border-top: 2px solid #9fb2ce; }
|
||||
.pipeline-index { width: 25px; height: 25px; display: grid; place-items: center; border-radius: 50%; background: var(--primary-soft); color: var(--primary); font-weight: 750; font-size: 12px; }
|
||||
.pipeline-step h3 { margin: 9px 0 3px; font-size: 13px; }
|
||||
.pipeline-step p { margin: 0; color: var(--text-muted); font-size: 12px; }
|
||||
.pipeline-step.running { border-color: #8bb2ff; box-shadow: inset 0 0 0 1px #8bb2ff; }
|
||||
|
||||
.alert-row { display: grid; grid-template-columns: 5px minmax(0, 1fr) auto; gap: 12px; padding: 13px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); cursor: pointer; }
|
||||
.alert-row:hover, .alert-row.selected { border-color: #9db7e8; background: #f8fbff; }
|
||||
.severity-bar { border-radius: 999px; background: var(--danger); }
|
||||
.severity-bar.medium { background: var(--warning); }
|
||||
.alert-title { font-weight: 720; }
|
||||
.alert-meta { display: flex; flex-wrap: wrap; gap: 8px 14px; margin-top: 4px; color: var(--text-muted); font-size: 12px; }
|
||||
.timeline { position: relative; display: grid; gap: 0; }
|
||||
.timeline-item { position: relative; padding: 0 0 18px 26px; }
|
||||
.timeline-item::before { content: ""; position: absolute; left: 6px; top: 7px; width: 9px; height: 9px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 0 4px var(--primary-soft); }
|
||||
.timeline-item:not(:last-child)::after { content: ""; position: absolute; left: 10px; top: 19px; bottom: 2px; border-left: 1px solid #bbcae0; }
|
||||
.timeline-title { font-weight: 680; }
|
||||
.timeline-meta { color: var(--text-muted); font-size: 12px; }
|
||||
|
||||
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 14px; overflow-x: auto; }
|
||||
.tabs button { min-height: 42px; padding: 8px 12px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--text-muted); font-weight: 650; white-space: nowrap; }
|
||||
.tabs button[aria-selected="true"] { border-bottom-color: var(--primary); color: var(--primary); }
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
.modal-backdrop { position: fixed; inset: 0; z-index: 100; display: none; align-items: center; justify-content: center; padding: 18px; background: rgba(7, 13, 24, .55); }
|
||||
.modal-backdrop.open { display: flex; }
|
||||
.modal { width: min(520px, 100%); max-height: min(760px, calc(100vh - 36px)); overflow-y: auto; border-radius: var(--radius-lg); background: var(--surface); box-shadow: 0 24px 70px rgba(0,0,0,.28); animation: modal-in 180ms ease-out; }
|
||||
@keyframes modal-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.modal-header, .modal-footer { padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.modal-header { border-bottom: 1px solid var(--border); }
|
||||
.modal-header h2 { margin: 0; font-size: 17px; }
|
||||
.modal-body { padding: 18px; }
|
||||
.modal-footer { border-top: 1px solid var(--border); justify-content: flex-end; }
|
||||
|
||||
.toast-region { position: fixed; right: 18px; bottom: 18px; z-index: 200; display: grid; gap: 8px; width: min(360px, calc(100vw - 36px)); }
|
||||
.toast { padding: 12px 14px; border: 1px solid #b9c8dc; border-radius: 9px; background: #172033; color: #fff; box-shadow: var(--shadow-md); animation: toast-in 180ms ease-out; }
|
||||
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
.prototype-home { min-height: 100vh; background: radial-gradient(circle at 80% 0%, #dbeafe, transparent 30%), var(--bg); }
|
||||
.home-nav { min-height: 68px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px clamp(18px, 5vw, 72px); border-bottom: 1px solid var(--border); background: rgba(255,255,255,.88); }
|
||||
.home-main { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: clamp(42px, 8vw, 92px) 0; }
|
||||
.hero { max-width: 850px; }
|
||||
.hero h1 { margin: 14px 0 16px; font-size: clamp(34px, 6vw, 64px); line-height: 1.05; letter-spacing: -0.045em; }
|
||||
.hero p { max-width: 720px; margin: 0; color: var(--text-muted); font-size: clamp(16px, 2vw, 20px); }
|
||||
.product-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 44px; }
|
||||
.product-card { min-height: 300px; padding: 22px; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-lg); background: rgba(255,255,255,.94); box-shadow: var(--shadow-sm); text-decoration: none; transition: border-color 180ms ease, box-shadow 180ms ease; }
|
||||
.product-card:hover { border-color: #9db7e8; box-shadow: var(--shadow-md); }
|
||||
.product-icon { width: 48px; height: 48px; display: grid; place-items: center; border-radius: 12px; background: var(--primary-soft); color: var(--primary); }
|
||||
.product-icon svg { width: 26px; height: 26px; }
|
||||
.product-card h2 { margin: 22px 0 8px; font-size: 23px; }
|
||||
.product-card p { margin: 0; color: var(--text-muted); }
|
||||
.product-card ul { padding-left: 18px; color: #465268; }
|
||||
.card-link { margin-top: auto; display: flex; align-items: center; justify-content: space-between; color: var(--primary); font-weight: 720; }
|
||||
.boundary-note { margin-top: 20px; padding: 14px 16px; border-left: 4px solid var(--warning); border-radius: 6px; background: var(--warning-soft); color: #714300; }
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.grid-2, .grid-even { grid-template-columns: 1fr; }
|
||||
.product-grid { grid-template-columns: 1fr; }
|
||||
.product-card { min-height: auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.app-shell { display: block; }
|
||||
.sidebar { position: fixed; left: 0; top: 0; width: min(300px, 88vw); transform: translateX(-102%); transition: transform 200ms ease; box-shadow: var(--shadow-md); }
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.btn.mobile-menu { display: inline-flex; }
|
||||
.topbar { padding: 9px 14px; }
|
||||
.topbar-actions .prototype-pill { display: none; }
|
||||
.content { padding: 18px 14px 28px; }
|
||||
.page-header { display: grid; }
|
||||
.page-actions { justify-content: flex-start; }
|
||||
.metric-grid { grid-template-columns: 1fr 1fr; }
|
||||
.metric-value { font-size: 22px; }
|
||||
.video-stage { min-height: 260px; }
|
||||
.pipeline { grid-template-columns: repeat(2, minmax(0, 1fr)); overflow: visible; }
|
||||
.pipeline-step:not(:last-child)::after { display: none; }
|
||||
.home-nav { padding: 10px 16px; }
|
||||
.home-main { width: min(100% - 28px, 1180px); padding-top: 44px; }
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.metric-grid { grid-template-columns: 1fr; }
|
||||
.topbar-actions .btn { display: none; }
|
||||
.page-actions .btn { flex: 1 1 auto; }
|
||||
.activity-item { grid-template-columns: auto minmax(0, 1fr); }
|
||||
.activity-time { grid-column: 2; }
|
||||
.pipeline { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="YoVision Bell 当前 MVP 交互原型">
|
||||
<title>YoVision Bell · 当前 MVP 原型</title>
|
||||
<link rel="stylesheet" href="../assets/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg class="sr-only" aria-hidden="true">
|
||||
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
|
||||
<symbol id="icon-bell" viewBox="0 0 24 24"><path d="M18 8a6 6 0 00-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"/></symbol>
|
||||
<symbol id="icon-file" viewBox="0 0 24 24"><path d="M6 2h8l4 4v16H6zM14 2v5h5M9 13h6m-6 4h6"/></symbol>
|
||||
<symbol id="icon-rule" viewBox="0 0 24 24"><path d="M4 6h10m4 0h2M4 12h3m4 0h9M4 18h7m4 0h5"/><circle cx="16" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="13" cy="18" r="2"/></symbol>
|
||||
<symbol id="icon-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
||||
<symbol id="icon-activity" viewBox="0 0 24 24"><path d="M3 12h4l2-7 4 14 2-7h6"/></symbol>
|
||||
</svg>
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" data-sidebar aria-label="Bell 主导航">
|
||||
<a class="brand" href="../index.html" style="text-decoration:none"><span class="brand-mark">B</span><span><span class="brand-name">YoVision Bell</span><span class="brand-subtitle">事件预警与处置</span></span></a>
|
||||
<div class="side-label">值班工作区</div>
|
||||
<nav class="side-nav">
|
||||
<button class="active" data-view-target="duty" data-title="实时值班台"><svg aria-hidden="true"><use href="#icon-bell"></use></svg>实时值班台</button>
|
||||
<button data-view-target="events" data-title="事件中心"><svg aria-hidden="true"><use href="#icon-file"></use></svg>事件中心</button>
|
||||
<button data-view-target="rules" data-title="规则策略"><svg aria-hidden="true"><use href="#icon-rule"></use></svg>规则策略</button>
|
||||
</nav>
|
||||
<div class="side-note"><strong>独立产品边界</strong>Bell 使用自己的账户、会话和数据库。当前通过合成事件验收,不接受 Sense 用户 token。</div>
|
||||
</aside>
|
||||
<div class="main-column">
|
||||
<header class="topbar"><div class="topbar-left"><button class="btn btn-icon mobile-menu" data-mobile-menu aria-label="打开主导航" aria-expanded="false"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button><div><div class="page-eyebrow">青藤学校 · Bell 值班账号</div><h2 class="page-name" data-page-name>实时值班台</h2></div></div><div class="topbar-actions"><span class="prototype-pill">交互原型 · 非生产数据</span><a class="btn" href="../index.html">返回原型入口</a></div></header>
|
||||
<main id="main-content" class="content" tabindex="-1">
|
||||
<section class="view active" data-view="duty">
|
||||
<div class="page-header"><div><h1>实时值班台</h1><p>使用合成事件完成 Event → Rule → Alert → ack → close;原始 Event 始终保持不可变。</p></div><div class="page-actions"><button class="btn btn-primary" data-open-modal="inject-event"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>注入合成事件</button></div></div>
|
||||
<div class="metric-grid"><article class="metric"><div class="metric-label">待确认 Alert</div><div class="metric-value" data-alert-count>3</div><div class="metric-foot"><span class="status status-danger">1 高危</span></div></article><article class="metric"><div class="metric-label">处理中</div><div class="metric-value">2</div><div class="metric-foot"><span>已明确处置人</span></div></article><article class="metric"><div class="metric-label">今日关闭</div><div class="metric-value">18</div><div class="metric-foot"><strong>中位 3m 42s</strong></div></article><article class="metric"><div class="metric-label">重复收据</div><div class="metric-value">6</div><div class="metric-foot"><span>均未重复建 Alert</span></div></article></div>
|
||||
<div class="grid grid-2">
|
||||
<div class="stack" aria-label="待处置预警列表">
|
||||
<article class="alert-row selected" role="button" tabindex="0" data-select-alert="危险区域闯入"><span class="severity-bar"></span><div><div class="alert-title">危险区域闯入</div><div class="alert-meta"><span>北围墙 01</span><span>2 个关联 Event</span><span>未确认</span></div></div><span class="activity-time">10:24</span></article>
|
||||
<article class="alert-row" role="button" tabindex="0" data-select-alert="方向越线"><span class="severity-bar medium"></span><div><div class="alert-title">反向越过警戒线</div><div class="alert-meta"><span>宿舍东门 02</span><span>1 个 Event</span><span>处理中</span></div></div><span class="activity-time">10:19</span></article>
|
||||
<article class="alert-row" role="button" tabindex="0" data-select-alert="区域停留"><span class="severity-bar medium"></span><div><div class="alert-title">危险区域持续停留</div><div class="alert-meta"><span>食堂后门 01</span><span>3 个关联 Event</span><span>未确认</span></div></div><span class="activity-time">10:11</span></article>
|
||||
</div>
|
||||
<article class="card"><header class="card-header"><div><h2>危险区域闯入</h2><p class="card-subtitle">Alert BEL-A-20260812-0031</p></div><span class="status status-danger" data-alert-state>未确认</span></header><div class="card-body">
|
||||
<div class="tabs" role="tablist" data-tabs aria-label="预警详情"><button role="tab" aria-selected="true" data-tab-target="alert-summary">处置摘要</button><button role="tab" aria-selected="false" tabindex="-1" data-tab-target="alert-events">关联 Event</button></div>
|
||||
<div id="alert-summary" class="tab-panel active"><div class="stack"><div><span class="metric-label">规则</span><div class="table-title">校园高危区域 · v4</div></div><div><span class="metric-label">处置建议</span><div>确认现场情况;如无法确认,联系校内保安巡查。</div></div><div class="inline"><button class="btn btn-primary" data-action="ack-alert">确认预警</button><button class="btn" data-action="close-alert" disabled>完成并关闭</button></div></div></div>
|
||||
<div id="alert-events" class="tab-panel" hidden><div class="activity-list"><div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-file"></use></svg></div><div><div class="activity-title">producer-lab / evt-000041</div><div class="activity-meta">不可变 Event · 10:23:48 · 证据引用可用</div></div></div><div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-file"></use></svg></div><div><div class="activity-title">producer-lab / evt-000042</div><div class="activity-meta">不可变 Event · 10:23:55 · 同一 Alert 聚合</div></div></div></div></div>
|
||||
</div><header class="card-header"><h3>审计时间线</h3></header><div class="card-body timeline" data-alert-timeline><div class="timeline-item"><div class="timeline-title">规则命中并创建 Alert</div><div class="timeline-meta">10:23:56 · system · 规则 v4</div></div><div class="timeline-item"><div class="timeline-title">值班台已显示</div><div class="timeline-meta">10:23:57 · sent/seen 与 ack 分离</div></div></div></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="events" hidden><div class="page-header"><div><h1>事件中心</h1><p>结构化 Event 校验后不可修改;误报和处置结果记录在 Alert/审计事实中。</p></div><div class="page-actions"><button class="btn btn-primary" data-open-modal="inject-event">注入合成事件</button></div></div><article class="card"><div class="card-body"><div class="toolbar"><div class="field grow"><label for="event-filter">搜索事件</label><input id="event-filter" class="input" data-filter-input="#event-table" placeholder="producer、source event ID 或规则"></div><div class="field"><label for="receipt-status">收据状态</label><select id="receipt-status" class="input"><option>全部</option><option>首次接收</option><option>重复收据</option></select></div></div><div class="table-wrap"><table id="event-table"><thead><tr><th>Event</th><th>Producer / Source ID</th><th>接收时间</th><th>收据</th><th>Alert</th></tr></thead><tbody><tr><td><span class="table-title">危险区域进入</span><span class="table-subtitle">结构化事实已冻结</span></td><td>producer-lab / evt-000042</td><td>10:23:55</td><td><span class="status status-success">首次接收</span></td><td>BEL-A-0031</td></tr><tr><td><span class="table-title">危险区域进入</span><span class="table-subtitle">正文与首次一致</span></td><td>producer-lab / evt-000042</td><td>10:24:03</td><td><span class="status status-neutral">重复收据</span></td><td>未重复创建</td></tr><tr><td><span class="table-title">方向越线</span><span class="table-subtitle">结构化事实已冻结</span></td><td>producer-lab / evt-000039</td><td>10:19:12</td><td><span class="status status-success">首次接收</span></td><td>BEL-A-0029</td></tr></tbody></table></div></div></article></section>
|
||||
|
||||
<section class="view" data-view="rules" hidden><div class="page-header"><div><h1>规则策略</h1><p>MVP 仅展示事件到 Alert 的基本匹配;自动升级、通知和排班属于后续阶段。</p></div><div class="page-actions"><button class="btn btn-primary" data-toast="已创建规则草稿,不影响当前发布版本">新建规则草稿</button></div></div><div class="grid grid-even"><article class="card"><header class="card-header"><div><h2>校园高危区域</h2><p class="card-subtitle">发布版本 v4</p></div><span class="status status-success">生效</span></header><div class="card-body stack"><div><span class="metric-label">匹配条件</span><div class="table-title">event.type = zone_intrusion</div></div><div><span class="metric-label">Alert 严重度</span><div class="table-title">高危</div></div><button class="btn" data-toast="已进入校园高危区域规则草稿">编辑草稿</button></div></article><article class="card"><header class="card-header"><div><h2>方向越线</h2><p class="card-subtitle">发布版本 v2</p></div><span class="status status-success">生效</span></header><div class="card-body stack"><div><span class="metric-label">匹配条件</span><div class="table-title">event.type = line_crossing</div></div><div><span class="metric-label">Alert 严重度</span><div class="table-title">中危</div></div><button class="btn" data-toast="已进入方向越线规则草稿">编辑草稿</button></div></article></div></section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop" id="inject-event" aria-hidden="true"><section class="modal" role="dialog" aria-modal="true" aria-labelledby="inject-title"><header class="modal-header"><h2 id="inject-title">注入合成事件</h2><button class="btn btn-icon btn-quiet" data-close-modal aria-label="关闭合成事件窗口">×</button></header><div class="modal-body stack"><div class="boundary-note" style="margin:0">该入口仅用于开发测试,生产环境必须禁用。</div><div class="field"><label for="synthetic-type">事件类型</label><select id="synthetic-type" class="input"><option>危险区域进入</option><option>方向越线</option></select></div><div class="field"><label for="synthetic-producer">Producer identity</label><input id="synthetic-producer" class="input" value="producer-lab" readonly></div><div class="field"><label for="synthetic-source">Source event ID</label><input id="synthetic-source" class="input" value="evt-demo-0001"></div></div><footer class="modal-footer"><button class="btn" data-close-modal>取消</button><button class="btn btn-primary" data-action="inject-event">注入合成事件</button></footer></section></div>
|
||||
<div class="toast-region" data-toast-region aria-live="polite"></div>
|
||||
<script src="../assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="YoVision Brain 当前 MVP 内部流程原型">
|
||||
<title>YoVision Brain · 当前 MVP 内部流程原型</title>
|
||||
<link rel="stylesheet" href="../assets/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg class="sr-only" aria-hidden="true">
|
||||
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
|
||||
<symbol id="icon-cpu" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 1v3m6-3v3M9 20v3m6-3v3M1 9h3m-3 6h3m16-6h3m-3 6h3M9 9h6v6H9z"/></symbol>
|
||||
<symbol id="icon-input" viewBox="0 0 24 24"><path d="M4 12h13m-5-5l5 5-5 5M20 5v14"/></symbol>
|
||||
<symbol id="icon-activity" viewBox="0 0 24 24"><path d="M3 12h4l2-7 4 14 2-7h6"/></symbol>
|
||||
<symbol id="icon-file" viewBox="0 0 24 24"><path d="M6 2h8l4 4v16H6zM14 2v5h5M9 13h6m-6 4h6"/></symbol>
|
||||
</svg>
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" data-sidebar aria-label="Brain 内部导航">
|
||||
<a class="brand" href="../index.html" style="text-decoration:none"><span class="brand-mark">B</span><span><span class="brand-name">YoVision Brain</span><span class="brand-subtitle">匿名推理交付单元</span></span></a>
|
||||
<div class="side-label">内部验收</div>
|
||||
<nav class="side-nav">
|
||||
<button class="active" data-view-target="pipeline" data-title="推理流水线"><svg aria-hidden="true"><use href="#icon-cpu"></use></svg>推理流水线</button>
|
||||
<button data-view-target="inputs" data-title="输入夹具"><svg aria-hidden="true"><use href="#icon-input"></use></svg>输入夹具</button>
|
||||
<button data-view-target="events" data-title="项目内事件"><svg aria-hidden="true"><use href="#icon-activity"></use></svg>项目内事件</button>
|
||||
</nav>
|
||||
<div class="side-note"><strong>不是客户管理后台</strong>Brain 无交互式账户,不持有 Alert、ack 或通知状态。本页仅用于开发、演示与验收内部流水线。</div>
|
||||
</aside>
|
||||
<div class="main-column">
|
||||
<header class="topbar"><div class="topbar-left"><button class="btn btn-icon mobile-menu" data-mobile-menu aria-label="打开内部导航" aria-expanded="false"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button><div><div class="page-eyebrow">synthetic-lab · CPU 降级模式</div><h2 class="page-name" data-page-name>推理流水线</h2></div></div><div class="topbar-actions"><span class="prototype-pill">内部流程原型</span><a class="btn" href="../index.html">返回原型入口</a></div></header>
|
||||
<main id="main-content" class="content" tabindex="-1">
|
||||
<section class="view active" data-view="pipeline">
|
||||
<div class="page-header"><div><h1>单路匿名推理流水线</h1><p>使用合成视频和项目内配置独立验收,不依赖 Sense/Bell 在线,也不输出正式跨项目契约。</p></div><div class="page-actions"><button class="btn" data-toast="流水线已暂停;现有项目内事件不受影响">暂停</button><button class="btn btn-primary" data-action="run-frame">处理下一帧</button></div></div>
|
||||
<div class="metric-grid"><article class="metric"><div class="metric-label">已处理帧</div><div class="metric-value" data-frame-count>12,842</div><div class="metric-foot"><strong>24.8 FPS</strong><span>· 单路</span></div></article><article class="metric"><div class="metric-label">端到端延迟</div><div class="metric-value">82 ms</div><div class="metric-foot"><strong>实验室值</strong><span>· 非容量承诺</span></div></article><article class="metric"><div class="metric-label">当前目标</div><div class="metric-value">3</div><div class="metric-foot"><span>匿名 track ID</span></div></article><article class="metric"><div class="metric-label">候选事件</div><div class="metric-value">7</div><div class="metric-foot"><span class="status status-neutral">项目内</span></div></article></div>
|
||||
<article class="card"><header class="card-header"><div><h2>执行链</h2><p class="card-subtitle">每层均可替换并独立测试</p></div><span class="status status-success">运行中</span></header><div class="card-body"><div class="pipeline" aria-label="推理流水线六个步骤">
|
||||
<div class="pipeline-step running"><span class="pipeline-index">1</span><h3>输入适配</h3><p>synthetic-campus.mp4<br>1920×1080 · 25 FPS</p></div>
|
||||
<div class="pipeline-step running"><span class="pipeline-index">2</span><h3>视频解码</h3><p>software decode<br>队列 2 帧</p></div>
|
||||
<div class="pipeline-step running"><span class="pipeline-index">3</span><h3>匿名检测</h3><p>person · 3 boxes<br>模型 demo-v1</p></div>
|
||||
<div class="pipeline-step running"><span class="pipeline-index">4</span><h3>单路跟踪</h3><p>3 active tracks<br>无身份信息</p></div>
|
||||
<div class="pipeline-step running"><span class="pipeline-index">5</span><h3>区域判定</h3><p>危险区域 + 方向线<br>规则 v3</p></div>
|
||||
<div class="pipeline-step"><span class="pipeline-index">6</span><h3>项目内事件</h3><p>本地 JSON<br>未绑定 Bell 契约</p></div>
|
||||
</div></div></article>
|
||||
<div class="grid grid-2" style="margin-top:16px"><article class="card"><header class="card-header"><div><h2>模拟画面</h2><p class="card-subtitle">不包含真实未成年人影像</p></div></header><div class="card-body"><div class="video-stage" role="img" aria-label="合成校园画面,显示匿名跟踪框和方向警戒线"><div class="camera-caption"><span class="status status-success">合成输入</span>synthetic-camera-01</div><div class="zone-shape"></div><span class="zone-label">track-0042 · person</span><div style="position:absolute;left:62%;top:34%;width:72px;height:132px;border:2px solid #f59e0b"></div><span class="video-time">frame 12,842</span></div></div></article><article class="card"><header class="card-header"><div><h2>最近候选事实</h2><p class="card-subtitle">正式事件结构等待协同工单</p></div></header><div class="card-body activity-list" data-brain-events><div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-activity"></use></svg></div><div><div class="activity-title">危险区域进入候选</div><div class="activity-meta">track-0042 · 置信度 0.88 · rule-v3</div></div><div class="activity-time">10:20:16</div></div><div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-activity"></use></svg></div><div><div class="activity-title">方向越线候选</div><div class="activity-meta">track-0038 · east-to-west · rule-v3</div></div><div class="activity-time">10:19:42</div></div></div></article></div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="inputs" hidden><div class="page-header"><div><h1>输入夹具</h1><p>本地视频、合成帧和项目内区域配置,确保 Brain 可脱离 Sense 独立测试。</p></div><div class="page-actions"><button class="btn btn-primary" data-toast="已校验夹具:视频、Profile 与区域坐标匹配">校验全部夹具</button></div></div><article class="card"><div class="table-wrap"><table><thead><tr><th>夹具</th><th>类型</th><th>Profile</th><th>区域配置</th><th>状态</th></tr></thead><tbody><tr><td><span class="table-title">synthetic-campus.mp4</span><span class="table-subtitle">无真实人物数据</span></td><td>合成视频</td><td>1920×1080 / 25 FPS</td><td>zone-v3.json</td><td><span class="status status-success">可用</span></td></tr><tr><td><span class="table-title">cross-line-east.jsonl</span><span class="table-subtitle">可重复确定性输入</span></td><td>合成帧序列</td><td>1280×720 / 15 FPS</td><td>line-v2.json</td><td><span class="status status-success">可用</span></td></tr><tr><td><span class="table-title">local-sample.mp4</span><span class="table-subtitle">仓库外路径引用</span></td><td>本地视频</td><td>尚未探测</td><td>无</td><td><span class="status status-warning">待配置</span></td></tr></tbody></table></div></article></section>
|
||||
|
||||
<section class="view" data-view="events" hidden><div class="page-header"><div><h1>项目内匿名事件</h1><p>仅用于 Brain 独立纵切验收。它不是 `contracts/` 中的标准事件,也不会直接投递 Bell。</p></div><div class="page-actions"><button class="btn" data-toast="已导出 7 条脱敏项目内事件">导出测试结果</button></div></div><div class="boundary-note" style="margin:0 0 16px"><strong>契约门禁:</strong>禁止在本目录复制一份临时 Bell 事件协议。正式 producer/source event ID、设备逻辑引用和证据字段由后续协同工单冻结。</div><article class="card"><div class="table-wrap"><table><thead><tr><th>本地事件 ID</th><th>规则</th><th>匿名观测</th><th>规则/模型版本</th><th>结果</th></tr></thead><tbody><tr><td>local-0007</td><td>危险区域进入</td><td>track-0042</td><td>rule-v3 / demo-v1</td><td><span class="status status-success">已生成</span></td></tr><tr><td>local-0006</td><td>方向越线</td><td>track-0038</td><td>rule-v3 / demo-v1</td><td><span class="status status-success">已生成</span></td></tr><tr><td>local-0005</td><td>危险区域停留</td><td>track-0031</td><td>rule-v2 / demo-v1</td><td><span class="status status-neutral">已抑制</span></td></tr></tbody></table></div></article></section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast-region" data-toast-region aria-live="polite"></div>
|
||||
<script src="../assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,227 @@
|
||||
# Design System Master File
|
||||
|
||||
> **LOGIC:** When building a specific page, first check `design-system/pages/[page-name].md`.
|
||||
> If that file exists, its rules **override** this Master file.
|
||||
> If not, strictly follow the rules below.
|
||||
|
||||
---
|
||||
|
||||
**Project:** YoVision
|
||||
**Generated:** 2026-08-12 10:10:03
|
||||
**Category:** Analytics Dashboard
|
||||
**Design Dials:** Variance 4/10 (Balanced / Modern) | Motion 3/10 (Subtle) | Density 8/10 (Dense / Dashboard)
|
||||
|
||||
---
|
||||
|
||||
## Global Rules
|
||||
|
||||
### Color Palette
|
||||
|
||||
| Role | Hex | CSS Variable |
|
||||
|------|-----|--------------|
|
||||
| Primary | `#DC2626` | `--color-primary` |
|
||||
| On Primary | `#FFFFFF` | `--color-on-primary` |
|
||||
| Secondary | `#EF4444` | `--color-secondary` |
|
||||
| Accent/CTA | `#2563EB` | `--color-accent` |
|
||||
| Background | `#FFF1F2` | `--color-background` |
|
||||
| Foreground | `#0F172A` | `--color-foreground` |
|
||||
| Muted | `#FCF1F1` | `--color-muted` |
|
||||
| Border | `#FAE4E4` | `--color-border` |
|
||||
| Destructive | `#DC2626` | `--color-destructive` |
|
||||
| Ring | `#DC2626` | `--color-ring` |
|
||||
|
||||
**Color Notes:** Alert red + safety blue
|
||||
|
||||
### Typography
|
||||
|
||||
- **Heading Font:** Inter
|
||||
- **Body Font:** Inter
|
||||
- **Mood:** minimal, clean, swiss, functional, neutral, professional
|
||||
- **Google Fonts:** [Inter + Inter](https://fonts.google.com/share?selection.family=Inter:wght@300;400;500;600;700)
|
||||
|
||||
**CSS Import:**
|
||||
```css
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
```
|
||||
|
||||
### Spacing Variables
|
||||
|
||||
*Density: 8/10 — Dense / Dashboard*
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| `--space-xs` | `2px` / `0.125rem` | Tight gaps |
|
||||
| `--space-sm` | `4px` / `0.25rem` | Icon gaps, inline spacing |
|
||||
| `--space-md` | `8px` / `0.5rem` | Standard padding |
|
||||
| `--space-lg` | `12px` / `0.75rem` | Section padding |
|
||||
| `--space-xl` | `16px` / `1rem` | Large gaps |
|
||||
| `--space-2xl` | `24px` / `1.5rem` | Section margins |
|
||||
| `--space-3xl` | `32px` / `2rem` | Hero padding |
|
||||
|
||||
### Shadow Depths
|
||||
|
||||
| Level | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| `--shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift |
|
||||
| `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | Cards, buttons |
|
||||
| `--shadow-lg` | `0 10px 15px rgba(0,0,0,0.1)` | Modals, dropdowns |
|
||||
| `--shadow-xl` | `0 20px 25px rgba(0,0,0,0.15)` | Hero images, featured cards |
|
||||
|
||||
---
|
||||
|
||||
## Component Specs
|
||||
|
||||
### Buttons
|
||||
|
||||
```css
|
||||
/* Primary Button */
|
||||
.btn-primary {
|
||||
background: #2563EB;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Secondary Button */
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #DC2626;
|
||||
border: 2px solid #DC2626;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
```
|
||||
|
||||
### Cards
|
||||
|
||||
```css
|
||||
.card {
|
||||
background: #FFF1F2;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
```
|
||||
|
||||
### Inputs
|
||||
|
||||
```css
|
||||
.input {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 200ms ease;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #DC2626;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px #DC262620;
|
||||
}
|
||||
```
|
||||
|
||||
### Modals
|
||||
|
||||
```css
|
||||
.modal-overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-xl);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style Guidelines
|
||||
|
||||
**Style:** Data-Dense Dashboard
|
||||
|
||||
**Keywords:** Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility
|
||||
|
||||
**Best For:** Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing
|
||||
|
||||
**Key Effects:** Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners
|
||||
|
||||
### Page Pattern
|
||||
|
||||
**Pattern Name:** Real-Time / Operations Landing
|
||||
|
||||
- **Conversion Strategy:** For ops/security/iot products. Demo or sandbox link. Trust signals.
|
||||
- **CTA Placement:** Primary CTA in nav + After metrics
|
||||
- **Section Order:** 1. Hero (product + live preview or status), 2. Key metrics/indicators, 3. How it works, 4. CTA (Start trial / Contact)
|
||||
|
||||
---
|
||||
|
||||
## Motion
|
||||
|
||||
**Page Transition** (Subtle) — Trigger: route change | Duration: 200-300ms | Easing: `power1.inOut`
|
||||
|
||||
```js
|
||||
gsap.to(main, { opacity: 0, duration: 0.2, onComplete: () => { navigate(); gsap.fromTo(main, { opacity: 0 }, { opacity: 1, duration: 0.2 }); } });
|
||||
```
|
||||
|
||||
**Framework notes:** Pair with the router's transition hooks (Next.js App Router transitions, React Router's useNavigate, Vue Router's beforeEach/afterEach)
|
||||
|
||||
- ✅ Preload the destination route's critical assets before the exit tween finishes
|
||||
- ❌ Don't block navigation on animation; cap exit duration at ~250ms so the app never feels unresponsive
|
||||
- ⚡ Exit animation should always resolve faster than entrance (asymmetric timing) so back/forward feels snappy
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (Do NOT Use)
|
||||
|
||||
- ❌ Ornate design
|
||||
- ❌ No filtering
|
||||
|
||||
### Additional Forbidden Patterns
|
||||
|
||||
- ❌ **Emojis as icons** — Use SVG icons (Heroicons, Lucide, Simple Icons)
|
||||
- ❌ **Missing cursor:pointer** — All clickable elements must have cursor:pointer
|
||||
- ❌ **Layout-shifting hovers** — Avoid scale transforms that shift layout
|
||||
- ❌ **Low contrast text** — Maintain 4.5:1 minimum contrast ratio
|
||||
- ❌ **Instant state changes** — Always use transitions (150-300ms)
|
||||
- ❌ **Invisible focus states** — Focus states must be visible for a11y
|
||||
|
||||
---
|
||||
|
||||
## Pre-Delivery Checklist
|
||||
|
||||
Before delivering any UI code, verify:
|
||||
|
||||
- [ ] No emojis used as icons (use SVG instead)
|
||||
- [ ] All icons from consistent icon set (Heroicons/Lucide)
|
||||
- [ ] `cursor-pointer` on all clickable elements
|
||||
- [ ] Hover states with smooth transitions (150-300ms)
|
||||
- [ ] Light mode: text contrast 4.5:1 minimum
|
||||
- [ ] Focus states visible for keyboard navigation
|
||||
- [ ] `prefers-reduced-motion` respected
|
||||
- [ ] Responsive: 375px, 768px, 1024px, 1440px
|
||||
- [ ] No content hidden behind fixed navbars
|
||||
- [ ] No horizontal scroll on mobile
|
||||
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="YoVision Sense、Brain、Bell 当前 MVP 交互原型入口">
|
||||
<title>YoVision · 当前 MVP 交互原型</title>
|
||||
<link rel="stylesheet" href="assets/styles.css">
|
||||
</head>
|
||||
<body class="prototype-home">
|
||||
<svg class="sr-only" aria-hidden="true">
|
||||
<symbol id="icon-camera" viewBox="0 0 24 24"><path d="M15 10l4.5-2.5v9L15 14z"/><rect x="3" y="6" width="12" height="12" rx="2"/></symbol>
|
||||
<symbol id="icon-cpu" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 1v3m6-3v3M9 20v3m6-3v3M1 9h3m-3 6h3m16-6h3m-3 6h3M9 9h6v6H9z"/></symbol>
|
||||
<symbol id="icon-bell" viewBox="0 0 24 24"><path d="M18 8a6 6 0 00-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"/></symbol>
|
||||
<symbol id="icon-arrow" viewBox="0 0 24 24"><path d="M5 12h14m-5-5l5 5-5 5"/></symbol>
|
||||
</svg>
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
<header class="home-nav">
|
||||
<div class="brand" aria-label="YoVision">
|
||||
<span class="brand-mark">YV</span>
|
||||
<span><span class="brand-name" style="color:var(--text)">YoVision</span><span class="brand-subtitle" style="color:var(--text-muted)">School Safety Suite</span></span>
|
||||
</div>
|
||||
<span class="prototype-pill">MVP #8 · 交互原型</span>
|
||||
</header>
|
||||
<main id="main-content" class="home-main" tabindex="-1">
|
||||
<section class="hero">
|
||||
<span class="status status-neutral">三项目 · 两个独立产品</span>
|
||||
<h1>先独立跑通,<br>再可靠协作。</h1>
|
||||
<p>这组原型对应当前 YoVision MVP:Sense 负责设备与视频感知管理,Brain 负责无界面匿名推理,Bell 负责独立预警处置。页面中的数据均为演示数据。</p>
|
||||
</section>
|
||||
|
||||
<section class="product-grid" aria-label="选择产品原型">
|
||||
<a class="product-card" href="sense/index.html">
|
||||
<span class="product-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></span>
|
||||
<h2>Sense</h2>
|
||||
<p>智能 NVR 与视频感知管理面</p>
|
||||
<ul><li>设备与 ONVIF/RTSP</li><li>MediaMTX 与实时监看</li><li>区域和方向警戒线</li></ul>
|
||||
<span class="card-link">打开 Sense 原型 <svg aria-hidden="true"><use href="#icon-arrow"></use></svg></span>
|
||||
</a>
|
||||
<a class="product-card" href="brain/index.html">
|
||||
<span class="product-icon"><svg aria-hidden="true"><use href="#icon-cpu"></use></svg></span>
|
||||
<h2>Brain</h2>
|
||||
<p>无客户界面的匿名推理交付单元</p>
|
||||
<ul><li>合成/本地输入</li><li>解码、检测与跟踪</li><li>区域/越线项目内事件</li></ul>
|
||||
<span class="card-link">打开内部流程原型 <svg aria-hidden="true"><use href="#icon-arrow"></use></svg></span>
|
||||
</a>
|
||||
<a class="product-card" href="bell/index.html">
|
||||
<span class="product-icon"><svg aria-hidden="true"><use href="#icon-bell"></use></svg></span>
|
||||
<h2>Bell</h2>
|
||||
<p>独立事件预警与处置产品</p>
|
||||
<ul><li>合成事件与不可变 Event</li><li>规则匹配与 Alert</li><li>并发 ack、close 与审计</li></ul>
|
||||
<span class="card-link">打开 Bell 原型 <svg aria-hidden="true"><use href="#icon-arrow"></use></svg></span>
|
||||
</a>
|
||||
</section>
|
||||
<aside class="boundary-note"><strong>边界说明:</strong>三个原型不代表共享账号、数据库或会话。Brain 页面仅供开发与验收理解内部流水线,不是第三个销售管理后台。</aside>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="YoVision Sense 当前 MVP 交互原型">
|
||||
<title>YoVision Sense · 当前 MVP 原型</title>
|
||||
<link rel="stylesheet" href="../assets/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg class="sr-only" aria-hidden="true">
|
||||
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
|
||||
<symbol id="icon-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></symbol>
|
||||
<symbol id="icon-camera" viewBox="0 0 24 24"><path d="M15 10l4.5-2.5v9L15 14z"/><rect x="3" y="6" width="12" height="12" rx="2"/></symbol>
|
||||
<symbol id="icon-monitor" viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="14" rx="2"/><path d="M8 22h8m-4-4v4"/></symbol>
|
||||
<symbol id="icon-zone" viewBox="0 0 24 24"><path d="M5 4l14 2 2 12-11 3-7-8z"/><circle cx="5" cy="4" r="1"/><circle cx="19" cy="6" r="1"/><circle cx="21" cy="18" r="1"/><circle cx="10" cy="21" r="1"/><circle cx="3" cy="13" r="1"/></symbol>
|
||||
<symbol id="icon-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
|
||||
<symbol id="icon-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></symbol>
|
||||
<symbol id="icon-activity" viewBox="0 0 24 24"><path d="M3 12h4l2-7 4 14 2-7h6"/></symbol>
|
||||
</svg>
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" data-sidebar aria-label="Sense 主导航">
|
||||
<a class="brand" href="../index.html" style="text-decoration:none">
|
||||
<span class="brand-mark">S</span><span><span class="brand-name">YoVision Sense</span><span class="brand-subtitle">视频感知管理面</span></span>
|
||||
</a>
|
||||
<div class="side-label">MVP 工作区</div>
|
||||
<nav class="side-nav">
|
||||
<button class="active" data-view-target="overview" data-title="运行总览"><svg aria-hidden="true"><use href="#icon-grid"></use></svg>运行总览</button>
|
||||
<button data-view-target="devices" data-title="设备接入"><svg aria-hidden="true"><use href="#icon-camera"></use></svg>设备接入</button>
|
||||
<button data-view-target="live" data-title="实时监看"><svg aria-hidden="true"><use href="#icon-monitor"></use></svg>实时监看</button>
|
||||
<button data-view-target="zones" data-title="区域配置"><svg aria-hidden="true"><use href="#icon-zone"></use></svg>区域配置</button>
|
||||
</nav>
|
||||
<div class="side-note"><strong>独立产品边界</strong>当前页面不需要 Brain 或 Bell 在线;摄像头凭据只写不读,演示中不展示真实密码。</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-column">
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<button class="btn btn-icon mobile-menu" type="button" data-mobile-menu aria-label="打开主导航" aria-expanded="false"><svg aria-hidden="true"><use href="#icon-menu"></use></svg></button>
|
||||
<div><div class="page-eyebrow">青藤实验站点 · Sense</div><h2 class="page-name" data-page-name>运行总览</h2></div>
|
||||
</div>
|
||||
<div class="topbar-actions"><span class="prototype-pill">交互原型 · 非生产数据</span><a class="btn" href="../index.html">返回原型入口</a></div>
|
||||
</header>
|
||||
|
||||
<main id="main-content" class="content" tabindex="-1">
|
||||
<section class="view active" data-view="overview">
|
||||
<div class="page-header"><div><h1>运行总览</h1><p>设备、媒体与配置的最小可见状态。Brain、Bell 当前均未连接,不影响 Sense 独立运行。</p></div><div class="page-actions"><button class="btn btn-primary" data-open-modal="add-device"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>添加设备</button></div></div>
|
||||
<div class="metric-grid">
|
||||
<article class="metric"><div class="metric-label">视频设备</div><div class="metric-value">6</div><div class="metric-foot"><strong>5 在线</strong><span>· 1 待激活</span></div></article>
|
||||
<article class="metric"><div class="metric-label">MediaMTX 路径</div><div class="metric-value">5/5</div><div class="metric-foot"><strong>已收敛</strong><span>· 12 秒前</span></div></article>
|
||||
<article class="metric"><div class="metric-label">区域版本</div><div class="metric-value">8</div><div class="metric-foot"><strong>7 生效</strong><span>· 1 草稿</span></div></article>
|
||||
<article class="metric"><div class="metric-label">需要处理</div><div class="metric-value">2</div><div class="metric-foot"><span class="status status-warning">认证 / 校时</span></div></article>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<article class="card"><header class="card-header"><div><h2>设备与媒体状态</h2><p class="card-subtitle">期望态与实际态的快速对照</p></div><button class="btn btn-quiet" data-view-target="devices">查看设备</button></header><div class="card-body">
|
||||
<div class="activity-list">
|
||||
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">北围墙 01</div><div class="activity-meta">主码流 1080p · MediaMTX reader 1</div></div><span class="status status-success">在线</span></div>
|
||||
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">宿舍东门 02</div><div class="activity-meta">ONVIF 认证失败 · 凭据未回显</div></div><span class="status status-danger">需处理</span></div>
|
||||
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-camera"></use></svg></div><div><div class="activity-title">食堂后门 01</div><div class="activity-meta">已发现 · 等待选择 Profile</div></div><span class="status status-warning">待激活</span></div>
|
||||
</div>
|
||||
</div></article>
|
||||
<article class="card"><header class="card-header"><div><h2>独立运行状态</h2><p class="card-subtitle">跨项目未连接不构成故障</p></div></header><div class="card-body stack">
|
||||
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-monitor"></use></svg></div><div><div class="activity-title">Sense Core</div><div class="activity-meta">API、数据库、审计可用</div></div><span class="status status-success">正常</span></div>
|
||||
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-activity"></use></svg></div><div><div class="activity-title">Brain 适配器</div><div class="activity-meta">MVP #8 不要求连接</div></div><span class="status status-neutral">未配置</span></div>
|
||||
<div class="activity-item"><div class="activity-icon"><svg aria-hidden="true"><use href="#icon-activity"></use></svg></div><div><div class="activity-title">Bell Connector</div><div class="activity-meta">后续协同工单实现</div></div><span class="status status-neutral">未安装</span></div>
|
||||
</div></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="devices" hidden>
|
||||
<div class="page-header"><div><h1>设备接入</h1><p>从发现、验证到激活的单设备闭环;凭据更新后只显示脱敏结果。</p></div><div class="page-actions"><button class="btn btn-primary" data-open-modal="add-device"><svg aria-hidden="true"><use href="#icon-plus"></use></svg>添加设备</button></div></div>
|
||||
<article class="card"><div class="card-body">
|
||||
<div class="toolbar"><div class="field grow"><label for="device-filter">搜索设备</label><div class="inline"><svg aria-hidden="true"><use href="#icon-search"></use></svg><input id="device-filter" class="input" data-filter-input="#device-table" placeholder="名称、地址或状态"></div></div><div class="field"><label for="device-status">状态</label><select id="device-status" class="input"><option>全部状态</option><option>在线</option><option>需处理</option><option>待激活</option></select></div></div>
|
||||
<div class="table-wrap"><table id="device-table"><thead><tr><th>设备</th><th>管理地址</th><th>接入状态</th><th>选定 Profile</th><th>操作</th></tr></thead><tbody>
|
||||
<tr><td><span class="table-title">北围墙 01</span><span class="table-subtitle">video · ONVIF</span></td><td>192.168.10.21</td><td><span class="status status-success">在线</span></td><td>MainStream · 1080p</td><td><button class="btn btn-quiet" data-toast="已打开北围墙 01 详情">查看</button></td></tr>
|
||||
<tr><td><span class="table-title">宿舍东门 02</span><span class="table-subtitle">video · ONVIF</span></td><td>192.168.10.32</td><td><span class="status status-danger">认证失败</span></td><td>SubStream · 720p</td><td><button class="btn btn-quiet" data-toast="凭据更新窗口仅允许写入,不会回显旧密码">更新凭据</button></td></tr>
|
||||
<tr><td><span class="table-title">食堂后门 01</span><span class="table-subtitle">video · 手工添加</span></td><td>192.168.10.45</td><td><span class="status status-warning">待激活</span></td><td>尚未选择</td><td><button class="btn btn-quiet" data-toast="已开始重新探测 Profile">探测</button></td></tr>
|
||||
</tbody></table></div>
|
||||
</div></article>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="live" hidden>
|
||||
<div class="page-header"><div><h1>实时监看</h1><p>单路视频优先;明确展示加载、失败、重连和当前 Profile。</p></div><div class="page-actions"><select class="input" aria-label="选择摄像头"><option>北围墙 01</option><option>宿舍东门 02</option></select><button class="btn" data-toast="已发起重连,当前画面保持显示">重新连接</button></div></div>
|
||||
<div class="grid grid-2"><article class="card"><div class="card-body"><div class="video-stage" role="img" aria-label="北围墙摄像头的模拟实时画面,画面中显示已配置的危险区域"><div class="camera-caption"><span class="status status-success">实时</span>北围墙 01 · MainStream</div><div class="zone-shape"></div><span class="zone-label">危险区域 A</span><span class="video-time">2026-08-12 10:18:24 CST</span></div></div></article><article class="card"><header class="card-header"><h2>播放状态</h2></header><div class="card-body stack"><div><span class="metric-label">协议</span><div class="table-title">WebRTC / MediaMTX</div></div><div><span class="metric-label">源 Profile</span><div class="table-title">MainStream · 1920×1080</div></div><div><span class="metric-label">连接质量</span><div class="table-title">1.8 Mbps · 延迟 340 ms</div></div><div><span class="metric-label">最近重连</span><div class="table-title">无 · 已稳定 04:18:32</div></div></div></article></div>
|
||||
</section>
|
||||
|
||||
<section class="view" data-view="zones" hidden>
|
||||
<div class="page-header"><div><h1>区域配置</h1><p>区域与方向线绑定设备和 Profile;分辨率变化时必须显式重新校准。</p></div><div class="page-actions"><button class="btn" data-toast="方向已反转,草稿尚未发布">反转方向</button><button class="btn btn-primary" data-toast="区域草稿已保存为 v8,尚未发布">保存草稿</button></div></div>
|
||||
<div class="grid grid-2"><article class="card"><div class="card-body"><div class="video-stage" role="img" aria-label="区域编辑画布,包含五边形危险区域"><div class="camera-caption">北围墙 01 · MainStream · 1920×1080</div><div class="zone-shape"></div><span class="zone-label">危险区域 A · 草稿</span></div></div></article><article class="card"><header class="card-header"><h2>区域属性</h2><span class="status status-warning">草稿 v8</span></header><div class="card-body stack"><div class="field"><label for="zone-name">区域名称</label><input id="zone-name" class="input" value="危险区域 A"></div><div class="field"><label for="zone-rule">区域类型</label><select id="zone-rule" class="input"><option>危险区域</option><option>方向警戒线</option><option>聚集区域</option></select></div><div class="field"><label for="zone-profile">绑定 Profile</label><input id="zone-profile" class="input" value="MainStream · 1920×1080" readonly></div><p class="help">坐标按当前 Profile 归一化保存。切换分辨率后,此版本将进入“需重新校准”。</p><button class="btn btn-primary" data-toast="区域 v8 已发布,v7 保留用于审计">发布区域版本</button></div></article></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-backdrop" id="add-device" aria-hidden="true">
|
||||
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="add-device-title">
|
||||
<header class="modal-header"><h2 id="add-device-title">添加视频设备</h2><button class="btn btn-icon btn-quiet" data-close-modal aria-label="关闭添加设备窗口">×</button></header>
|
||||
<div class="modal-body stack"><div class="field"><label for="device-name">设备名称</label><input id="device-name" class="input" value="教学楼西门 01"></div><div class="field"><label for="device-address">管理地址</label><input id="device-address" class="input" inputmode="url" value="192.168.10.88"></div><div class="field"><label for="device-user">ONVIF 用户名</label><input id="device-user" class="input" autocomplete="off" value="operator"></div><div class="field"><label for="device-password">密码</label><input id="device-password" class="input" type="password" autocomplete="new-password" placeholder="只写不读,保存后不会回显"><span class="help">原型不会保存输入内容;正式产品中密码不得进入日志或 URL。</span></div></div>
|
||||
<footer class="modal-footer"><button class="btn" data-close-modal>取消</button><button class="btn btn-primary" data-action="add-device">保存为待激活</button></footer>
|
||||
</section>
|
||||
</div>
|
||||
<div class="toast-region" data-toast-region aria-live="polite"></div>
|
||||
<script src="../assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user