From a3f4544ddf90e006e8234fcd1299990ab34e6dd3 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 4 Oct 2026 08:59:15 +0800 Subject: [PATCH] feat: introduce Chinese courtyard visual theme v0.1.22 --- README.md | 6 +- VERSION | 2 +- app.py | 2 +- test_app.py | 2 +- test_theme.js | 61 +++++++++++ web/chinese-theme.css | 245 ++++++++++++++++++++++++++++++++++++++++++ web/index.html | 30 +++--- web/recording-ui.js | 4 +- 8 files changed, 331 insertions(+), 21 deletions(-) create mode 100644 test_theme.js create mode 100644 web/chinese-theme.css diff --git a/README.md b/README.md index 79881fd..adcc445 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ 运行在自有小电脑上的视频管理程序。Python 标准库负责对象、空间、配置与登录;MediaMTX 负责 RTSP 接入、HLS 实时预览、本机录像及回放;浏览器使用本地打包的 hls.js。 +## 界面风格 + +v0.1.22 采用「庭院书卷」视觉:宣纸白底、黛青导航、朱砂点缀与淡金本人标记。统一实时监控、家谱树、家族记事、设备档案、表单和登录界面;全屏监控保留深色背景。样式集中在 `web/chinese-theme.css`,本地系统宋体用于标题,不请求外部字体。桌面和手机使用同一套功能与自适应布局。 + ## 对象与空间 - **空间档案**:家庭 / 公司 / 其他,所属家庭或单位,省、市、区县、街道、小区或园区、楼栋、单元、楼层、门牌和详细地址。 @@ -49,7 +53,7 @@ python3 app.py mediamtx ./data/mediamtx.yml ``` -回归测试:`python3 -m unittest -v test_app test_ptz test_camera_settings test_ptz_presets` 和 `node --test test_live_player.js test_kinship.js test_calendar.js test_family_graph.js test_ui_logic.js test_recording_tools.js`。测试使用临时数据库和模拟播放器,不连接真实设备。 +回归测试:`python3 -m unittest -v test_app test_ptz test_camera_settings test_ptz_presets` 和 `node --test test_live_player.js test_kinship.js test_calendar.js test_family_graph.js test_ui_logic.js test_recording_tools.js test_theme.js`。测试使用临时数据库和模拟播放器,不连接真实设备。 访问 `http://127.0.0.1:8790/`。后端配置变量:`VISION_BIND`、`VISION_PORT`、`VISION_DATA`、`VISION_RECORDINGS`;高清转码可用 `VISION_VAAPI_DEVICE` 指定渲染设备(默认 `/dev/dri/renderD128`)。HTTPS 反向代理场景可设 `VISION_SECURE_COOKIE=1`。当前管理 API 使用管理员与个人账号分权。 diff --git a/VERSION b/VERSION index 7906299..7e72641 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.21 +0.1.22 diff --git a/app.py b/app.py index 08ffd9b..91089ad 100644 --- a/app.py +++ b/app.py @@ -875,7 +875,7 @@ class Handler(BaseHTTPRequestHandler): files = {'/': 'index.html', '/app.js': 'app.js', '/live-player.js': 'live-player.js', '/style.css': 'style.css', '/family.js': 'family.js', '/kinship.js': 'kinship.js', '/family-graph.js': 'family-graph.js', '/family-map.js': 'family-map.js', - '/family-map.css': 'family-map.css', + '/family-map.css': 'family-map.css', '/chinese-theme.css': 'chinese-theme.css', '/family-events.js': 'family-events.js', '/family-history.js': 'family-history.js', '/camera-settings.js': 'camera-settings.js', diff --git a/test_app.py b/test_app.py index bf02601..e214ae2 100644 --- a/test_app.py +++ b/test_app.py @@ -105,7 +105,7 @@ class WebTests(unittest.TestCase): self.assertEqual(headers['Cache-Control'], 'no-store') self.assertNotIn(b'__ASSET_VERSION__', body) assets = re.findall(r'(?:src|href)="(/[^"?]+\.(?:js|css)\?v=[^"]+)"', body.decode()) - self.assertEqual(len(assets), 18) + self.assertEqual(len(assets), 19) version = (app.ROOT / 'VERSION').read_text().strip() for asset in assets: self.assertTrue(asset.endswith('?v=' + version)) diff --git a/test_theme.js b/test_theme.js new file mode 100644 index 0000000..0697dde --- /dev/null +++ b/test_theme.js @@ -0,0 +1,61 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const css = fs.readFileSync(path.join(__dirname, 'web/chinese-theme.css'), 'utf8'); +const html = fs.readFileSync(path.join(__dirname, 'web/index.html'), 'utf8'); +const root = css.match(/:root\s*\{([^}]+)\}/)[1]; +const token = name => root.match(new RegExp('--' + name + ':\\s*(#[0-9a-f]{6})'))[1]; +function luminance(hex) { + const values = hex.slice(1).match(/../g).map(v => parseInt(v, 16) / 255) + .map(v => v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4); + return values[0] * .2126 + values[1] * .7152 + values[2] * .0722; +} +function contrast(a, b) { + const x = luminance(a), y = luminance(b); + return (Math.max(x, y) + .05) / (Math.min(x, y) + .05); +} +test('paper theme is loaded last, versioned and uses only local fonts', () => { + const styles = [...html.matchAll(/ x[1]); + assert.equal(styles.at(-1), '/chinese-theme.css?v=__ASSET_VERSION__'); + assert.doesNotMatch(css, /@import|https?:|@font-face/i); + assert.match(html, /name="theme-color" content="#213d34"/); + assert.match(html, /class="courtyard-landscape"[^>]+aria-hidden="true"/); +}); +test('primary text, muted text and actions have at least 4.5:1 contrast', () => { + for (const [fg, bg] of [ + [token('ink'), token('paper')], [token('muted'), token('paper')], + [token('muted'), token('panel')], ['#fffdf6', token('accent')], + ['#c9d1be', '#213d34'], ['#244638', '#efebdc'], + ['#355b3d', '#e4eddf'], ['#835428', '#f2e8d9'], + ['#994437', '#fbf8ef'], ['#d9dfca', '#24372b'], + ]) assert.ok(contrast(fg, bg) >= 4.5, `${fg} on ${bg}: ${contrast(fg, bg).toFixed(2)}`); +}); +test('monitor fullscreen, reduced motion and mobile navigation are explicitly styled', () => { + assert.match(css, /#page-live:fullscreen\s*\{[^}]+color-scheme: dark/); + assert.match(css, /@media \(prefers-reduced-motion: reduce\)/); + assert.match(css, /overflow-x: auto/); + assert.match(css, /\.family-node\.self\s*\{[^}]+background: #f1ddb0/); + assert.match(css, /--timeline-paper:/); +}); +test('recording timeline paints with theme tokens and keeps range geometry', () => { + const vm = require('node:vm'); + const source = fs.readFileSync(path.join(__dirname, 'web/recording-ui.js'), 'utf8'); + const draw = source.slice(source.indexOf('function drawTimeline()'), source.indexOf('function playbackParams(')); + const rectangles = []; + const ctx = { scale() {}, beginPath() {}, moveTo() {}, lineTo() {}, stroke() {}, + fillRect(...coordinates) { rectangles.push({ color: this.fillStyle, coordinates }); } }; + const canvas = { clientWidth: 480, getContext: () => ctx }; + const values = { '--timeline-paper': '#ede8dd', '--timeline-grid': '#d3ccbc', '--timeline-range': '#4c7761' }; + const scope = vm.createContext({ $: () => canvas, window: { devicePixelRatio: 2 }, document: { documentElement: {} }, + getComputedStyle: () => ({ getPropertyValue: name => values[name] }), + queryStart: new Date(0), queryEnd: new Date(60000), spans: [{ start: 10000, end: 20000 }] }); + vm.runInContext(draw + ';drawTimeline()', scope); + assert.equal(canvas.width, 960); + assert.equal(canvas.height, 152); + assert.deepEqual(rectangles, [ + { color: '#ede8dd', coordinates: [0, 0, 480, 76] }, + { color: '#4c7761', coordinates: [80, 25, 80, 25] }, + ]); + assert.equal(ctx.strokeStyle, '#d3ccbc'); +}); diff --git a/web/chinese-theme.css b/web/chinese-theme.css new file mode 100644 index 0000000..3f3d631 --- /dev/null +++ b/web/chinese-theme.css @@ -0,0 +1,245 @@ +/* 赵府智家 · 庭院书卷。Keep visual tokens separate from layout and playback. */ +:root { + color-scheme: light; + --paper: #f4f0e7; + --ink: #2d3932; + --muted: #6e6b5f; + --line: #dcd5c6; + --panel: #fffdf8; + --accent: #315b4d; + --cinnabar: #9e4b3d; + --gold: #86682f; + --serif: "Songti SC", "STSong", "SimSun", serif; + --timeline-paper: #ede8dd; + --timeline-grid: #d3ccbc; + --timeline-range: #4c7761; + color: var(--ink); + background: var(--paper); + font-family: "Microsoft YaHei", "PingFang SC", system-ui, sans-serif; +} +body { + background-image: radial-gradient(ellipse at 85% 0, #e4e9df80, transparent 50%); + min-width: 0; +} +::selection { background: #e8d7ad; color: #2d3932; } +button, .button, input, select, textarea, fieldset { border-radius: 5px; } +button, .button { background: var(--accent); color: #fffdf6; font-weight: 500; transition: background .15s, border-color .15s, box-shadow .15s; } +button:hover, .button:hover { filter: brightness(.95); } +.secondary { background: #e8ede5; color: #315b4d; border-color: #c7d3c4; } +.quiet { background: transparent; color: #586653; border-color: #cfcfbf; } +input, select, textarea { color: var(--ink); background: #fffefa; border-color: #cbc5b6; } +input::placeholder, textarea::placeholder { color: #847f72; } +input:focus, select:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px #315b4d12; } +input[type=range], input[type=checkbox] { accent-color: var(--accent); } +label, .family-date legend { color: #625f53; } +h1, h2, h3 { font-family: var(--serif); font-weight: 600; } +h1 { font-size: 34px; letter-spacing: 4px; margin-top: 17px; } +h2 { letter-spacing: 1px; } +.sidebar { + width: 222px; + background: #213d34; + background-image: linear-gradient(145deg, #2d4b3d, transparent 60%); + border-right: 1px solid #425344; + padding: 30px 18px 20px; + color: #ebe7d8; + scrollbar-width: thin; + scrollbar-color: #6f8271 #213d34; +} +.sidebar .brand { gap: 11px; padding: 4px 5px 30px; color: #f3eddf; font-family: var(--serif); font-size: 22px; letter-spacing: 2px; } +.brand-icon { width: 44px; height: 44px; border-radius: 5px; outline: 1px solid #c6b98c66; outline-offset: 3px; } +.brand small { font-family: "Microsoft YaHei", sans-serif; color: #c0c7b5; font-size: 9px; letter-spacing: 2px; margin-top: 10px; } +.nav-label { color: #b2bba6; font-size: 10px; padding: 12px 13px 17px; letter-spacing: 3px; border-top: 1px solid #b9c2aa24; } +.sidebar nav { gap: 5px; } +.sidebar nav button { color: #c9d1be; border-radius: 4px; padding: 12px 13px; gap: 13px; position: relative; border: 1px solid transparent; background: transparent; } +.sidebar nav button span { font-size: 13px; letter-spacing: .6px; } +.sidebar nav button:hover { background: #ffffff09; filter: none; } +.sidebar nav button.active { background: #efebdc; border-color: #e0d6b8; color: #244638; } +.sidebar nav button.active::after { content: ""; width: 5px; height: 5px; background: var(--cinnabar); position: absolute; right: 13px; transform: rotate(45deg); } +.sidebar .side-note { padding: 22px 5px 8px; color: #c4ccb9; font-size: 11px; } +.sidebar .dot { background: #afc39c; width: 5px; height: 5px; } +.sidebar #current-user { color: #d3d7c8; padding: 10px 5px 0; border-top: 1px solid #b9c2aa24; } +.sidebar .quiet { color: #d1d6c6; border-color: #8b9a813f; padding: 9px; font-size: 11px; } +.sidebar #account-password { margin-bottom: 8px; } +.sidebar :focus-visible { outline-color: #e7c887; } +main { margin-left: 222px; padding: 30px 38px 22px; max-width: 1900px; } +main > header { min-height: 142px; padding: 13px 0 23px; margin-bottom: 25px; border-bottom: 1px solid #b4b5a2; position: relative; gap: 24px; isolation: isolate; } +main > header::after { content: ""; position: absolute; bottom: -2px; left: 0; height: 3px; width: 48px; background: var(--cinnabar); } +main > header > div { position: relative; z-index: 1; } +.eyebrow { color: var(--gold); font-size: 10px; letter-spacing: 3px; } +header p { color: #686f60; font-size: 12px; letter-spacing: .5px; } +.header-status { color: #666e5e; gap: 12px; font-size: 11px; flex-shrink: 0; } +.courtyard-landscape { position: absolute; width: 330px; height: 125px; right: 5px; bottom: 0; z-index: 0; pointer-events: none; opacity: .55; } +.badge { background: #e4eddf; color: #355b3d; border: 1px solid #c8d6be; border-radius: 3px; font-size: 10px; } +.badge.off { background: #f2e8d9; color: #835428; border-color: #dfc6a3; } +.metrics { background: #fcfaf4; border-radius: 6px; box-shadow: 0 3px 12px #3a402905; } +.metrics > div { padding: 20px 22px; align-items: baseline; } +.metrics span { color: #6f7260; } +.metrics strong { font-family: Georgia, var(--serif); color: #365846; font-size: 29px; } +.filter-row { color: #6e6b5f; margin: 24px 0; } +.section-head { margin-top: 25px; } +.section-head small { color: #8a7859; font-size: 9px; letter-spacing: 2px; } +.wall-toolbar { padding: 12px 15px; background: #eae8dc; border: 1px solid var(--line); border-radius: 5px; gap: 14px; } +.wall-toolbar select { background: #f9f6ed; } +.live-grid { gap: 22px; } +.camera-card, .site-card, .panel, .empty, .family-canvas-panel { border-radius: 6px; box-shadow: 0 4px 18px #39422a05; } +.camera-card { border-color: #d2cebe; } +.camera-meta { padding: 17px 18px 13px; } +.camera-meta strong { font-family: var(--serif); font-size: 16px; letter-spacing: .5px; } +.camera-meta p { color: #737464; margin-top: 7px; } +.camera-actions { flex-wrap: wrap; gap: 7px; padding-bottom: 16px; } +.camera-actions .quiet { border-color: #dbd9cb; color: #52604d; background: #faf9f2; } +.video-panel { border-radius: 5px; border-color: #c1c6b8; } +.video-empty { color: #b0bdae; background: radial-gradient(ellipse at center, #243d34 0, transparent 70%); } +.video-empty .lens { color: #a5bca4; } +.video-error { color: #f6d7c1; background: #472c26ed; } +.empty { color: #6c7462; border-color: #c3c6b6; } +.empty h3 { color: #384e3b; font-size: 21px; } +.panel { padding: 24px; } +.time-labels { color: #6e705e; } +.recording-item { color: #35533f; background: #f9f7ee; border-color: #d5d1c0; } +.recording-item small { color: #737261; } +.scan-chip { background: #fffdf7; border-radius: 5px; } +.device-table { border-radius: 5px; } +.device-table th { color: #6a6d5c; background: #ecebde; } +.device-table td small { color: #737261; } +.device-table tbody tr:hover { background: #f2f1e7; } +.site-card h3 { font-size: 20px; } +.site-card dt { color: #737261; } +.site-card dd { color: #384e3b; } +.audit-item { color: #56654f; } +.audit-item small { color: #767461; } +.banner { color: #784c26; background: #f6e9cf; border-color: #dfc797; } +dialog { background: #fbf8ef; color: var(--ink); border: 1px solid #d5c8ac; border-radius: 8px; box-shadow: 0 28px 100px #172c3455; padding: 30px; } +dialog::backdrop { background: #20382cb8; backdrop-filter: blur(5px); } +.dialog-heading { border-bottom: 1px solid var(--line); padding-bottom: 20px; gap: 16px; } +.dialog-heading h2 { font-size: 23px; } +.dialog-footer { gap: 10px; flex-wrap: wrap; } +#auth-dialog { border-top: 4px solid var(--cinnabar); padding: 38px; background-image: radial-gradient(ellipse at 100% 0, #e4e9dd, transparent 65%); } +#auth-dialog .brand-icon { width: 52px; height: 52px; } +#auth-form h2 { font-size: 32px; letter-spacing: 3px; margin-top: 26px; } +#auth-form p { color: #6b725d; } +.form-error { color: #994437; } +footer { color: #716b5b; font-size: 10px; letter-spacing: .5px; } +#toast { background: #2a4c3c; color: #f3efdc; border-color: #819672; border-radius: 5px; box-shadow: 0 8px 25px #30433726; max-width: calc(100vw - 30px); } +.ptz-panel { background: #f8f5eb; border-color: #b9c5ad; border-radius: 7px; box-shadow: 0 12px 50px #26392d44; color: var(--ink); } +.ptz-pad [data-ptz-action=stop] { color: #8b4236; background: #efdbd0; border-color: #d5b2a4; } +#preset-confirmation { background: #f4e2d5; } +#snapshot-image { border-radius: 4px; } +/* Family register: paper, ink lines and a restrained gold highlight for oneself. */ +.family-canvas-panel { background: #fcfaf3; border-color: #c7cbb7; } +.family-map-heading { padding-top: 24px; } +.family-map-heading h2 { letter-spacing: 3px; font-size: 25px; } +.family-view-tabs { background: #f1eee2; border-color: #d7d0bf; border-radius: 4px; } +.family-view-tabs button { color: #606650; border-radius: 2px; } +.family-view-tabs [aria-pressed=true] { background: #365945; color: #fffbed; } +.family-map-viewport { background-color: #efece0; background-image: radial-gradient(#c9cbbb 1px, transparent 1px); scrollbar-color: #8b9a7b #e8e6d8; } +.family-map-lines marker path { fill: #678b71; } +.family-edge { stroke: #94a58a; } +.family-edge.focused { stroke: #446b50; } +.family-edge.spouse { stroke: #a9854f; } +.family-edge.nonbiological { stroke: #93829e; } +.family-node { background: #fffdf6; border-color: #c9cdb9; border-radius: 5px; color: #304632; box-shadow: 0 3px 12px #3943250d; } +.family-node > strong { font-family: var(--serif); font-size: 17px; } +.family-node-avatar { background: #e8ecdf; color: #536e45; border-radius: 4px; } +.family-node .family-node-relation { color: #53664a; } +.family-node > small, .family-node-flags > small { color: #6f775f; } +.family-node.self { background: #f1ddb0; border-color: #af873f; box-shadow: 0 5px 18px #b4872425; } +.family-node.self .family-node-avatar { background: #f8eac9; } +.family-node.selected { outline-color: #426c50; } +.family-node.search-hit::after { background: #596b78; } +.self-badge { background: #94682f; color: #fff8e9; border-radius: 2px; } +.family-map-footer { border-color: #d0d3c0; } +.family-map-legend { color: #69705b; } +.family-map-footer #family-map-note:not(:empty) { color: #87652f; } +.family-inspector.is-self { background: linear-gradient(135deg, #f3e3bd, #fcfaf1 65%); border-color: #c3a66b; } +.family-self-heading { color: #88652d; letter-spacing: 1px; } +.family-register { background: #eeecdf; } +.person-card { color: #344732; border-color: #c9cbb9; } +.person-card.selected { background: #e6ecdc; outline-color: #66845b; } +.person-card.self { background: #f1ddb0; border-color: #b89553; color: #5f461f; } +.person-card.self span:not(.self-badge), .person-card.self small { color: #765b2e; } +.family-tree ul { border-color: #a2b197; } +.family-workspace:is(:fullscreen, .family-expanded) { background: var(--paper); } +.family-event-filters { background: #f0eee2; } +.timeline-year { color: #456344; font-family: var(--serif); } +.timeline-event::before { border-color: #b7c2a5; } +.timeline-event::after { background: #768d5f; box-shadow: 0 0 0 4px #e7ebdc; } +.timeline-event > time { color: #6c745d; } +.timeline-event-body { background: #fffdf6; border-color: #d3d6c2; border-radius: 5px; } +.timeline-event:hover .timeline-event-body { background: #f7f7ec; border-color: #8caa78; } +.timeline-event-body > strong { color: #344d32; font-family: var(--serif); font-size: 18px; } +.timeline-event-status, .timeline-summary, .timeline-event-body > small { color: #6b725b; } +.timeline-read { color: #41623b; } +.timeline-event.draft .timeline-event-body { background: #efede3; } +.timeline-event.draft::after { background: #a28a62; } +.event-detail-meta, .person-event-link time { color: #71745e; } +.event-lead { background: #eaecdd; border-color: #8e9f77; color: #46553b; } +.event-article { color: #424c3a; } +.person-event-link { color: #455a3c; } +.history-columns dl > div { border-color: #d9d6c4; } +.history-changed { background: #e8ecda; border-radius: 4px; } +#history-confirm { border-color: #b3be98; border-radius: 5px; background: #efefdf; } +/* The monitoring wall stays dark to keep attention on live video. */ +#page-live:fullscreen { --muted: #c0c9b4; --line: #52614d; --panel: #25352c; color: #efecdd; background: #111c16; color-scheme: dark; } +#page-live:fullscreen .wall-toolbar { background: #24372b; border-color: #40543e; } +#page-live:fullscreen label { color: #d9deca; } +#page-live:fullscreen select { background: #1a2a20; color: #ebeede; border-color: #65765a; } +#page-live:fullscreen .quiet { color: #d9dfca; border-color: #66765a; background: transparent; } +#page-live:fullscreen .camera-card { border-color: #394f38; } +#page-live:fullscreen .camera-actions { background: #17251ee8; } +#page-live:fullscreen .ptz-panel { --muted: #6e6b5f; --line: #dcd5c6; color-scheme: light; } +#page-live:fullscreen .ptz-panel label, #page-live:fullscreen .ptz-panel .quiet { color: #586653; border-color: #cfcfbf; } +@media (min-width: 1600px) { main { padding-left: 46px; padding-right: 46px; } } +@media (max-width: 1100px) { + .sidebar { width: 200px; padding: 27px 14px 18px; } + .sidebar .brand { font-size: 20px; gap: 9px; } + .brand-icon { width: 38px; height: 38px; } + main { margin-left: 200px; padding: 24px; } + .metrics > div { padding: 18px 14px; } + .metrics strong { font-size: 25px; } + .courtyard-landscape { width: 260px; opacity: .4; } +} +@media (max-width: 760px) { + .sidebar { position: static; width: auto; padding: 22px 17px 14px; display: grid; grid-template-columns: 1fr auto; gap: 0 10px; overflow: visible; } + .sidebar .brand { padding: 2px 4px 20px; font-size: 22px; grid-column: 1/-1; } + .brand-icon { width: 38px; height: 38px; } + .brand small { font-size: 9px; } + .sidebar nav { grid-column: 1/-1; display: flex; overflow-x: auto; gap: 5px; padding-bottom: 12px; scrollbar-width: thin; scrollbar-color: #819573 #213d34; } + .sidebar nav button { padding: 10px 12px; gap: 7px; flex-shrink: 0; } + .sidebar nav button span { font-size: 12px; } + .sidebar nav button.active::after { display: none; } + .sidebar #current-user { position: static; grid-column: 1/-1; margin: 4px 0 10px; padding-top: 12px; font-size: 11px; } + .sidebar #account-password, .sidebar #logout { margin: 0; font-size: 11px; padding: 8px 12px; align-self: center; width: fit-content; } + .sidebar #logout { justify-self: end; } + main { margin: 0; padding: 20px 17px; } + main > header { min-height: 132px; padding-top: 9px; gap: 10px; margin-bottom: 20px; } + h1 { font-size: 28px; letter-spacing: 3px; } + .eyebrow { font-size: 9px; letter-spacing: 2px; } + .header-status { font-size: 9px; gap: 9px; max-width: 123px; padding-top: 2px; } + .header-status .badge { font-size: 9px; } + header p { max-width: 205px; font-size: 11px; line-height: 1.8; } + .courtyard-landscape { width: 170px; height: 90px; right: 0; opacity: .38; } + .metrics > div { padding: 16px; } + .metrics strong { font-size: 25px; } + .metrics > div:nth-last-child(-n+2) { border-bottom: 0; } + .filter-row { margin: 20px 0; } + .filter-row select { max-width: 155px; } + .filter-row > button { padding: 9px; } + .section-head { flex-wrap: wrap; } + .section-head > div { gap: 10px; } + .wall-toolbar { padding: 11px; gap: 10px; } + .wall-toolbar label { font-size: 12px; } + #wall-fullscreen { padding: 10px 12px; } + .camera-meta { padding: 14px; } + .camera-meta strong { font-size: 15px; } + .camera-actions { padding: 0 14px 14px; gap: 8px; } + .camera-actions button { padding: 9px 11px; } + .panel, .site-card { padding: 18px; } + .family-map-heading h2 { font-size: 23px; } + dialog { padding: 22px; } + #auth-dialog { padding: 30px 26px; } + .dialog-heading h2 { font-size: 21px; } + .dialog-footer { gap: 8px; } + footer { gap: 16px; flex-wrap: wrap; font-size: 9px; } +} +@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } } diff --git a/web/index.html b/web/index.html index 6c16073..2c59992 100644 --- a/web/index.html +++ b/web/index.html @@ -2,7 +2,7 @@ - 赵府智家 · 家庭空间 + 赵府智家 · 家庭空间 @@ -10,8 +10,8 @@
-
ZHAOFU HOME / FAMILY

实时画面

从一个空间,看到每一个位置。

等待连接
+
赵氏家宅 · 起居有序

实时画面

从一个空间,看到每一个位置。

等待连接
摄像头—
已接通码流—
录像占用—
空间档案—
家庭与公司,分别管理
-

现场画面 LIVE VIEW

主码流画面更清晰,最高 1920×1080;子码流更流畅,最高 768×432,适合网络较慢时使用。

+

现场画面 守望

主码流画面更清晰,最高 1920×1080;子码流更流畅,最高 768×432,适合网络较慢时使用。

- - - - - + + + + +
-
WELCOME HOME

欢迎回来

使用自己的账号,进入家庭空间。

+
灯火可亲 · 归家有时

欢迎回来

使用自己的账号,进入家庭空间。

空间档案

镜头通道配置

直接接入摄像头时,可读取设备实际提供的媒体配置。默认 ONVIF HTTP 端口为 80。

账号和密码只保存在本机受限数据目录。RTSP 服务响应代表设备可达,实际码流接通后才显示在线。路径预设需要与设备型号、通道一致。

摄像头对象

@@ -76,7 +76,7 @@

家谱人物

出生日期日期不详可留空

连接人物关系

每条亲子关系连接一位父母与一位子女。录入另一位父母时,再建立一条关系。

写一则家族记事

事情发生的日期 *
关联人物(可多选)

未选择人物时记为全家记事。每位关联人物的档案都可以追溯到这条记事。

正式发布后,有家谱查看权限的账号即可在时间线看到;草稿只对有编辑权限的人可见。

-
FAMILY CHRONICLE
+
家事 · 家声

修改记录

人物、关系与记事每次保存都会留存版本。仅有家谱编辑权限的账号可查阅和恢复。

修改我的密码

保存后,这个账号在所有设备上都需要用新密码重新登录。

摄像机参数

图像与日夜模式

作用于当前物理镜头,主、子码流共用这些图像参数。

码流与清晰度

作用于上方选定的码流。保存后画面可能短暂重连,录像机也会使用调整后的码流。本页保留原有视频编码。

其他萤石功能的接入情况
  • 云台方向微调与预置位:在实时画面的“云台”中使用,按设备能力启用。
  • 实时声音与全屏:通过画面播放器控制。
  • 双向对讲、智能追踪、声光告警、隐私遮蔽、设备翻转、云录像和消息推送:当前尚未接入,请使用萤石 App。
  • 录像机原有录像:仍通过录像机或原厂 App 查看;本系统已支持本机额外录像的回放。

功能按设备实际开放能力逐项接入,型号与固件不同可能有差异。

diff --git a/web/recording-ui.js b/web/recording-ui.js index 658ee88..3462c6e 100644 --- a/web/recording-ui.js +++ b/web/recording-ui.js @@ -21,9 +21,9 @@ async function queryRecordings(){ }catch(e){if(!e.stale&&generation===playbackGeneration)$('#play-info').textContent=e.message;} } function drawTimeline(){ - const c=$('#timeline'),w=c.clientWidth;if(!w)return;const scale=window.devicePixelRatio||1;c.width=w*scale;c.height=76*scale;const ctx=c.getContext('2d');ctx.scale(scale,scale);ctx.fillStyle='#101f25';ctx.fillRect(0,0,w,76);ctx.strokeStyle='#2b454e'; + const c=$('#timeline'),w=c.clientWidth;if(!w)return;const scale=window.devicePixelRatio||1;c.width=w*scale;c.height=76*scale;const ctx=c.getContext('2d');ctx.scale(scale,scale);const theme=getComputedStyle(document.documentElement);ctx.fillStyle=theme.getPropertyValue('--timeline-paper').trim()||'#ede8dd';ctx.fillRect(0,0,w,76);ctx.strokeStyle=theme.getPropertyValue('--timeline-grid').trim()||'#d3ccbc'; for(let i=0;i<=24;i++){const x=w*i/24;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,i%6===0?76:12);ctx.stroke();} - if(!queryStart||!queryEnd)return;ctx.fillStyle='#72cdb5';const total=queryEnd-queryStart; + if(!queryStart||!queryEnd)return;ctx.fillStyle=theme.getPropertyValue('--timeline-range').trim()||'#4c7761';const total=queryEnd-queryStart; for(const s of spans){const a=(s.start-queryStart)/total,b=(s.end-queryStart)/total;if(b>a)ctx.fillRect(a*w,25,Math.max(2,(b-a)*w),25);} } function playbackParams(range){return new URLSearchParams({camera:queryCamera,start:new Date(range.start).toISOString(),duration:String((range.end-range.start)/1000)});}