From 533225d712fd9556b1f7ea8304c50cc643923d06 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 10:22:39 +0800 Subject: [PATCH 1/6] feat: add current MVP interactive prototypes (#28) --- prototypes/README.md | 36 +++ prototypes/assets/app.js | 199 ++++++++++++++++ prototypes/assets/styles.css | 244 ++++++++++++++++++++ prototypes/bell/index.html | 62 +++++ prototypes/brain/index.html | 56 +++++ prototypes/design-system/yovision/MASTER.md | 227 ++++++++++++++++++ prototypes/index.html | 58 +++++ prototypes/sense/index.html | 106 +++++++++ 8 files changed, 988 insertions(+) create mode 100644 prototypes/README.md create mode 100644 prototypes/assets/app.js create mode 100644 prototypes/assets/styles.css create mode 100644 prototypes/bell/index.html create mode 100644 prototypes/brain/index.html create mode 100644 prototypes/design-system/yovision/MASTER.md create mode 100644 prototypes/index.html create mode 100644 prototypes/sense/index.html diff --git a/prototypes/README.md b/prototypes/README.md new file mode 100644 index 0000000..d2e9223 --- /dev/null +++ b/prototypes/README.md @@ -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、数据库或跨项目契约。 +- 旧仓库原型仅用于只读参考;本目录没有复制旧原型代码。 +- 页面数据均为虚构演示数据,不得放入真实账号、摄像头凭据、客户数据或生产地址。 diff --git a/prototypes/assets/app.js b/prototypes/assets/app.js new file mode 100644 index 0000000..edcb6b3 --- /dev/null +++ b/prototypes/assets/app.js @@ -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 = `待探测 · 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) => { + 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", `
王值班员确认预警
刚刚 · 首个成功 ack · 已写入审计
`); + toast("预警已由王值班员确认"); + }); + closeButton?.addEventListener("click", () => { + alertState.textContent = "已关闭"; + alertState.className = "status status-success"; + 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); + 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/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/index.html b/prototypes/bell/index.html new file mode 100644 index 0000000..3d33de0 --- /dev/null +++ b/prototypes/bell/index.html @@ -0,0 +1,62 @@ + + + + + + + YoVision Bell · 当前 MVP 原型 + + + + + +
+ +
+
青藤学校 · Bell 值班账号

实时值班台

交互原型 · 非生产数据返回原型入口
+
+
+ +
待确认 Alert
3
1 高危
处理中
2
已明确处置人
今日关闭
18
中位 3m 42s
重复收据
6
均未重复建 Alert
+
+
+
危险区域闯入
北围墙 012 个关联 Event未确认
10:24
+
反向越过警戒线
宿舍东门 021 个 Event处理中
10:19
+
危险区域持续停留
食堂后门 013 个关联 Event未确认
10:11
+
+

危险区域闯入

Alert BEL-A-20260812-0031

未确认
+
+
规则
校园高危区域 · v4
处置建议
确认现场情况;如无法确认,联系校内保安巡查。
+ +

审计时间线

规则命中并创建 Alert
10:23:56 · system · 规则 v4
值班台已显示
10:23:57 · sent/seen 与 ack 分离
+
+
+ + + + +
+
+
+ + +
+ + + 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..2e3035c --- /dev/null +++ b/prototypes/sense/index.html @@ -0,0 +1,106 @@ + + + + + + + YoVision Sense · 当前 MVP 原型 + + + + + +
+ + +
+
+
+ +
青藤实验站点 · Sense

运行总览

+
+
交互原型 · 非生产数据返回原型入口
+
+ +
+
+ +
+
视频设备
6
5 在线· 1 待激活
+
MediaMTX 路径
5/5
已收敛· 12 秒前
+
区域版本
8
7 生效· 1 草稿
+
需要处理
2
认证 / 校时
+
+
+

设备与媒体状态

期望态与实际态的快速对照

+
+
北围墙 01
主码流 1080p · MediaMTX reader 1
在线
+
宿舍东门 02
ONVIF 认证失败 · 凭据未回显
需处理
+
食堂后门 01
已发现 · 等待选择 Profile
待激活
+
+
+

独立运行状态

跨项目未连接不构成故障

+
Sense Core
API、数据库、审计可用
正常
+
Brain 适配器
MVP #8 不要求连接
未配置
+
Bell Connector
后续协同工单实现
未安装
+
+
+
+ + + + + + +
+
+
+ + +
+ + + -- 2.34.1 From 1da77fe0b1d2794bb58c198148a72a174c5f7adb Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 12 Aug 2026 10:41:28 +0800 Subject: [PATCH 2/6] feat: align Sense prototype with go-admin-ui (#30) --- prototypes/README.md | 11 +- prototypes/sense/go-admin-ui.css | 181 +++++++++++++++++++++++++++++++ prototypes/sense/index.html | 138 ++++++++++++++--------- prototypes/sense/sense.js | 43 ++++++++ 4 files changed, 319 insertions(+), 54 deletions(-) create mode 100644 prototypes/sense/go-admin-ui.css create mode 100644 prototypes/sense/sense.js diff --git a/prototypes/README.md b/prototypes/README.md index d2e9223..7f5604c 100644 --- a/prototypes/README.md +++ b/prototypes/README.md @@ -14,7 +14,7 @@ python -m http.server 4173 --directory prototypes ## 原型范围 -- `sense/index.html`:设备接入、ONVIF/RTSP、MediaMTX、实时监看和区域配置。 +- `sense/index.html`:设备接入、ONVIF/RTSP、MediaMTX、实时监看和区域配置;直接映射 go-admin-ui 与 Element Plus 的通用管理端组件。 - `brain/index.html`:合成输入到项目内匿名事件的内部流水线;Brain 无客户管理 UI。 - `bell/index.html`:合成事件、不可变 Event、规则/Alert、ack/close 和审计时间线。 - `assets/`:三套原型复用的样式和交互脚本。 @@ -28,6 +28,15 @@ python -m http.server 4173 --directory prototypes - Bell 可注入合成事件,并演示 Alert 的 ack 和 close。 - 所有弹窗支持 `Esc` 关闭,交互元素支持键盘焦点。 +## Sense 的 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` 权限按钮模式。 +- Sense 专用组件:视频舞台 `SenseVideoStage` 与区域画布 `SenseZoneCanvas`;正式实现前先确认 go-admin-ui 内没有等价组件。 +- 原型仍是无构建依赖的 HTML/CSS/JavaScript,并未嵌入或复制 go-admin-ui / Element Plus 源码;这些名称用于约束正式 Vue 实现的组件落点。 + ## 事实来源边界 - 产品范围以 Gitea Wiki 的 Product Requirements、Product Roadmap 和对应工单为准。 diff --git a/prototypes/sense/go-admin-ui.css b/prototypes/sense/go-admin-ui.css new file mode 100644 index 0000000..aba4ec2 --- /dev/null +++ b/prototypes/sense/go-admin-ui.css @@ -0,0 +1,181 @@ +/* + * Sense 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. + */ +.sense-go-admin { + --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; +} + +.sense-go-admin .go-admin-shell { grid-template-columns: var(--ga-sidebar-width) minmax(0, 1fr); } +.sense-go-admin .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; } +.sense-go-admin .sidebar-logo { height: 64px; min-height: 64px; padding: 0 16px; gap: 12px; color: #fff; text-decoration: none; background: #001529; } +.sense-go-admin .brand-mark { width: 34px; height: 34px; flex: 0 0 34px; border-radius: 5px; background: var(--ga-primary); } +.sense-go-admin .brand-copy { min-width: 0; } +.sense-go-admin .brand-name { white-space: nowrap; } +.sense-go-admin .brand-subtitle { color: rgba(255,255,255,.56); white-space: nowrap; } +.sense-go-admin .el-menu { gap: 0; padding: 8px 0; } +.sense-go-admin .menu-group, +.sense-go-admin .el-menu-item { min-height: 50px; height: 50px; padding: 0 18px; border-radius: 0; gap: 14px; color: rgba(255,255,255,.82); } +.sense-go-admin .menu-group { display: flex; align-items: center; font-size: 14px; font-weight: 500; } +.sense-go-admin .menu-group .menu-arrow { width: 14px; margin-left: auto; transform: rotate(90deg); } +.sense-go-admin .el-menu-item:hover { color: #fff; background: var(--ga-sidebar-hover); } +.sense-go-admin .el-menu-item.active { color: var(--ga-primary); background: var(--ga-sidebar-hover); box-shadow: inset 3px 0 0 var(--ga-primary); } +.sense-go-admin .side-note { margin: 12px; padding: 11px 12px; border-radius: 4px; color: rgba(255,255,255,.72); } + +.sense-go-admin .main-container { min-width: 0; background: var(--ga-app-bg); } +.sense-go-admin .fixed-header { position: sticky; top: 0; z-index: 30; } +.sense-go-admin .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; } +.sense-go-admin .navbar::after { content: ""; position: absolute; right: 0; bottom: 0; left: 0; height: 2px; background: linear-gradient(90deg, #1677ff, #40a9ff); opacity: .7; } +.sense-go-admin .navbar-action, +.sense-go-admin .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; } +.sense-go-admin .navbar-action:hover, +.sense-go-admin .avatar-button:hover { color: var(--ga-primary); background: rgba(22,119,255,.06); } +.sense-go-admin .navbar-action svg { width: 19px; height: 19px; } +.sense-go-admin .mobile-menu { display: none; } +.sense-go-admin .breadcrumb { display: flex; align-items: center; gap: 9px; color: #97a8be; font-size: 13px; white-space: nowrap; } +.sense-go-admin .breadcrumb a { color: #606266; text-decoration: none; } +.sense-go-admin .breadcrumb strong { color: #303133; font-weight: 500; } +.sense-go-admin .avatar-button { min-width: auto; } +.sense-go-admin .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); } +.sense-go-admin .prototype-pill { min-height: 25px; border-radius: 4px; } + +.sense-go-admin .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); } +.sense-go-admin .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; } +.sense-go-admin .tags-view-item:hover { color: var(--ga-primary); background: #e6f4ff; } +.sense-go-admin .tags-view-item.active { color: var(--ga-primary); border-color: #91caff; border-bottom-color: #fff; background: #fff; font-weight: 500; } +.sense-go-admin .tags-view-item span { margin-left: 4px; color: #909399; } + +.sense-go-admin .app-main { width: 100%; min-height: calc(100vh - 90px); padding: 16px; background: var(--ga-app-bg); } +.sense-go-admin .compact-header { margin-bottom: 14px; } +.sense-go-admin .compact-header h1 { font-size: 20px; font-weight: 500; letter-spacing: 0; } +.sense-go-admin .compact-header p { font-size: 13px; } +.sense-go-admin .el-card { border-color: var(--ga-border); border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,.03); } +.sense-go-admin .card-header { min-height: 48px; padding: 10px 14px; } +.sense-go-admin .card-body { padding: 14px; } +.sense-go-admin .metric { padding: 14px; } + +.sense-go-admin .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; } +.sense-go-admin .el-button svg { width: 15px; height: 15px; } +.sense-go-admin .el-button:hover { color: var(--ga-primary); border-color: #a0cfff; background: #ecf5ff; } +.sense-go-admin .el-button-primary { color: #fff; border-color: var(--ga-primary); background: var(--ga-primary); } +.sense-go-admin .el-button-primary:hover { color: #fff; border-color: var(--ga-primary-hover); background: var(--ga-primary-hover); } +.sense-go-admin .el-button-link { min-height: 30px; height: 30px; padding: 3px 4px; border: 0; color: var(--ga-primary); background: transparent; } +.sense-go-admin .el-button:disabled { color: #a8abb2; border-color: #e4e7ed; background: #f5f7fa; } + +.sense-go-admin .el-form { color: var(--ga-text); } +.sense-go-admin .el-form-inline { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 18px; padding-bottom: 14px; border-bottom: 1px solid #ebeef5; } +.sense-go-admin .form-item { min-width: 180px; display: grid; grid-template-columns: auto minmax(150px, 220px); align-items: center; gap: 8px; } +.sense-go-admin .form-item label { color: #606266; font-size: 13px; white-space: nowrap; } +.sense-go-admin .form-actions { display: flex; gap: 8px; } +.sense-go-admin .el-input, +.sense-go-admin .el-select { min-height: 32px; height: 32px; padding: 4px 10px; border-color: #dcdfe6; border-radius: 4px; color: #606266; font-size: 13px; } +.sense-go-admin .el-input:focus, +.sense-go-admin .el-select:focus { border-color: var(--ga-primary); outline: 2px solid rgba(22,119,255,.16); } +.sense-go-admin .vertical-form { display: grid; gap: 16px; } +.sense-go-admin .vertical-form .form-item { grid-template-columns: 92px minmax(0,1fr); } +.sense-go-admin .vertical-form .help { margin: -8px 0 0 100px; } +.sense-go-admin .form-footer { display: flex; justify-content: flex-end; } + +.sense-go-admin .operation-bar { min-height: 48px; display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.sense-go-admin .operation-actions { display: flex; flex-wrap: wrap; gap: 8px; } +.sense-go-admin .permission-hint { display: inline-flex; align-items: center; gap: 5px; color: #909399; font-size: 12px; } +.sense-go-admin .permission-hint svg { width: 14px; height: 14px; } +.sense-go-admin .el-table { min-width: 760px; border: 1px solid #ebeef5; } +.sense-go-admin .el-table th, +.sense-go-admin .el-table td { padding: 9px 10px; border-right: 1px solid #ebeef5; border-bottom-color: #ebeef5; } +.sense-go-admin .el-table th { background: #f5f7fa; color: #909399; font-weight: 500; } +.sense-go-admin .el-table tbody tr:nth-child(even) { background: #fafafa; } +.sense-go-admin .el-table tbody tr:hover { background: #f5f7fa; } +.sense-go-admin .checkbox-faux { width: 14px; height: 14px; display: inline-block; border: 1px solid #dcdfe6; border-radius: 2px; background: #fff; } +.sense-go-admin .action-divider { height: 14px; margin: 0 3px; display: inline-block; border-left: 1px solid #dcdfe6; vertical-align: middle; } +.sense-go-admin .el-tag { min-height: 24px; padding: 2px 8px; border: 1px solid currentColor; border-radius: 4px; font-weight: 400; } +.sense-go-admin .el-tag::before { display: none; } + +.sense-go-admin .pagination-container { min-height: 48px; display: flex; align-items: center; justify-content: flex-end; gap: 6px; color: #606266; font-size: 12px; } +.sense-go-admin .pagination-container button { min-width: 30px; height: 28px; border: 0; border-radius: 3px; background: #f4f4f5; color: #606266; } +.sense-go-admin .pagination-container button.active { color: #fff; background: var(--ga-primary); } +.sense-go-admin .pagination-container button:disabled { color: #c0c4cc; cursor: not-allowed; } +.sense-go-admin .pagination-size { height: 28px; border: 1px solid #dcdfe6; border-radius: 4px; color: #606266; } +.sense-go-admin .pagination-total { margin-right: 4px; } + +.sense-go-admin .description-list { display: grid; } +.sense-go-admin .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; } +.sense-go-admin .description-row:last-child { border-bottom: 0; } +.sense-go-admin .description-row > span:first-child { color: #909399; font-size: 12px; } +.sense-go-admin .description-row strong { font-size: 13px; font-weight: 500; } + +.sense-go-admin .sense-video-stage, +.sense-go-admin .sense-zone-canvas { border-radius: 4px; } +.sense-go-admin .el-overlay { background: rgba(0,0,0,.5); } +.sense-go-admin .el-dialog { width: min(600px,100%); border-radius: 4px; } +.sense-go-admin .el-dialog-header, +.sense-go-admin .el-dialog-footer { padding: 16px 20px; } +.sense-go-admin .el-dialog-body { padding: 20px; } +.sense-go-admin .dialog-close { min-height: 32px; height: 32px; color: #909399; } + +.sense-go-admin .go-admin-shell.compact { grid-template-columns: 54px minmax(0,1fr); } +.sense-go-admin .go-admin-shell.compact .sidebar-container { width: 54px; } +.sense-go-admin .go-admin-shell.compact .brand-copy, +.sense-go-admin .go-admin-shell.compact .el-menu-item span, +.sense-go-admin .go-admin-shell.compact .menu-group span, +.sense-go-admin .go-admin-shell.compact .menu-arrow, +.sense-go-admin .go-admin-shell.compact .side-note { display: none; } +.sense-go-admin .go-admin-shell.compact .sidebar-logo, +.sense-go-admin .go-admin-shell.compact .menu-group, +.sense-go-admin .go-admin-shell.compact .el-menu-item { padding-right: 0; padding-left: 0; justify-content: center; } + +@media (max-width: 900px) { + .sense-go-admin .go-admin-shell, + .sense-go-admin .go-admin-shell.compact { display: block; } + .sense-go-admin .sidebar-container, + .sense-go-admin .go-admin-shell.compact .sidebar-container { position: fixed; top: 0; left: 0; z-index: 40; width: min(300px,88vw); height: 100vh; transform: translateX(-102%); } + .sense-go-admin .sidebar-container.open, + .sense-go-admin .go-admin-shell.compact .sidebar-container.open { transform: translateX(0); } + .sense-go-admin .go-admin-shell.compact .brand-copy, + .sense-go-admin .go-admin-shell.compact .el-menu-item span, + .sense-go-admin .go-admin-shell.compact .menu-group span, + .sense-go-admin .go-admin-shell.compact .menu-arrow, + .sense-go-admin .go-admin-shell.compact .side-note { display: initial; } + .sense-go-admin .go-admin-shell.compact .sidebar-logo, + .sense-go-admin .go-admin-shell.compact .menu-group, + .sense-go-admin .go-admin-shell.compact .el-menu-item { padding: 0 18px; justify-content: flex-start; } + .sense-go-admin .desktop-collapse { display: none; } + .sense-go-admin .mobile-menu { display: inline-flex; } + .sense-go-admin .breadcrumb { max-width: 46vw; overflow: hidden; } + .sense-go-admin .breadcrumb a, + .sense-go-admin .breadcrumb span { display: none; } + .sense-go-admin .app-main { padding: 12px; } + .sense-go-admin .tags-view-container { height: 48px; padding-top: 8px; } + .sense-go-admin .tags-view-item { height: 40px; } + .sense-go-admin .el-button { min-height: 44px; height: 44px; } + .sense-go-admin .el-input, + .sense-go-admin .el-select { min-height: 44px; height: 44px; font-size: 16px; } + .sense-go-admin .el-form-inline { align-items: stretch; } + .sense-go-admin .el-form-inline .form-item { width: 100%; grid-template-columns: 82px minmax(0,1fr); } + .sense-go-admin .el-form-inline .form-actions { margin-left: 90px; } + .sense-go-admin .operation-bar { align-items: flex-start; flex-direction: column; padding: 9px 0; } + .sense-go-admin .pagination-container button, + .sense-go-admin .pagination-size { min-width: 40px; height: 40px; } +} + +@media (max-width: 430px) { + .sense-go-admin .desktop-only, + .sense-go-admin .prototype-pill { display: none; } + .sense-go-admin .topbar-actions { gap: 0; } + .sense-go-admin .navbar-action, + .sense-go-admin .avatar-button { min-width: 42px; padding: 0 8px; } + .sense-go-admin .vertical-form .form-item { grid-template-columns: 1fr; gap: 5px; } + .sense-go-admin .vertical-form .help { margin-left: 0; } + .sense-go-admin .pagination-container { justify-content: flex-start; overflow-x: auto; } +} diff --git a/prototypes/sense/index.html b/prototypes/sense/index.html index 2e3035c..c45a039 100644 --- a/prototypes/sense/index.html +++ b/prototypes/sense/index.html @@ -3,11 +3,13 @@ - - YoVision Sense · 当前 MVP 原型 + + YoVision Sense · go-admin-ui 对齐原型 + + - + + -
-