diff --git a/prototypes/README.md b/prototypes/README.md new file mode 100644 index 0000000..2051fdf --- /dev/null +++ b/prototypes/README.md @@ -0,0 +1,48 @@ +# 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、实时监看和区域配置;直接映射 go-admin-ui 与 Element Plus 的通用管理端组件。 +- `brain/index.html`:合成输入到项目内匿名事件的内部流水线;Brain 无客户管理 UI。 +- `bell/index.html`:合成事件、不可变 Event、规则/Alert、ack/close 和审计时间线;直接映射 go-admin-ui 与 Element Plus 的通用管理端组件。 +- `assets/`:三套原型复用的基础样式和交互脚本;Sense/Bell 共同复用 go-admin-ui 原型外壳适配器。 +- `design-system/yovision/MASTER.md`:由 UI/UX Pro Max 生成的设计系统基线。 + +## 交互提示 + +- 左侧导航可切换页面;移动端使用顶部菜单按钮。 +- Sense 可筛选设备、添加待激活设备并查看区域配置。 +- Brain 可模拟处理下一帧并生成项目内事件。 +- Bell 可注入合成事件,并演示 Alert 的 ack 和 close。 +- 所有弹窗支持 `Esc` 关闭,交互元素支持键盘焦点。 + +## Sense / Bell 的 go-admin-ui 对齐基线 + +- 只读参考仓库:`D:\github\goadmin\go-admin-ui`。 +- 参考 commit:`67d393d713877572fab0b897296a4c1d525fc81d`。 +- 应用外壳映射:`Sidebar`、`Navbar`、`TagsView`、`AppMain` 和 `BasicLayout` / 页面容器。 +- 通用组件映射:Element Plus 的 Button、Form、Input/Select、Table、Pagination、Dialog 和 Tag,以及 go-admin-ui 的搜索区与 `v-permisaction` 权限按钮模式。 +- 共用原型适配器:`assets/go-admin-ui.css` 与 `assets/go-admin-shell.js`;两套产品不复制应用外壳和通用组件原型代码。 +- Sense 专用组件:视频舞台 `SenseVideoStage` 与区域画布 `SenseZoneCanvas`。 +- Bell 专用组件:值班处置工作台 `AlertWorkbench` 与审计时间线 `AlertAuditTimeline`。 +- 新增专用组件前必须先确认 go-admin-ui / Element Plus 中没有等价组件。 +- 原型仍是无构建依赖的 HTML/CSS/JavaScript,并未嵌入或复制 go-admin-ui / Element Plus 源码;这些名称用于约束正式 Vue 实现的组件落点。 + +## 事实来源边界 + +- 产品范围以 Gitea Wiki 的 Product Requirements、Product Roadmap 和对应工单为准。 +- 本原型不定义 API、数据库或跨项目契约。 +- 旧仓库原型仅用于只读参考;本目录没有复制旧原型代码。 +- 页面数据均为虚构演示数据,不得放入真实账号、摄像头凭据、客户数据或生产地址。 diff --git a/prototypes/assets/app.js b/prototypes/assets/app.js new file mode 100644 index 0000000..82561d0 --- /dev/null +++ b/prototypes/assets/app.js @@ -0,0 +1,203 @@ +(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 = `待探测 · video待激活尚未选择`; + 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 = `
方向越线候选
synthetic-camera-01 · 置信度 0.91 · 项目内事件
刚刚
`; + events.prepend(item); + } + event.currentTarget.disabled = false; + event.currentTarget.textContent = "处理下一帧"; + toast("已完成一帧推理并生成项目内候选事件"); + }, 520); + }); + + qs("[data-action='inject-event']")?.addEventListener("click", (event) => { + const button = event.currentTarget; + const idleText = button.textContent; + button.disabled = true; + button.textContent = "注入中…"; + window.setTimeout(() => { + const count = qs("[data-alert-count]"); + if (count) count.textContent = String(Number(count.textContent) + 1); + button.disabled = false; + button.textContent = idleText; + closeModal(button.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 el-tag"; + ackButton.disabled = true; + closeButton.disabled = false; + const timeline = qs("[data-alert-timeline]"); + timeline?.insertAdjacentHTML("beforeend", `
王值班员确认预警
刚刚 · 首个成功 ack · 已写入审计
`); + toast("预警已由王值班员确认"); + }); + closeButton?.addEventListener("click", () => { + alertState.textContent = "已关闭"; + alertState.className = "status status-success el-tag"; + closeButton.disabled = true; + qs("[data-alert-timeline]")?.insertAdjacentHTML("beforeend", `
处置完成并关闭
刚刚 · 原始 Event 保持不变
`); + 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); + if (row.tagName !== "BUTTON") { + 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); +})(); diff --git a/prototypes/assets/go-admin-shell.js b/prototypes/assets/go-admin-shell.js new file mode 100644 index 0000000..7588340 --- /dev/null +++ b/prototypes/assets/go-admin-shell.js @@ -0,0 +1,41 @@ +(function () { + "use strict"; + + const shell = document.querySelector("[data-go-admin-shell]"); + const collapse = document.querySelector("[data-collapse-sidebar]"); + + function syncTags(viewName) { + document.querySelectorAll(".tags-view-item").forEach((tag) => { + const active = tag.dataset.viewTarget === viewName; + tag.classList.toggle("active", active); + if (active) tag.setAttribute("aria-current", "page"); + else tag.removeAttribute("aria-current"); + }); + } + + document.querySelectorAll("[data-view-target]").forEach((trigger) => { + trigger.addEventListener("click", () => syncTags(trigger.dataset.viewTarget)); + }); + + if (collapse && shell) { + collapse.addEventListener("click", () => { + const compact = shell.classList.toggle("compact"); + collapse.setAttribute("aria-expanded", String(!compact)); + collapse.setAttribute("aria-label", compact ? "展开主导航" : "折叠主导航"); + }); + } + + document.querySelectorAll("[data-go-admin-query]").forEach((form) => { + const input = document.querySelector(form.dataset.queryInput || ""); + form.addEventListener("submit", (event) => { + event.preventDefault(); + input?.dispatchEvent(new Event("input", { bubbles: true })); + }); + form.addEventListener("reset", () => { + window.setTimeout(() => input?.dispatchEvent(new Event("input", { bubbles: true })), 0); + }); + }); + + const initialView = document.querySelector("[data-view-target].active")?.dataset.viewTarget; + if (initialView) syncTags(initialView); +})(); diff --git a/prototypes/assets/go-admin-ui.css b/prototypes/assets/go-admin-ui.css new file mode 100644 index 0000000..929d345 --- /dev/null +++ b/prototypes/assets/go-admin-ui.css @@ -0,0 +1,182 @@ +/* + * Shared prototype adapter for go-admin-ui commit 67d393d713877572fab0b897296a4c1d525fc81d. + * This is an offline visual/interaction mapping, not copied framework CSS and not an Element Plus runtime. + */ +.go-admin-prototype { + --ga-primary: #1677ff; + --ga-primary-hover: #409eff; + --ga-sidebar: #001529; + --ga-sidebar-hover: #000c17; + --ga-app-bg: #f0f2f5; + --ga-border: #e4e7ed; + --ga-text: #303133; + --ga-muted: #606266; + --ga-sidebar-width: 210px; + color: var(--ga-text); + background: var(--ga-app-bg); + font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Microsoft YaHei", Arial, sans-serif; +} + +.go-admin-prototype .go-admin-shell { grid-template-columns: var(--ga-sidebar-width) minmax(0, 1fr); } +.go-admin-prototype .sidebar-container { width: var(--ga-sidebar-width); padding: 0; background: var(--ga-sidebar); box-shadow: 2px 0 6px rgba(0, 21, 41, .35); transition: width .28s ease, transform .28s ease; } +.go-admin-prototype .sidebar-logo { height: 64px; min-height: 64px; padding: 0 16px; gap: 12px; color: #fff; text-decoration: none; background: #001529; } +.go-admin-prototype .brand-mark { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 5px; background: var(--ga-primary); } +.go-admin-prototype .brand-copy { min-width: 0; } +.go-admin-prototype .brand-name { white-space: nowrap; } +.go-admin-prototype .brand-subtitle { color: rgba(255,255,255,.56); white-space: nowrap; } +.go-admin-prototype .el-menu { gap: 0; padding: 8px 0; } +.go-admin-prototype .menu-group, +.go-admin-prototype .el-menu-item { min-height: 50px; height: 50px; padding: 0 18px; border-radius: 0; gap: 14px; color: rgba(255,255,255,.82); } +.go-admin-prototype .menu-group { display: flex; align-items: center; font-size: 14px; font-weight: 500; } +.go-admin-prototype .menu-group .menu-arrow { width: 14px; margin-left: auto; transform: rotate(90deg); } +.go-admin-prototype .el-menu-item:hover { color: #fff; background: var(--ga-sidebar-hover); } +.go-admin-prototype .el-menu-item.active { color: var(--ga-primary); background: var(--ga-sidebar-hover); box-shadow: inset 3px 0 0 var(--ga-primary); } +.go-admin-prototype .side-note { margin: 12px; padding: 11px 12px; border-radius: 4px; color: rgba(255,255,255,.72); } + +.go-admin-prototype .main-container { min-width: 0; background: var(--ga-app-bg); } +.go-admin-prototype .fixed-header { position: sticky; top: 0; z-index: 30; } +.go-admin-prototype .navbar { position: relative; min-height: 50px; height: 50px; padding: 0 16px 0 0; border-bottom: 1px solid #eef0f6; background: #fff; box-shadow: 0 1px 8px rgba(79,70,229,.06); backdrop-filter: none; } +.go-admin-prototype .navbar::after { content: ""; position: absolute; right: 0; bottom: 0; left: 0; height: 2px; background: linear-gradient(90deg, #1677ff, #40a9ff); opacity: .7; } +.go-admin-prototype .navbar-action, +.go-admin-prototype .avatar-button { min-width: 46px; height: 48px; padding: 0 12px; border: 0; display: inline-flex; align-items: center; justify-content: center; gap: 7px; background: transparent; color: #64748b; } +.go-admin-prototype .navbar-action:hover, +.go-admin-prototype .avatar-button:hover { color: var(--ga-primary); background: rgba(22,119,255,.06); } +.go-admin-prototype .navbar-action svg { width: 19px; height: 19px; } +.go-admin-prototype .mobile-menu { display: none; } +.go-admin-prototype .breadcrumb { display: flex; align-items: center; gap: 9px; color: #97a8be; font-size: 13px; white-space: nowrap; } +.go-admin-prototype .breadcrumb a { color: #606266; text-decoration: none; } +.go-admin-prototype .breadcrumb strong { color: #303133; font-weight: 500; } +.go-admin-prototype .avatar-button { min-width: auto; } +.go-admin-prototype .avatar { width: 32px; height: 32px; display: grid; place-items: center; border: 2px solid rgba(22,119,255,.2); border-radius: 50%; color: var(--ga-primary); } +.go-admin-prototype .prototype-pill { min-height: 25px; border-radius: 4px; } + +.go-admin-prototype .tags-view-container { height: 40px; padding: 8px 8px 0; display: flex; align-items: flex-end; gap: 3px; overflow-x: auto; border-bottom: 1px solid #e8eaec; background: #fff; box-shadow: 0 1px 4px rgba(0,21,41,.08); } +.go-admin-prototype .tags-view-item { height: 32px; padding: 0 10px; border: 1px solid #e0e0e6; border-radius: 3px 3px 0 0; flex: 0 0 auto; background: #f8f9fa; color: #606266; font-size: 12px; } +.go-admin-prototype .tags-view-item:hover { color: var(--ga-primary); background: #e6f4ff; } +.go-admin-prototype .tags-view-item.active { color: var(--ga-primary); border-color: #91caff; border-bottom-color: #fff; background: #fff; font-weight: 500; } +.go-admin-prototype .tags-view-item span { margin-left: 4px; color: #909399; } + +.go-admin-prototype .app-main { width: 100%; min-height: calc(100vh - 90px); padding: 16px; background: var(--ga-app-bg); } +.go-admin-prototype .compact-header { margin-bottom: 14px; } +.go-admin-prototype .compact-header h1 { font-size: 20px; font-weight: 500; letter-spacing: 0; } +.go-admin-prototype .compact-header p { font-size: 13px; } +.go-admin-prototype .el-card { border-color: var(--ga-border); border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,.03); } +.go-admin-prototype .card-header { min-height: 48px; padding: 10px 14px; } +.go-admin-prototype .card-body { padding: 14px; } +.go-admin-prototype .metric { padding: 14px; } + +.go-admin-prototype .el-button { min-height: 32px; height: 32px; padding: 5px 12px; border-color: #dcdfe6; border-radius: 4px; background: #fff; color: #606266; font-size: 12px; font-weight: 400; } +.go-admin-prototype .el-button svg { width: 15px; height: 15px; } +.go-admin-prototype .el-button:hover { color: var(--ga-primary); border-color: #a0cfff; background: #ecf5ff; } +.go-admin-prototype .el-button-primary { color: #fff; border-color: var(--ga-primary); background: var(--ga-primary); } +.go-admin-prototype .el-button-primary:hover { color: #fff; border-color: var(--ga-primary-hover); background: var(--ga-primary-hover); } +.go-admin-prototype .el-button-link { min-height: 30px; height: 30px; padding: 3px 4px; border: 0; color: var(--ga-primary); background: transparent; } +.go-admin-prototype .el-button:disabled { color: #a8abb2; border-color: #e4e7ed; background: #f5f7fa; } + +.go-admin-prototype .el-form { color: var(--ga-text); } +.go-admin-prototype .el-form-inline { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 18px; padding-bottom: 14px; border-bottom: 1px solid #ebeef5; } +.go-admin-prototype .form-item { min-width: 180px; display: grid; grid-template-columns: auto minmax(150px, 220px); align-items: center; gap: 8px; } +.go-admin-prototype .form-item label { color: #606266; font-size: 13px; white-space: nowrap; } +.go-admin-prototype .form-actions { display: flex; gap: 8px; } +.go-admin-prototype .el-input, +.go-admin-prototype .el-select { min-height: 32px; height: 32px; padding: 4px 10px; border-color: #dcdfe6; border-radius: 4px; color: #606266; font-size: 13px; } +.go-admin-prototype .el-input:focus, +.go-admin-prototype .el-select:focus { border-color: var(--ga-primary); outline: 2px solid rgba(22,119,255,.16); } +.go-admin-prototype .vertical-form { display: grid; gap: 16px; } +.go-admin-prototype .vertical-form .form-item { grid-template-columns: 92px minmax(0,1fr); } +.go-admin-prototype .vertical-form .help { margin: -8px 0 0 100px; } +.go-admin-prototype .form-footer { display: flex; justify-content: flex-end; } + +.go-admin-prototype .operation-bar { min-height: 48px; display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.go-admin-prototype .operation-actions { display: flex; flex-wrap: wrap; gap: 8px; } +.go-admin-prototype .permission-hint { display: inline-flex; align-items: center; gap: 5px; color: #909399; font-size: 12px; } +.go-admin-prototype .permission-hint svg { width: 14px; height: 14px; } +.go-admin-prototype .el-table { min-width: 760px; border: 1px solid #ebeef5; } +.go-admin-prototype .el-table th, +.go-admin-prototype .el-table td { padding: 9px 10px; border-right: 1px solid #ebeef5; border-bottom-color: #ebeef5; } +.go-admin-prototype .el-table th { background: #f5f7fa; color: #909399; font-weight: 500; } +.go-admin-prototype .el-table tbody tr:nth-child(even) { background: #fafafa; } +.go-admin-prototype .el-table tbody tr:hover { background: #f5f7fa; } +.go-admin-prototype .checkbox-faux { width: 14px; height: 14px; display: inline-block; border: 1px solid #dcdfe6; border-radius: 2px; background: #fff; } +.go-admin-prototype .action-divider { height: 14px; margin: 0 3px; display: inline-block; border-left: 1px solid #dcdfe6; vertical-align: middle; } +.go-admin-prototype .el-tag { min-height: 24px; padding: 2px 8px; border: 1px solid currentColor; border-radius: 4px; font-weight: 400; } +.go-admin-prototype .el-tag::before { display: none; } + +.go-admin-prototype .pagination-container { min-height: 48px; display: flex; align-items: center; justify-content: flex-end; gap: 6px; color: #606266; font-size: 12px; } +.go-admin-prototype .pagination-container button { min-width: 30px; height: 28px; border: 0; border-radius: 3px; background: #f4f4f5; color: #606266; } +.go-admin-prototype .pagination-container button.active { color: #fff; background: var(--ga-primary); } +.go-admin-prototype .pagination-container button:disabled { color: #c0c4cc; cursor: not-allowed; } +.go-admin-prototype .pagination-size { height: 28px; border: 1px solid #dcdfe6; border-radius: 4px; color: #606266; } +.go-admin-prototype .pagination-total { margin-right: 4px; } + +.go-admin-prototype .description-list { display: grid; } +.go-admin-prototype .description-row { min-height: 43px; padding: 8px 0; display: grid; grid-template-columns: minmax(90px,.7fr) minmax(0,1.5fr) auto; align-items: center; gap: 10px; border-bottom: 1px solid #ebeef5; } +.go-admin-prototype .description-row:last-child { border-bottom: 0; } +.go-admin-prototype .description-row > span:first-child { color: #909399; font-size: 12px; } +.go-admin-prototype .description-row strong { font-size: 13px; font-weight: 500; } + +.go-admin-prototype .sense-video-stage, +.go-admin-prototype .sense-zone-canvas { border-radius: 4px; } +.go-admin-prototype .el-overlay { background: rgba(0,0,0,.5); } +.go-admin-prototype .el-dialog { width: min(600px,100%); border-radius: 4px; } +.go-admin-prototype .el-dialog-header, +.go-admin-prototype .el-dialog-footer { padding: 16px 20px; } +.go-admin-prototype .el-dialog-body { padding: 20px; } +.go-admin-prototype .dialog-close { min-height: 32px; height: 32px; color: #909399; } + +.go-admin-prototype .go-admin-shell.compact { grid-template-columns: 54px minmax(0,1fr); } +.go-admin-prototype .go-admin-shell.compact .sidebar-container { width: 54px; } +.go-admin-prototype .go-admin-shell.compact .brand-copy, +.go-admin-prototype .go-admin-shell.compact .el-menu-item span, +.go-admin-prototype .go-admin-shell.compact .menu-group span, +.go-admin-prototype .go-admin-shell.compact .menu-arrow, +.go-admin-prototype .go-admin-shell.compact .side-note { display: none; } +.go-admin-prototype .go-admin-shell.compact .sidebar-logo, +.go-admin-prototype .go-admin-shell.compact .menu-group, +.go-admin-prototype .go-admin-shell.compact .el-menu-item { padding-right: 0; padding-left: 0; justify-content: center; } + +@media (max-width: 900px) { + .go-admin-prototype .go-admin-shell, + .go-admin-prototype .go-admin-shell.compact { display: block; } + .go-admin-prototype .sidebar-container, + .go-admin-prototype .go-admin-shell.compact .sidebar-container { position: fixed; top: 0; left: 0; z-index: 40; width: min(300px,88vw); height: 100vh; transform: translateX(-102%); } + .go-admin-prototype .sidebar-container.open, + .go-admin-prototype .go-admin-shell.compact .sidebar-container.open { transform: translateX(0); } + .go-admin-prototype .go-admin-shell.compact .brand-copy, + .go-admin-prototype .go-admin-shell.compact .el-menu-item span, + .go-admin-prototype .go-admin-shell.compact .menu-group span, + .go-admin-prototype .go-admin-shell.compact .menu-arrow, + .go-admin-prototype .go-admin-shell.compact .side-note { display: initial; } + .go-admin-prototype .go-admin-shell.compact .sidebar-logo, + .go-admin-prototype .go-admin-shell.compact .menu-group, + .go-admin-prototype .go-admin-shell.compact .el-menu-item { padding: 0 18px; justify-content: flex-start; } + .go-admin-prototype .desktop-collapse { display: none; } + .go-admin-prototype .mobile-menu { display: inline-flex; } + .go-admin-prototype .breadcrumb { max-width: 46vw; overflow: hidden; } + .go-admin-prototype .breadcrumb a, + .go-admin-prototype .breadcrumb span { display: none; } + .go-admin-prototype .app-main { padding: 12px; } + .go-admin-prototype .tags-view-container { height: 48px; padding-top: 8px; } + .go-admin-prototype .tags-view-item { height: 40px; } + .go-admin-prototype .el-button { min-height: 44px; height: 44px; } + .go-admin-prototype .el-input, + .go-admin-prototype .el-select { min-height: 44px; height: 44px; font-size: 16px; } + .go-admin-prototype .el-form-inline { align-items: stretch; } + .go-admin-prototype .el-form-inline .form-item { width: 100%; grid-template-columns: 82px minmax(0,1fr); } + .go-admin-prototype .el-form-inline .form-actions { margin-left: 90px; } + .go-admin-prototype .operation-bar { align-items: flex-start; flex-direction: column; padding: 9px 0; } + .go-admin-prototype .pagination-container button, + .go-admin-prototype .pagination-size { min-width: 40px; height: 40px; } +} + +@media (max-width: 430px) { + .go-admin-prototype .desktop-only, + .go-admin-prototype .prototype-pill { display: none; } + .go-admin-prototype .topbar-actions { gap: 0; } + .go-admin-prototype .navbar-action, + .go-admin-prototype .avatar-button { min-width: 42px; padding: 0 8px; } + .go-admin-prototype .vertical-form .form-item { grid-template-columns: 1fr; gap: 5px; } + .go-admin-prototype .vertical-form .help { margin-left: 0; } + .go-admin-prototype .pagination-container { justify-content: flex-start; overflow-x: auto; } +} + diff --git a/prototypes/assets/styles.css b/prototypes/assets/styles.css new file mode 100644 index 0000000..d4ebb71 --- /dev/null +++ b/prototypes/assets/styles.css @@ -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; } +} diff --git a/prototypes/bell/bell.css b/prototypes/bell/bell.css new file mode 100644 index 0000000..dfcad35 --- /dev/null +++ b/prototypes/bell/bell.css @@ -0,0 +1,54 @@ +.bell-go-admin .sidebar { background: linear-gradient(180deg, #7c2d12 0%, #9a3412 44%, #431407 100%); } +.bell-go-admin .brand-mark { background: #ffedd5; color: #9a3412; } +.bell-go-admin .el-menu-item.active { background: rgba(255, 255, 255, 0.18); } +.bell-go-admin .nav-count { margin-left: auto; min-width: 20px; padding: 1px 6px; border-radius: 10px; background: rgba(255,255,255,.2); font-size: 12px; text-align: center; } +.bell-go-admin .compact-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.bell-workbench { grid-template-columns: minmax(290px, .8fr) minmax(440px, 1.2fr); align-items: start; } +.alert-workbench .card-body { padding: 0; } +.alert-list { display: grid; } +.alert-row { display: grid; grid-template-columns: 4px minmax(0, 1fr) auto; gap: 14px; width: 100%; min-height: 82px; padding: 15px 18px; border: 0; border-bottom: 1px solid var(--border); background: #fff; color: var(--text); font: inherit; text-align: left; cursor: pointer; } +.alert-row:hover { background: #fff7ed; } +.alert-row:focus-visible { outline: 2px solid #c2410c; outline-offset: -3px; } +.alert-row.selected { background: #fff7ed; box-shadow: inset 3px 0 #c2410c; } +.severity-bar { width: 4px; height: 48px; border-radius: 4px; background: var(--danger); } +.severity-bar.medium { background: var(--warning); } +.alert-title { display: block; margin-bottom: 7px; } +.alert-meta { display: flex; flex-wrap: wrap; gap: 6px 12px; color: var(--text-muted); font-size: 13px; } +.activity-time { color: var(--text-muted); font-size: 13px; } +.situation-summary { display: grid; gap: 12px; } +.situation-summary > div { display: grid; grid-template-columns: 92px 1fr; gap: 14px; } +.situation-summary span { color: var(--text-muted); } +.evidence-preview { display: flex; align-items: flex-end; justify-content: space-between; min-height: 170px; margin: 18px 0; padding: 18px; border-radius: 8px; background: linear-gradient(140deg, #172033, #506078); color: #fff; } +.evidence-preview span { color: #e2e8f0; } +.quick-actions { display: flex; flex-wrap: wrap; gap: 10px; } +.primary-task { margin-top: 18px; padding: 15px; border-left: 4px solid #c2410c; border-radius: 4px; background: #fff7ed; } +.primary-task p { margin: 0 0 12px; line-height: 1.6; } +.technical-details { margin-top: 18px; border: 1px solid var(--border); border-radius: 6px; background: #f8fafc; } +.technical-details summary { padding: 11px 14px; cursor: pointer; color: #475569; font-weight: 600; } +.technical-details summary:focus-visible { outline: 2px solid #c2410c; outline-offset: 2px; } +.technical-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; padding: 0 14px 14px; } +.technical-grid > div { display: grid; gap: 4px; } +.technical-grid span { color: var(--text-muted); font-size: 13px; } +.technical-grid strong { overflow-wrap: anywhere; } +.working-list { display: grid; gap: 10px; } +.working-item { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 15px; border: 1px solid var(--border); border-radius: 8px; } +.working-item p { margin: 5px 0 0; color: var(--text-muted); } +.result-options { display: grid; gap: 10px; margin: 0; padding: 0; border: 0; } +.result-options legend { margin-bottom: 4px; font-weight: 700; } +.result-card { display: flex; gap: 12px; min-height: 64px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; } +.result-card:has(input:checked) { border-color: #c2410c; background: #fff7ed; box-shadow: 0 0 0 1px #c2410c; } +.result-card input { margin-top: 4px; } +.result-card span { display: grid; gap: 3px; } +.result-card small { color: var(--text-muted); } +.field-error { color: #b91c1c; font-size: 13px; } +.dialog-subtitle { margin: 4px 0 0; color: var(--text-muted); font-size: 13px; } +.admin-records { margin-bottom: 14px; } +@media (max-width: 1100px) { .bell-workbench { grid-template-columns: 1fr; } } +@media (max-width: 767px) { + .bell-go-admin .compact-metrics { grid-template-columns: 1fr; } + .situation-summary > div { grid-template-columns: 1fr; gap: 3px; } + .technical-grid { grid-template-columns: 1fr; } + .working-item { align-items: stretch; flex-direction: column; } + .working-item .btn { width: 100%; } + .evidence-preview { align-items: flex-start; flex-direction: column; } +} diff --git a/prototypes/bell/bell.js b/prototypes/bell/bell.js new file mode 100644 index 0000000..131256e --- /dev/null +++ b/prototypes/bell/bell.js @@ -0,0 +1,76 @@ +(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]"); + const 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); + }; + const number = (selector) => Number(qs(selector)?.textContent || 0); + + qsa("[data-select-alert]").forEach((row) => row.addEventListener("click", () => { + qs("[data-detail-title]").textContent = row.dataset.selectAlert; + qs("[data-detail-what]").textContent = row.dataset.selectAlert; + qs("[data-detail-location]").textContent = row.dataset.location; + qs("[data-detail-meta]").textContent = `${row.dataset.location} · ${row.dataset.time}`; + qs("[data-detail-suggestion]").textContent = row.dataset.suggestion; + const state = qs("[data-alert-state]"); + state.textContent = row.dataset.risk; + state.className = `status ${row.dataset.risk === "高风险" ? "status-danger" : "status-warning"} el-tag`; + })); + + let started = false; + const startButtons = qsa("[data-action='start-alert'], [data-action='table-start-alert']"); + const startAlert = () => { + if (started) { + toast("该预警已由王值班员开始处理"); + return; + } + started = true; + startButtons.forEach((button) => { + button.disabled = true; + button.textContent = "已确认"; + }); + const dashboardState = qs("[data-alert-state]"); + dashboardState.textContent = "处理中"; + dashboardState.className = "status status-warning el-tag"; + const tableState = qs("[data-table-alert-state]"); + tableState.textContent = "处理中"; + tableState.className = "status status-warning el-tag"; + qs("[data-table-owner]").textContent = "王值班员"; + qs("[data-finish-from-table]").disabled = false; + qs("[data-pending-count]").textContent = String(Math.max(0, number("[data-pending-count]") - 1)); + qs("[data-working-count]").textContent = String(number("[data-working-count]") + 1); + toast("预警已确认并由王值班员开始处理"); + }; + startButtons.forEach((button) => button.addEventListener("click", startAlert)); + + qsa("input[name='result']").forEach((input) => input.addEventListener("change", () => { + qs("[data-result-error]").hidden = true; + })); + + qs("[data-action='finish-alert']")?.addEventListener("click", () => { + const selected = qs("input[name='result']:checked"); + const error = qs("[data-result-error]"); + if (!selected) { + error.hidden = false; + qs("input[name='result']")?.focus(); + return; + } + const tableState = qs("[data-table-alert-state]"); + tableState.textContent = "已关闭"; + tableState.className = "status status-success el-tag"; + qs("[data-finish-from-table]").disabled = true; + qs("[data-completed-count]").textContent = String(number("[data-completed-count]") + 1); + qs("[data-working-count]").textContent = String(Math.max(0, number("[data-working-count]") - 1)); + qs("[data-close-modal]", qs("#finish-alert"))?.click(); + toast(`预警已关闭,处理结果:${selected.value}`); + }); +})(); diff --git a/prototypes/bell/index.html b/prototypes/bell/index.html new file mode 100644 index 0000000..77726a1 --- /dev/null +++ b/prototypes/bell/index.html @@ -0,0 +1,83 @@ + + + + + + + YoVision Bell · 预警管理后台 + + + + + + + + + +
+ + +
+
+ + +
+ +
+
+ +
待确认预警
3
1 条高风险
处理中
2
均已明确处理人
今日已关闭
18
平均用时约 4 分钟
今日事件
29
已生成 23 条预警
+
+

最新预警

按风险和时间排序

+

危险区域有人进入

北围墙 01 · 今天 10:24

高风险
预警内容危险区域有人进入
发生地点北围墙 01
处理建议先查看现场;如无法确认,请联系校内保安巡查。
技术详情
预警编号BEL-A-20260812-0031
关联事件2 条不可变事件
规则版本校园高危区域 · v4
状态映射Alert new → ack → close
+
+
+ + + + + + +
+
+
+ + +
+ + + + + diff --git a/prototypes/brain/index.html b/prototypes/brain/index.html new file mode 100644 index 0000000..b8b0d3e --- /dev/null +++ b/prototypes/brain/index.html @@ -0,0 +1,56 @@ + + + + + + + YoVision Brain · 当前 MVP 内部流程原型 + + + + + +
+ +
+
synthetic-lab · CPU 降级模式

推理流水线

内部流程原型返回原型入口
+
+
+ +
已处理帧
12,842
24.8 FPS· 单路
端到端延迟
82 ms
实验室值· 非容量承诺
当前目标
3
匿名 track ID
候选事件
7
项目内
+

执行链

每层均可替换并独立测试

运行中
+
1

输入适配

synthetic-campus.mp4
1920×1080 · 25 FPS

+
2

视频解码

software decode
队列 2 帧

+
3

匿名检测

person · 3 boxes
模型 demo-v1

+
4

单路跟踪

3 active tracks
无身份信息

+
5

区域判定

危险区域 + 方向线
规则 v3

+
6

项目内事件

本地 JSON
未绑定 Bell 契约

+
+

模拟画面

不包含真实未成年人影像

最近候选事实

正式事件结构等待协同工单

危险区域进入候选
track-0042 · 置信度 0.88 · rule-v3
10:20:16
方向越线候选
track-0038 · east-to-west · rule-v3
10:19:42
+
+ + + + +
+
+
+
+ + + diff --git a/prototypes/design-system/yovision/MASTER.md b/prototypes/design-system/yovision/MASTER.md new file mode 100644 index 0000000..98e3052 --- /dev/null +++ b/prototypes/design-system/yovision/MASTER.md @@ -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 diff --git a/prototypes/index.html b/prototypes/index.html new file mode 100644 index 0000000..6134780 --- /dev/null +++ b/prototypes/index.html @@ -0,0 +1,58 @@ + + + + + + + YoVision · 当前 MVP 交互原型 + + + + + +
+
+ YV + YoVisionSchool Safety Suite +
+ MVP #8 · 交互原型 +
+
+
+ 三项目 · 两个独立产品 +

先独立跑通,
再可靠协作。

+

这组原型对应当前 YoVision MVP:Sense 负责设备与视频感知管理,Brain 负责无界面匿名推理,Bell 负责独立预警处置。页面中的数据均为演示数据。

+
+ +
+ + +

Sense

+

智能 NVR 与视频感知管理面

+
  • 设备与 ONVIF/RTSP
  • MediaMTX 与实时监看
  • 区域和方向警戒线
+ 打开 Sense 原型 +
+ + +

Brain

+

无客户界面的匿名推理交付单元

+
  • 合成/本地输入
  • 解码、检测与跟踪
  • 区域/越线项目内事件
+ 打开内部流程原型 +
+ + +

Bell

+

独立事件预警与处置产品

+
  • 合成事件与不可变 Event
  • 规则匹配与 Alert
  • 并发 ack、close 与审计
+ 打开 Bell 原型 +
+
+ +
+ + diff --git a/prototypes/sense/index.html b/prototypes/sense/index.html new file mode 100644 index 0000000..284334d --- /dev/null +++ b/prototypes/sense/index.html @@ -0,0 +1,147 @@ + + + + + + + YoVision Sense · 摄像头管理 + + + + + + + + + +
+ + +
+
+ + +
+ +
+
+ +
+
摄像头
6
5 台正常· 1 台待处理
+
实时画面
5
全部可查看
+
已启用警戒区域
7
另有 1 个未发布草稿
+
需要处理
1
账号验证失败
+
+
+

需要你处理

每个问题都提供原因和下一步操作

1 项
+
宿舍东门 02 暂时无法连接

原因:摄像头拒绝了当前账号。影响:该位置暂时没有实时画面。

+
+

系统运行情况

日常使用只需关注是否正常

正常
+
摄像头服务运行正常正常
+
配置保存已同步,12 秒前检查正常
+
查看高级信息
媒体服务MediaMTX · 5/5 paths converged
智能分析连接Brain adapter · 未配置(不影响独立运行)
预警平台连接Bell connector · 未安装(不影响独立运行)
+
+
+
+ + + + + + +
+
+
+ + + + + +
+ + + + + diff --git a/prototypes/sense/sense.css b/prototypes/sense/sense.css new file mode 100644 index 0000000..047cb0d --- /dev/null +++ b/prototypes/sense/sense.css @@ -0,0 +1,51 @@ +.sense-go-admin .attention-card { border-top: 3px solid var(--danger); } +.sense-go-admin .recovery-item { display: flex; align-items: center; justify-content: space-between; gap: 20px; } +.sense-go-admin .recovery-item p, +.sense-go-admin .wizard-panel > p, +.sense-go-admin .recovery-summary p { margin: 6px 0 0; color: var(--text-muted); line-height: 1.6; } +.advanced-panel { margin-top: 14px; border: 1px solid var(--border); border-radius: 6px; background: #f8fafc; } +.advanced-panel summary { padding: 11px 14px; cursor: pointer; color: #475569; font-weight: 600; } +.advanced-panel summary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } +.advanced-content { display: grid; gap: 10px; padding: 0 14px 14px; } +.advanced-content > div { display: flex; justify-content: space-between; gap: 16px; color: var(--text-muted); } +.advanced-content strong { color: var(--text); text-align: right; overflow-wrap: anywhere; } +.table-advanced { margin-bottom: 12px; } +.advanced-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.advanced-grid > div { display: grid; } +.advanced-grid strong { text-align: left; } +.wizard-dialog { width: min(720px, calc(100vw - 32px)); } +.dialog-subtitle { margin: 4px 0 0; color: var(--text-muted); font-size: 13px; } +.wizard-progress { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px; padding: 0; margin: 0 0 24px; list-style: none; counter-reset: wizard; } +.wizard-progress li { position: relative; padding-top: 22px; color: var(--text-muted); font-size: 12px; text-align: center; } +.wizard-progress li::before { counter-increment: wizard; content: counter(wizard); position: absolute; top: 0; left: 50%; width: 18px; height: 18px; transform: translateX(-50%); border: 1px solid var(--border); border-radius: 50%; background: #fff; line-height: 18px; } +.wizard-progress li::after { content: ""; position: absolute; top: 9px; left: calc(50% + 11px); width: calc(100% - 22px); height: 1px; background: var(--border); } +.wizard-progress li:last-child::after { display: none; } +.wizard-progress li.active { color: var(--primary); font-weight: 700; } +.wizard-progress li.active::before { border-color: var(--primary); background: var(--primary); color: #fff; } +.wizard-panel h3 { margin: 0 0 4px; font-size: 18px; } +.choice-card { display: flex; align-items: flex-start; gap: 12px; min-height: 66px; margin-top: 12px; padding: 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; } +.choice-card:has(input:checked) { border-color: var(--primary); background: #eff6ff; box-shadow: 0 0 0 1px var(--primary); } +.choice-card input { margin-top: 4px; } +.choice-card span { display: grid; gap: 4px; } +.choice-card small { color: var(--text-muted); } +.test-status { display: grid; gap: 4px; margin: 18px 0; padding: 14px; border-left: 4px solid #94a3b8; border-radius: 4px; background: #f8fafc; } +.test-status.testing { border-color: var(--primary); background: #eff6ff; } +.test-status.success { border-color: var(--success); background: #f0fdf4; } +.test-status.error { border-color: var(--danger); background: #fef2f2; } +.test-status span { color: var(--text-muted); } +.inline-actions { display: flex; flex-wrap: wrap; gap: 10px; } +.wizard-preview { min-height: 260px; background: linear-gradient(140deg, #27364a, #526984); } +.completion-state { padding: 28px 12px; text-align: center; } +.completion-mark { display: inline-grid; width: 54px; height: 54px; place-items: center; border-radius: 50%; background: #16a34a; color: #fff; font-size: 30px; } +.completion-state h3 { margin-top: 16px; } +.recovery-summary { padding: 14px; border-left: 4px solid var(--danger); border-radius: 4px; background: #fef2f2; } +.field-error { margin-top: 6px; color: #b91c1c; font-size: 13px; } +@media (max-width: 767px) { + .sense-go-admin .recovery-item { align-items: stretch; flex-direction: column; } + .advanced-content > div { display: grid; } + .advanced-content strong { text-align: left; } + .advanced-grid { grid-template-columns: 1fr; } + .wizard-progress li { font-size: 0; } + .wizard-progress li::before { font-size: 12px; } + .wizard-dialog .modal-body { padding-inline: 16px; } +} diff --git a/prototypes/sense/sense.js b/prototypes/sense/sense.js new file mode 100644 index 0000000..435da19 --- /dev/null +++ b/prototypes/sense/sense.js @@ -0,0 +1,122 @@ +(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]"); + const announce = (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); + }; + + qsa(".choice-card input").forEach((input) => { + input.addEventListener("change", () => { + qsa(`input[name="${input.name}"]`).forEach((peer) => peer.closest(".choice-card")?.classList.toggle("selected", peer.checked)); + if (input.name === "discover-method") { + const manual = input.value === "manual"; + qs("[data-auto-choice]").hidden = manual; + qs("[data-manual-choice]").hidden = !manual; + } + }); + }); + + const wizard = qs("#add-camera"); + if (wizard) { + let step = 0; + let connectionPassed = false; + const panels = qsa("[data-wizard-panel]", wizard); + const progress = qsa(".wizard-progress li", wizard); + const back = qs("[data-wizard-back]", wizard); + const next = qs("[data-wizard-next]", wizard); + const stepLabel = qs("[data-wizard-step-label]", wizard); + const connectionStatus = qs("[data-connection-status]", wizard); + + const render = () => { + panels.forEach((panel, index) => { + panel.hidden = index !== step; + panel.classList.toggle("active", index === step); + }); + progress.forEach((item, index) => item.classList.toggle("active", index <= step)); + stepLabel.textContent = `第 ${step + 1} 步,共 ${panels.length} 步`; + back.disabled = step === 0; + next.textContent = step === panels.length - 1 ? "完成" : "下一步"; + next.disabled = step === 3 && !connectionPassed; + qs("input, button, h3[tabindex]", panels[step])?.focus({ preventScroll: true }); + }; + + qsa("[data-open-modal='add-camera']").forEach((trigger) => trigger.addEventListener("click", () => { + step = trigger.textContent.includes("继续") ? 2 : 0; + connectionPassed = false; + connectionStatus.className = "test-status"; + connectionStatus.innerHTML = "尚未测试点击“测试连接”继续。"; + render(); + })); + back.addEventListener("click", () => { if (step > 0) { step -= 1; render(); } }); + next.addEventListener("click", () => { + if (step < panels.length - 1) { step += 1; render(); return; } + qs("[data-close-modal]", wizard)?.click(); + announce("教学楼西门 01 已添加,可立即查看实时画面"); + }); + qs("[data-test-connection]", wizard)?.addEventListener("click", (event) => { + const button = event.currentTarget; + button.disabled = true; + button.textContent = "正在测试…"; + connectionStatus.className = "test-status testing"; + connectionStatus.innerHTML = "正在检查账号和画面通常几秒内完成。"; + window.setTimeout(() => { + connectionPassed = true; + connectionStatus.className = "test-status success"; + connectionStatus.innerHTML = "连接成功画面正常,已自动选择高清(推荐)。"; + button.disabled = false; + button.textContent = "重新测试"; + render(); + }, 520); + }); + qs("[data-simulate-failure]", wizard)?.addEventListener("click", () => { + connectionPassed = false; + connectionStatus.className = "test-status error"; + connectionStatus.innerHTML = "账号验证失败请返回上一步检查用户名和密码,然后重新测试。"; + next.disabled = true; + announce("连接失败:请检查账号密码后重试"); + }); + } + + const recovery = qs("#recover-camera"); + if (recovery) { + const password = qs("#recover-password", recovery); + const error = qs("[data-recovery-error]", recovery); + const status = qs("[data-recovery-status]", recovery); + const save = qs("[data-recovery-save]", recovery); + qs("[data-recovery-test]", recovery)?.addEventListener("click", (event) => { + const testButton = event.currentTarget; + if (!password.value.trim()) { + error.hidden = false; + password.setAttribute("aria-invalid", "true"); + password.focus(); + return; + } + error.hidden = true; + password.removeAttribute("aria-invalid"); + testButton.disabled = true; + testButton.textContent = "正在测试…"; + status.className = "test-status testing"; + status.innerHTML = "正在重新连接正在检查账号和实时画面。"; + window.setTimeout(() => { + status.className = "test-status success"; + status.innerHTML = "连接成功实时画面已恢复,可以安全保存。"; + testButton.disabled = false; + testButton.textContent = "重新测试"; + save.disabled = false; + }, 520); + }); + save.addEventListener("click", () => { + qs("[data-close-modal]", recovery)?.click(); + announce("宿舍东门 02 的账号已更新,实时画面恢复正常"); + }); + } +})();