diff --git a/apps/web-console/app.js b/apps/web-console/app.js index ce41832..bebc071 100755 --- a/apps/web-console/app.js +++ b/apps/web-console/app.js @@ -211,6 +211,60 @@ function refreshIcons() { } } +const GLASS_THEME_STORAGE_KEY = "cognitive-agent.glass-theme"; + +function currentGlassTheme() { + return document.documentElement.dataset.glassTheme === "light" ? "light" : "dark"; +} + +function applyGlassTheme(theme, { persist = false, announce = true } = {}) { + const normalized = theme === "light" ? "light" : "dark"; + document.documentElement.dataset.glassTheme = normalized; + document.documentElement.style.colorScheme = normalized; + const toggle = $("#glassThemeToggle"); + const nextLabel = normalized === "dark" ? "切换到白日模式" : "切换到夜间模式"; + if (toggle) { + toggle.dataset.theme = normalized; + toggle.setAttribute("aria-label", nextLabel); + toggle.setAttribute("aria-pressed", String(normalized === "dark")); + toggle.title = nextLabel; + } + const themeColor = $("#themeColorMeta"); + if (themeColor) { + themeColor.setAttribute("content", normalized === "dark" ? "#17182b" : "#eef7ff"); + } + if (persist) { + try { + localStorage.setItem(GLASS_THEME_STORAGE_KEY, normalized); + } catch (_error) { + // Keep the in-memory theme active if storage is unavailable. + } + try { + document.cookie = `${GLASS_THEME_STORAGE_KEY}=${normalized}; Path=/; Max-Age=31536000; SameSite=Lax`; + } catch (_error) { + // Cookie fallback can also be disabled; theme switching remains usable. + } + } + if (announce) { + window.dispatchEvent(new CustomEvent("cognitive-agent:theme-change", { + detail: { theme: normalized }, + })); + } + return normalized; +} + +function initGlassThemeToggle() { + const toggle = $("#glassThemeToggle"); + if (!toggle || toggle.dataset.themeToggleBound === "true") return; + toggle.dataset.themeToggleBound = "true"; + applyGlassTheme(currentGlassTheme(), { announce: false }); + toggle.addEventListener("click", () => { + applyGlassTheme(currentGlassTheme() === "dark" ? "light" : "dark", { + persist: true, + }); + }); +} + const TWIN_DEFAULT_POSITIONS = { longitudinal: 50, cushion_tilt: 50, @@ -1566,6 +1620,15 @@ function createTwinCabin(THREE, scene) { root: cabin, fallbackExteriorObjects, fallbackInteriorObjects, + themeMaterials: { + vehicleBody: [shellMaterial, bodyPanelMaterial], + vehicleDetail: [softTrim, consoleTop], + vehicleInterior: [trim, cockpit], + vehicleTrim: [exteriorTrim, tireMaterial], + vehicleMetal: [rimMaterial], + vehicleOutline: [shellEdge], + glass: [glassMaterial, greenhouseMaterial], + }, shellMaterial, glassMaterial, referenceWindowPanes: { @@ -1642,7 +1705,7 @@ function createTwinCabin(THREE, scene) { } const TWIN_THEME_PALETTES = { - user: { + dark: { accent: 0x7b70f5, accentStrong: 0x5d4dd9, accentSoft: 0x9ba8ff, @@ -1675,8 +1738,49 @@ const TWIN_THEME_PALETTES = { lampEmissive: 0x55acf4, lampEdge: 0x3476b9, }, + light: { + accent: 0x2f80ed, + accentStrong: 0x1d5fbf, + accentSoft: 0xdbeeff, + sceneBackground: 0xeff6ff, + floor: 0xe5f0fd, + gridStrong: 0x75a9e8, + gridSoft: 0xc7daf2, + fillLight: 0xb7d9ff, + vehicleBody: 0x91c6f5, + vehicleDetail: 0x77a9d8, + vehicleInterior: 0x4d79a6, + vehicleTrim: 0x284c70, + vehicleMetal: 0x93b9da, + vehicleOutline: 0x2879d8, + vehicleOccluded: 0x9abbdc, + seatShell: 0x2b76c9, + seatInsert: 0xcfe7ff, + seatThermal: 0x72b5f5, + seatBolster: 0x1b5fae, + seatStructure: 0x25476e, + seatEdge: 0x69b4ff, + seatSeam: 0xe4f2ff, + glass: 0x4b9bea, + glassEmissive: 0x164f91, + glassEdge: 0x1b6fc4, + glassGlow: 0x79c7ff, + roofClear: 0x67aef4, + roofDark: 0x183b70, + lamp: 0x2e7fd4, + lampEmissive: 0x48a4f2, + lampEdge: 0x1e6bb8, + }, }; +function currentTwinThemeRole() { + return currentGlassTheme(); +} + +function currentTwinThemePalette() { + return TWIN_THEME_PALETTES[currentTwinThemeRole()]; +} + const TWIN_DEVICE_HOTSPOT_LAYOUT = Object.freeze({ windows: { normalized: [0.08, 0.62, 0.54], labelOffset: [-115, 0] }, shade: { normalized: [0.5, 0.94, 0.43], labelOffset: [0, -65] }, @@ -1783,11 +1887,17 @@ function applyTwinGroundTheme(twin, palette) { if (previousTexture && previousTexture !== nextTexture) previousTexture.dispose(); } -function applyTwinVehicleTheme(systems) { +function applyTwinVehicleTheme(systems, palette = currentTwinThemePalette()) { if (!systems) return; - const palette = TWIN_THEME_PALETTES.user; - systems.themeRole = "user"; + systems.themeRole = currentTwinThemeRole(); systems.themePalette = palette; + Object.entries(systems.themeMaterials || {}).forEach(([token, materials]) => { + const color = palette[token]; + if (color === undefined) return; + materials.forEach((material) => { + if (material?.color) material.color.setHex(color); + }); + }); systems.colors.roofClear.setHex(palette.roofClear); systems.colors.roofDark.setHex(palette.roofDark); @@ -1871,15 +1981,32 @@ function applyTwinSeatTheme(model, palette) { }); } +function applyTwinReferenceVehicleTheme(root, palette) { + if (!root) return; + root.traverse((object) => { + if (!object.isMesh) return; + const materials = Array.isArray(object.material) ? object.material : [object.material]; + materials.filter(Boolean).forEach((material) => { + const token = material.userData?.twinThemeToken; + if (token && palette[token] !== undefined && material.color) { + material.color.setHex(palette[token]); + material.needsUpdate = true; + } + }); + }); +} + function applyTwinSceneTheme(twin) { if (!twin) return; - const palette = TWIN_THEME_PALETTES.user; - applyTwinVehicleTheme(twin.vehicleSystems); + const role = currentTwinThemeRole(); + const palette = currentTwinThemePalette(); + applyTwinVehicleTheme(twin.vehicleSystems, palette); twin.scene.background.setHex(palette.sceneBackground); if (twin.scene.fog?.color) twin.scene.fog.color.setHex(palette.sceneBackground); applyTwinGroundTheme(twin, palette); if (twin.fillLight?.color) twin.fillLight.color.setHex(palette.fillLight); Object.values(twin.models || {}).forEach((model) => applyTwinSeatTheme(model, palette)); + applyTwinReferenceVehicleTheme(twin.referenceVehicle, palette); [twin.vehicleSystems?.root, twin.referenceVehicle, twin.fixedModels?.rear] .filter(Boolean) .forEach((root) => root.traverse((object) => { @@ -1892,10 +2019,10 @@ function applyTwinSceneTheme(twin) { })); // Controllable glass and lamp entities use stronger semantic variants from // the same palette after the neutral vehicle outline pass above. - applyTwinVehicleTheme(twin.vehicleSystems); + applyTwinVehicleTheme(twin.vehicleSystems, palette); const canvas = twin.renderer?.domElement; if (canvas) { - canvas.dataset.themeRole = "user"; + canvas.dataset.themeRole = role; canvas.dataset.themeAccent = `#${palette.accent.toString(16).padStart(6, "0")}`; } } @@ -1980,7 +2107,7 @@ function updateTwinVehicleVisual(systems, timestamp) { : (fogStrength * 0.72 > mainLampStrength ? 0xfff0b3 : (isHighBeam ? 0xeafaff : 0xcdefff)); const updateFrontLampMaterial = (material, isReference = false) => { const isLit = lampStrength > 0.02; - const palette = systems.themePalette || TWIN_THEME_PALETTES.user; + const palette = systems.themePalette || currentTwinThemePalette(); if (material.color) material.color.setHex(isLit ? lampColor : palette.lamp); if (material.emissive) material.emissive.setHex(isLit ? lampColor : palette.lampEmissive); if ("emissiveIntensity" in material) { @@ -2001,7 +2128,7 @@ function updateTwinVehicleVisual(systems, timestamp) { activeOpacity, isHalo, }) => { - const palette = systems.themePalette || TWIN_THEME_PALETTES.user; + const palette = systems.themePalette || currentTwinThemePalette(); const idleColor = isHalo ? palette.glassGlow : palette.lamp; material.color.setHex(lampStrength > 0.02 ? lampColor : idleColor); material.opacity = idleOpacity + lampStrength * (activeOpacity - idleOpacity); @@ -3000,7 +3127,7 @@ function twinDeviceWorldAnchor(THREE, twin, deviceId, target) { } function configureReferenceVehicleModel(THREE, vehicle) { - const palette = TWIN_THEME_PALETTES.user; + const palette = currentTwinThemePalette(); const materialCache = new Map(); const report = { meshes: 0, materials: new Map(), nodes: [] }; vehicle.traverse((object) => { @@ -3016,6 +3143,7 @@ function configureReferenceVehicleModel(THREE, vehicle) { const material = sourceMaterial.clone(); const descriptor = referenceMaterialDescriptor(material); const category = classifyReferenceMaterial(material); + let themeToken = "vehicleDetail"; const outlineStyle = TWIN_OUTLINE_STYLE[category] || TWIN_OUTLINE_STYLE.detail; const key = `${category}:${material.name || "unnamed"}`; report.materials.set(key, (report.materials.get(key) || 0) + 1); @@ -3035,6 +3163,7 @@ function configureReferenceVehicleModel(THREE, vehicle) { if ("emissiveIntensity" in material) material.emissiveIntensity = 0; } if (category === "glass") { + themeToken = "glass"; if (material.color) material.color.setHex(palette.glass); material.transparent = true; material.opacity = 0.3; @@ -3043,25 +3172,37 @@ function configureReferenceVehicleModel(THREE, vehicle) { object.castShadow = false; object.renderOrder = 12; } else if (category === "body") { + themeToken = "vehicleBody"; if (material.color) material.color.setHex(palette.vehicleBody); if ("shininess" in material) material.shininess = 72; } else if (category === "light") { + themeToken = "lamp"; material.map = null; if (material.color) material.color.setHex(palette.lamp); if (material.emissive) material.emissive.setHex(0x000000); if ("emissiveIntensity" in material) material.emissiveIntensity = 0; } else if (category === "interior" && material.color) { + themeToken = "vehicleInterior"; material.color.setHex(palette.vehicleInterior); } else if (/lines|\badd\b/.test(descriptor) && material.color) { + themeToken = "vehicleTrim"; material.map = null; material.color.setHex(palette.vehicleTrim); } else if (/map[_ -]?[abc]/.test(descriptor) && material.color) { - if (/chrome|metal/.test(descriptor)) material.color.setHex(palette.vehicleMetal); - else if (/plastic/.test(descriptor)) material.color.setHex(palette.vehicleTrim); - else material.color.setHex(palette.vehicleDetail); + if (/chrome|metal/.test(descriptor)) { + themeToken = "vehicleMetal"; + material.color.setHex(palette.vehicleMetal); + } else if (/plastic/.test(descriptor)) { + themeToken = "vehicleTrim"; + material.color.setHex(palette.vehicleTrim); + } else { + material.color.setHex(palette.vehicleDetail); + } } else if (/plastic|under|black/.test(descriptor) && material.color) { + themeToken = "vehicleTrim"; material.color.setHex(palette.vehicleTrim); } else if (/chrome|metal|alloy|rim/.test(descriptor) && material.color) { + themeToken = "vehicleMetal"; material.color.setHex(palette.vehicleMetal); } material.transparent = true; @@ -3075,6 +3216,7 @@ function configureReferenceVehicleModel(THREE, vehicle) { if ("metalness" in material) material.metalness = category === "wheel" ? 0.48 : 0.08; if (material.emissive) material.emissive.setHex(0x000000); if ("emissiveIntensity" in material) material.emissiveIntensity = 0; + material.userData = { ...(material.userData || {}), twinThemeToken: themeToken }; material.needsUpdate = true; materialCache.set(sourceMaterial, material); return material; @@ -3945,7 +4087,7 @@ function initSeatTwin() { renderer.shadowMap.type = THREE.PCFSoftShadowMap; const scene = new THREE.Scene(); - const twinPalette = TWIN_THEME_PALETTES.user; + const twinPalette = currentTwinThemePalette(); scene.background = new THREE.Color(twinPalette.sceneBackground); scene.fog = new THREE.Fog(twinPalette.sceneBackground, 12, 19); const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 40); @@ -4081,6 +4223,9 @@ function initSeatTwin() { window.addEventListener("cognitive-agent:module-change", () => { applyTwinSceneTheme(state.seatTwin); }); + window.addEventListener("cognitive-agent:theme-change", () => { + applyTwinSceneTheme(state.seatTwin); + }); canvas.dataset.referenceModelState = "loading"; loadReferenceVehicleModel(THREE, scene, vehicleSystems, fallback) @@ -8866,6 +9011,7 @@ window.addEventListener("cognitive-agent:request-complete", (event) => { }); document.addEventListener("DOMContentLoaded", async () => { + initGlassThemeToggle(); initJointTwinLayout(); resetVehicleStatusDisplay(); initResultWorkspace(); diff --git a/apps/web-console/codepen-glass-theme.css b/apps/web-console/codepen-glass-theme.css index 5e3dada..f08c4dc 100644 --- a/apps/web-console/codepen-glass-theme.css +++ b/apps/web-console/codepen-glass-theme.css @@ -98,6 +98,95 @@ body.console-v2 { font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; } +html[data-glass-theme="light"] body.console-v2 { + color-scheme: light; + --background: oklch(0.965 0.022 239); + --foreground: oklch(0.27 0.055 253); + --card: oklch(0.995 0.008 245 / 0.72); + --card-foreground: oklch(0.32 0.045 251); + --popover: oklch(0.99 0.01 245 / 0.94); + --popover-foreground: oklch(0.27 0.055 253); + --primary: oklch(0.61 0.19 255); + --primary-foreground: oklch(0.99 0.004 245); + --secondary: oklch(0.93 0.035 244 / 0.76); + --secondary-foreground: oklch(0.36 0.055 252); + --muted: oklch(0.94 0.025 244 / 0.68); + --muted-foreground: oklch(0.5 0.045 250); + --accent: oklch(0.9 0.062 249 / 0.72); + --accent-foreground: oklch(0.36 0.12 257); + --destructive: oklch(0.61 0.19 24); + --border: oklch(0.58 0.07 249 / 0.2); + --input: oklch(0.55 0.075 249 / 0.28); + --ring: oklch(0.62 0.16 255); + --sidebar: oklch(0.965 0.025 244 / 0.7); + --sidebar-foreground: oklch(0.29 0.055 252); + --sidebar-primary: var(--primary); + --sidebar-primary-foreground: var(--primary-foreground); + --sidebar-accent: oklch(0.89 0.065 248 / 0.66); + --sidebar-accent-foreground: var(--foreground); + --sidebar-border: var(--border); + --sidebar-ring: var(--ring); + --heading-foreground: oklch(0.235 0.068 255); + --body-foreground: var(--card-foreground); + --subtle-foreground: var(--muted-foreground); + --status-success: oklch(0.62 0.15 158); + --status-online: var(--status-success); + --status-warning: oklch(0.7 0.15 78); + --status-offline: var(--destructive); + --status-info: var(--primary); + --status-success-foreground: oklch(0.4 0.12 158); + --status-warning-foreground: oklch(0.44 0.115 75); + --status-info-foreground: oklch(0.39 0.145 255); + --status-error-foreground: oklch(0.46 0.17 24); + --status-success-soft: color-mix(in oklch, var(--status-success) 12%, var(--card)); + --status-warning-soft: color-mix(in oklch, var(--status-warning) 12%, var(--card)); + --status-info-soft: color-mix(in oklch, var(--status-info) 11%, var(--card)); + --status-error-soft: color-mix(in oklch, var(--destructive) 10%, var(--card)); + --bg: var(--background); + --surface: var(--card); + --surface-muted: var(--muted); + --ink: var(--foreground); + --line: var(--border); + --line-strong: var(--input); + --teal: var(--primary); + --teal-dark: color-mix(in oklch, var(--primary) 80%, var(--foreground)); + --teal-soft: color-mix(in oklch, var(--primary) 11%, transparent); + --blue: var(--primary); + --blue-soft: color-mix(in oklch, var(--primary) 11%, transparent); + --amber: var(--status-warning); + --amber-soft: var(--status-warning-soft); + --red: var(--destructive); + --red-soft: var(--status-error-soft); + --accent-bright: color-mix(in oklch, var(--primary) 78%, white); + --accent-ink: var(--primary); + --accent-wash: color-mix(in oklch, var(--primary) 12%, transparent); + --v2-accent: var(--primary); + --v2-accent-strong: color-mix(in oklch, var(--primary) 78%, var(--foreground)); + --v2-accent-soft: color-mix(in oklch, var(--primary) 12%, transparent); + --v2-sidebar: var(--sidebar); + --v2-sidebar-deep: oklch(0.9 0.045 247 / 0.66); + --v2-ink: var(--foreground); + --v2-muted: var(--muted-foreground); + --v2-line: var(--border); + --v2-surface: var(--card); + --v2-canvas: transparent; + --glass-shell: oklch(0.975 0.018 244 / 0.68); + --glass-panel: oklch(0.99 0.012 245 / 0.62); + --glass-card: oklch(1 0 0 / 0.42); + --glass-card-hover: oklch(1 0 0 / 0.62); + --glass-line: oklch(0.46 0.075 252 / 0.16); + --glass-line-strong: oklch(0.42 0.09 253 / 0.24); + --glass-highlight: oklch(1 0 0 / 0.82); + --glass-shadow: 0 22px 64px oklch(0.35 0.09 254 / 0.18); + --glass-panel-shadow: 0 12px 30px oklch(0.36 0.075 252 / 0.13); + background: + radial-gradient(circle at 7% 8%, oklch(0.79 0.14 220 / 0.76), transparent 34%), + radial-gradient(circle at 77% -2%, oklch(0.78 0.14 286 / 0.52), transparent 36%), + radial-gradient(circle at 96% 80%, oklch(0.82 0.15 344 / 0.58), transparent 39%), + radial-gradient(circle at 17% 102%, oklch(0.84 0.12 26 / 0.48), transparent 37%), + linear-gradient(135deg, oklch(0.91 0.055 224), oklch(0.94 0.045 286) 52%, oklch(0.94 0.06 344)); +} + body.console-v2::before, body.console-v2::after { content: ""; @@ -134,18 +223,19 @@ body.console-v2 .v2-boot-screen { body.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .workspace, body.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .workspace { - width: calc(100vw - 28px) !important; - height: calc(100dvh - 28px) !important; + width: 100vw !important; + max-width: none !important; + height: 100dvh !important; min-height: 0; position: relative; z-index: 1; - margin: 14px !important; + margin: 0 !important; overflow: hidden; color: var(--foreground); background: var(--glass-shell) !important; - border: 1px solid var(--glass-line-strong); - border-radius: 18px; - box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-highlight); + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; -webkit-backdrop-filter: blur(22px) saturate(1.25); backdrop-filter: blur(22px) saturate(1.25); } @@ -384,6 +474,7 @@ body.console-v2 :is( .v2-extension-empty, .v2-pet-card-region, .v2-pet-card, + .v2-pet-presence-card, .v2-device-card, .joint-seat-panel, .domain-panel, @@ -405,6 +496,7 @@ body.console-v2 :is( .v2-extension-gateway, .v2-extension-synced-device, .v2-pet-card, + .v2-pet-presence-card, .v2-device-card, .joint-seat-panel, .domain-panel @@ -441,6 +533,7 @@ body.console-v2 :is(input, textarea)::placeholder { body.console-v2 :is( .v2-extension-nfc-launch, + .v2-pet-settings-launch, .v2-extension-gateway > button, .v2-nfc-inline > button, .v2-pet-card-region button, @@ -455,6 +548,7 @@ body.console-v2 :is( body.console-v2 :is( .v2-extension-nfc-launch, + .v2-pet-settings-launch, .v2-extension-gateway > button, .v2-nfc-inline > button, .v2-pet-card-region button, @@ -478,7 +572,7 @@ body.console-v2 .v2-pet-card > header > button:is(:hover, :focus-visible) { background: var(--status-error-soft) !important; } -body.console-v2 :is(.v2-nfc-inline-icon, .v2-extension-gateway > span, .v2-pet-card > header > span, .v2-pet-card-region > header > div > span) { +body.console-v2 :is(.v2-nfc-inline-icon, .v2-extension-gateway > span, .v2-pet-card > header > span, .v2-pet-presence-card > header > span, .v2-pet-card-region > header > div > span) { color: color-mix(in oklch, var(--primary) 74%, white) !important; background: color-mix(in oklch, var(--primary) 18%, transparent) !important; border-color: color-mix(in oklch, var(--primary) 32%, transparent) !important; @@ -507,6 +601,7 @@ body.console-v2 :is( .mode-editor, .v2-nfc-dialog, .v2-gscu-tcp-dialog, + .v2-pet-settings-dialog, .energy-monitor-page ) { color: var(--card-foreground) !important; @@ -547,13 +642,147 @@ body.console-v2 :is(.mode-gallery-close, .mode-editor-header > button, .v2-exten background: var(--glass-card-hover) !important; } +/* Persistent day / night appearance ------------------------------------- */ +.glass-theme-toggle { + width: 48px; + height: 48px; + position: fixed; + left: max(16px, env(safe-area-inset-left)); + bottom: max(16px, env(safe-area-inset-bottom)); + z-index: 1600; + display: grid; + place-items: center; + padding: 0; + color: var(--foreground); + background: var(--glass-card-hover); + border: 1px solid var(--glass-line-strong); + border-radius: 50%; + box-shadow: 0 12px 30px color-mix(in oklch, var(--foreground) 18%, transparent), inset 0 1px 0 var(--glass-highlight); + -webkit-backdrop-filter: blur(18px) saturate(1.24); + backdrop-filter: blur(18px) saturate(1.24); + cursor: pointer; + transition: transform 160ms ease, color 160ms ease, background 160ms ease, border-color 160ms ease; +} + +.glass-theme-toggle:is(:hover, :focus-visible) { + color: var(--primary-foreground); + background: var(--primary); + border-color: color-mix(in oklch, var(--primary) 72%, white 18%); + transform: translateY(-2px) rotate(6deg); +} + +.glass-theme-toggle svg { + grid-area: 1 / 1; + width: 21px; + height: 21px; + transition: opacity 150ms ease, transform 180ms ease; +} + +html[data-glass-theme="dark"] .glass-theme-toggle [data-theme-icon="moon"], +html[data-glass-theme="light"] .glass-theme-toggle [data-theme-icon="sun"] { + opacity: 0; + transform: scale(0.5) rotate(-35deg); + pointer-events: none; +} + +html[data-glass-theme="dark"] .glass-theme-toggle [data-theme-icon="sun"], +html[data-glass-theme="light"] .glass-theme-toggle [data-theme-icon="moon"] { + opacity: 1; + transform: scale(1) rotate(0); +} + +html[data-glass-theme="light"] body.console-v2::before { + background: oklch(0.78 0.14 220 / 0.54); +} + +html[data-glass-theme="light"] body.console-v2::after { + background: oklch(0.8 0.15 338 / 0.5); +} + +html[data-glass-theme="light"] body.console-v2 .v2-boot-screen { + color: var(--foreground); + background: + radial-gradient(circle at 50% 42%, color-mix(in oklch, var(--primary) 20%, transparent), transparent 18rem), + var(--background); +} + +html[data-glass-theme="light"] body.console-v2.v2-passive-display.v2-interactive-sync .command-panel.v2-sidebar { + background: + linear-gradient(165deg, color-mix(in oklch, var(--primary) 6%, transparent), color-mix(in oklch, var(--card) 76%, transparent)) !important; +} + +html[data-glass-theme="light"] body.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail { + background: + linear-gradient(110deg, color-mix(in oklch, var(--primary) 9%, transparent), transparent 44%), + color-mix(in oklch, var(--glass-panel) 92%, transparent) !important; + box-shadow: inset 0 1px 0 var(--glass-highlight), var(--glass-panel-shadow); +} + +html[data-glass-theme="light"] body.console-v2.v2-passive-display.v2-interactive-sync :is( + #assistantDrawer, + .joint-twin-shell > .twin-band, + #v2DevicePanel, + #v2ExtensionPanel .v2-extension-sync-mount +) { + background: + linear-gradient(145deg, color-mix(in oklch, var(--primary) 5%, transparent), transparent 46%), + var(--glass-panel) !important; +} + +html[data-glass-theme="light"] body.console-v2 :is( + .v2-operation-heading, + .joint-twin-shell .twin-heading, + .v2-device-sync-board > header, + .v2-extension-sync-heading +) { + background: color-mix(in oklch, var(--card) 72%, transparent) !important; +} + +html[data-glass-theme="light"] body.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-request-form .command-composer { + background: color-mix(in oklch, var(--popover) 70%, transparent) !important; + box-shadow: inset 0 1px 0 var(--glass-highlight), var(--glass-panel-shadow) !important; +} + +html[data-glass-theme="light"] body.console-v2.v2-passive-display.v2-interactive-sync .joint-twin-shell .twin-stage { + background: + radial-gradient(ellipse at 53% 58%, color-mix(in oklch, var(--primary) 15%, transparent), transparent 44%), + radial-gradient(circle at 82% 8%, color-mix(in oklch, var(--status-info) 11%, transparent), transparent 34%), + linear-gradient(160deg, color-mix(in oklch, var(--card) 58%, transparent), color-mix(in oklch, var(--accent) 42%, transparent)) !important; +} + +html[data-glass-theme="light"] body.console-v2 .v2-twin-hotspot-label, +html[data-glass-theme="light"] body.console-v2 .v2-twin-device-popover { + background: color-mix(in oklch, var(--popover) 88%, transparent) !important; + box-shadow: var(--glass-panel-shadow), inset 0 1px 0 var(--glass-highlight) !important; +} + +html[data-glass-theme="light"] body.console-v2 :is(input, textarea, select), +html[data-glass-theme="light"] body.console-v2 .v2-pet-nfc-field output { + background: color-mix(in oklch, var(--popover) 72%, transparent) !important; + box-shadow: inset 0 1px 0 var(--glass-highlight) !important; +} + +html[data-glass-theme="light"] body.console-v2 :is( + .mode-gallery, + .mode-editor, + .v2-nfc-dialog, + .v2-gscu-tcp-dialog, + .v2-pet-settings-dialog, + .energy-monitor-page +) { + background: + linear-gradient(145deg, color-mix(in oklch, var(--primary) 6%, transparent), transparent 44%), + color-mix(in oklch, var(--popover) 94%, transparent) !important; + box-shadow: 0 28px 84px color-mix(in oklch, var(--foreground) 20%, transparent), inset 0 1px 0 var(--glass-highlight) !important; +} + /* Scrollbars ------------------------------------------------------------- */ body.console-v2 * { - scrollbar-color: color-mix(in oklch, var(--primary) 70%, white 8%) oklch(0.08 0.025 286 / 0.32); + scrollbar-color: color-mix(in oklch, var(--primary) 70%, white 8%) color-mix(in oklch, var(--background) 66%, transparent); } body.console-v2 *::-webkit-scrollbar-track { - background: oklch(0.08 0.025 286 / 0.28); + background: color-mix(in oklch, var(--background) 66%, transparent); } body.console-v2 *::-webkit-scrollbar-thumb { diff --git a/apps/web-console/index.html b/apps/web-console/index.html index 1fe59ae..0a8ca75 100755 --- a/apps/web-console/index.html +++ b/apps/web-console/index.html @@ -4,19 +4,43 @@ - + 511工程-智能座舱 + - + - + @@ -24,12 +48,12 @@ - + - + @@ -606,6 +630,11 @@ +
diff --git a/apps/web-console/self-extension.css b/apps/web-console/self-extension.css index 0c133d1..fb50f82 100644 --- a/apps/web-console/self-extension.css +++ b/apps/web-console/self-extension.css @@ -3985,15 +3985,12 @@ form > .v2-extension-primary { /* Durable NFC pet cards ------------------------------------------------- */ .v2-extension-sync-mount { - grid-template-rows: auto auto auto auto minmax(0, 1fr) auto; + grid-template-rows: auto auto auto minmax(0, 1fr) auto; } .console-v2 #v2ExtensionPanel .v2-extension-sync-mount { overflow-x: hidden; - overflow-y: auto; - overscroll-behavior-y: contain; - scrollbar-gutter: stable; - touch-action: pan-y; + overflow-y: hidden; } .v2-pet-card-region { @@ -4466,7 +4463,8 @@ form > .v2-extension-primary { .v2-nfc-no-pets { display: grid; - grid-template-columns: auto minmax(0, 1fr); + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; } .v2-nfc-body > .v2-nfc-existing { @@ -4525,4 +4523,294 @@ form > .v2-extension-primary { .v2-nfc-body > .v2-nfc-existing button { width: 100%; } + + .v2-nfc-no-pets { + grid-template-columns: auto minmax(0, 1fr); + } + + .v2-nfc-no-pets > button { + grid-column: 1 / -1; + width: 100%; + } +} + +/* Pet settings dialog and detected-pet presence ------------------------ */ +.v2-extension-sync-actions { + min-width: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; +} + +.v2-pet-settings-launch { + min-height: 25px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 5px; + padding: 4px 8px; + color: var(--primary-foreground); + background: var(--primary); + border: 1px solid var(--primary); + border-radius: var(--ui-radius-control-inner, 7px); + box-shadow: inset 0 1px 0 color-mix(in oklch, white 24%, transparent); + font-size: 7.5px; + font-weight: 850; + white-space: nowrap; + cursor: pointer; +} + +.v2-pet-settings-launch:is(:hover, :focus-visible) { + background: color-mix(in oklch, var(--primary) 86%, var(--foreground)); +} + +.v2-pet-settings-launch svg { + width: 13px; + height: 13px; +} + +.v2-extension-device-list { + min-height: 0; + max-height: none; + display: grid; + grid-auto-rows: max-content; + align-content: start; + gap: 8px; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior-y: contain; + scrollbar-gutter: stable; + touch-action: pan-y; +} + +.v2-pet-settings-dialog { + width: min(760px, calc(100vw - 32px)); + height: min(760px, calc(100dvh - 32px)); + max-height: calc(100dvh - 32px); + overflow: hidden; +} + +.v2-pet-settings-dialog[open] { + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; +} + +.v2-pet-settings-body { + min-height: 0; + padding: 16px; + overflow: hidden; +} + +.v2-pet-settings-body .v2-pet-card-region { + height: 100%; + grid-template-rows: auto minmax(0, 1fr); +} + +.v2-pet-settings-body .v2-pet-card-list { + padding-right: 4px; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior-y: contain; + scrollbar-gutter: stable; + touch-action: pan-y; +} + +.v2-pet-settings-footnote { + min-height: 48px; + display: flex; + align-items: center; + gap: 8px; + padding: 11px 18px; + color: var(--muted-foreground); + background: color-mix(in oklch, var(--popover) 92%, transparent); + border-top: 1px solid var(--border); + font-size: 10px; + line-height: 1.45; +} + +.v2-pet-settings-footnote svg { + width: 15px; + height: 15px; + flex: none; + color: var(--primary); +} + +.v2-pet-presence-card { + min-width: 0; + position: relative; + isolation: isolate; + display: grid; + gap: 11px; + padding: 12px; + color: var(--card-foreground); + background: + linear-gradient(145deg, color-mix(in oklch, var(--card) 82%, transparent), color-mix(in oklch, var(--primary) 10%, transparent)); + border: 1px solid color-mix(in oklch, var(--primary) 28%, var(--border)); + border-radius: var(--ui-radius-item, 12px); + box-shadow: + 0 10px 26px color-mix(in oklch, var(--foreground) 8%, transparent), + inset 0 1px 0 color-mix(in oklch, white 28%, transparent); + -webkit-backdrop-filter: blur(16px) saturate(1.24); + backdrop-filter: blur(16px) saturate(1.24); +} + +.v2-pet-presence-card::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + pointer-events: none; + background: radial-gradient(circle at 10% 0%, color-mix(in oklch, white 24%, transparent), transparent 42%); +} + +.v2-pet-presence-card > header { + min-width: 0; + display: grid; + grid-template-columns: 38px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; +} + +.v2-pet-presence-card > header > span { + width: 38px; + height: 38px; + display: grid; + place-items: center; + color: var(--primary); + background: var(--accent); + border: 1px solid color-mix(in oklch, var(--primary) 24%, var(--border)); + border-radius: 11px; +} + +.v2-pet-presence-card > header > span svg { + width: 18px; + height: 18px; +} + +.v2-pet-presence-card > header > div { + min-width: 0; + display: grid; + gap: 2px; +} + +.v2-pet-presence-card > header small, +.v2-pet-presence-card dt { + color: var(--muted-foreground); + font-size: 9px; + font-weight: 750; +} + +.v2-pet-presence-card > header strong { + overflow: hidden; + color: var(--heading-foreground); + font-size: 15px; + font-weight: 850; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-pet-presence-card > header > b { + padding: 5px 8px; + color: var(--status-success-foreground); + background: var(--status-success-soft); + border: 1px solid color-mix(in oklch, var(--status-success) 28%, var(--border)); + border-radius: 999px; + font-size: 9px; + white-space: nowrap; +} + +.v2-pet-presence-card > header > b[data-state="error"] { + color: var(--status-error-foreground); + background: var(--status-error-soft); + border-color: color-mix(in oklch, var(--status-error) 28%, var(--border)); +} + +.v2-pet-presence-card dl { + min-width: 0; + display: grid; + gap: 7px; + margin: 0; +} + +.v2-pet-presence-card dl > div { + min-width: 0; + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + align-items: baseline; + gap: 8px; +} + +.v2-pet-presence-card dd { + min-width: 0; + margin: 0; + overflow: hidden; + color: var(--foreground); + font-size: 11px; + font-weight: 720; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-pet-presence-card code { + color: var(--primary); + font: 750 10px/1.4 "SFMono-Regular", Consolas, monospace; +} + +.v2-pet-presence-card > footer { + min-width: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding-top: 9px; + color: var(--muted-foreground); + border-top: 1px solid color-mix(in oklch, var(--border) 72%, transparent); + font-size: 9px; +} + +.v2-pet-presence-card > footer span { + min-width: 0; + display: inline-flex; + align-items: center; + gap: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-pet-presence-card > footer svg { + width: 13px; + height: 13px; + flex: none; + color: var(--primary); +} + +.v2-pet-presence-card > footer strong { + flex: none; + color: var(--foreground); + font-size: 9px; +} + +@media (max-width: 720px) { + .v2-extension-sync-heading > div:first-child { + grid-template-columns: minmax(0, 1fr); + align-items: start; + } + + .v2-extension-sync-actions { + justify-content: flex-start; + flex-wrap: wrap; + } + + .v2-pet-settings-dialog { + width: calc(100vw - 16px); + height: calc(100dvh - 16px); + max-height: calc(100dvh - 16px); + } + + .v2-pet-settings-body { + padding: 10px; + } } diff --git a/apps/web-console/self-extension.js b/apps/web-console/self-extension.js index 3f1388c..c984496 100644 --- a/apps/web-console/self-extension.js +++ b/apps/web-console/self-extension.js @@ -761,6 +761,11 @@
JSON 调试结果
等待操作
+ +

宠物设置

添加或编辑宠物资料;保存后可在 NFC 设备授权中绑定卡片。

+
+ +
`; document.body.append(layer); } @@ -2941,6 +2946,7 @@ function bindEvents() { bindGscuTcpDialogEvents(); + bindPetSettingsDialogEvents(); $$('[data-extension-mode]').forEach((button) => { button.addEventListener("click", () => setMode(button.dataset.extensionMode)); }); @@ -3396,7 +3402,10 @@ } if (render) { renderNfcDialog(); - if (cardsChanged) renderUnifiedExtensionView?.(); + if (cardsChanged) { + renderPetSettingsDialog(); + renderUnifiedExtensionView?.(); + } else updatePetPublisherIndicators(); } return true; @@ -3427,7 +3436,7 @@ applyPetCardSnapshot(response); showToast("已添加宠物卡片,请编辑并保存后再绑定 NFC。", "success"); window.setTimeout(() => { - const card = $(`[data-pet-card-id="${selectedPetCardId}"]`); + const card = $(`[data-pet-card-id="${selectedPetCardId}"]`, $("#extensionPetSettingsDialog")); card?.scrollIntoView({ behavior: "smooth", block: "nearest" }); $('[data-pet-field="pet_name"]', card)?.focus(); }, 0); @@ -3482,7 +3491,7 @@ if (!petId) return; const petCard = petCards().find((card) => card.id === petId); if (!petCardCanBind(petCard)) { - showToast("请先在右侧宠物卡片中完成编辑并保存。", "error"); + showToast("请先在“宠物设置”中完成编辑并保存。", "error"); return; } const button = $("[data-nfc-bind-pet]"); @@ -3531,11 +3540,11 @@ `; } if (!petCards().length) { - return `
暂无宠物卡片请先返回右侧“宠物卡片”区域添加、编辑并保存卡片。
`; + return `
暂无宠物卡片请先打开“宠物设置”添加并保存宠物资料。
`; } const bindableCards = petCards().filter(petCardCanBind); if (!bindableCards.length) { - return `
暂无可绑定的已保存卡片请先在右侧完成宠物名与动作编辑并点击保存,再打开此弹窗绑定。
`; + return `
暂无可绑定的已保存卡片请先在“宠物设置”中完成宠物名与动作编辑并保存。
`; } const selected = bindableCards.some((card) => card.id === selectedPetCardId) ? selectedPetCardId @@ -3543,11 +3552,11 @@ return `
绑定宠物卡片
卡片 ID 由当前读卡器中的 NFC 自动写入,不能手动编辑;同一 NFC 只绑定一张宠物卡片。
`; } - function renderPetCardRegion() { + function renderPetCardEditor() { const publisher = petCardSnapshot.publisher || {}; const cards = petCards(); - return `
-

宠物卡片

+ return `
+

宠物卡片

${cards.length ? cards.map((card) => { const draft = petCardDrafts.get(card.id) || card; @@ -3569,19 +3578,82 @@
`; } + function renderPetSettingsDialog() { + const body = $("#extensionPetSettingsBody"); + if (!body) return; + body.innerHTML = renderPetCardEditor(); + window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }); + } + + function openPetSettingsDialog() { + renderPetSettingsDialog(); + openDialog("extensionPetSettingsDialog"); + } + + function bindPetSettingsDialogEvents() { + const dialog = $("#extensionPetSettingsDialog"); + if (!dialog || dialog.dataset.petSettingsBound === "true") return; + dialog.dataset.petSettingsBound = "true"; + dialog.addEventListener("click", (event) => { + if (event.target.closest('[data-close-extension-dialog="extensionPetSettingsDialog"]')) { + closeDialog("extensionPetSettingsDialog"); + return; + } + const addPet = event.target.closest("[data-pet-add]"); + const article = event.target.closest("[data-pet-card-id]"); + const savePet = event.target.closest("[data-pet-save]"); + const deletePet = event.target.closest("[data-pet-delete]"); + if (addPet) void createPetCard(addPet); + else if (savePet) void savePetCard(article, savePet); + else if (deletePet) void deletePetCard(article, deletePet); + }); + dialog.addEventListener("input", (event) => { + const field = event.target.closest("[data-pet-field]"); + const article = field?.closest("[data-pet-card-id]"); + if (!field || !article) return; + const current = petCardDrafts.get(article.dataset.petCardId) + || petCards().find((card) => card.id === article.dataset.petCardId) + || {}; + petCardDrafts.set(article.dataset.petCardId, { + ...current, + [field.dataset.petField]: field.value, + }); + const title = $("header strong", article); + if (field.dataset.petField === "pet_name" && title) { + title.textContent = field.value.trim() || "未命名宠物"; + } + updatePetPublisherIndicators(); + }); + } + + function renderDetectedPetCard(card) { + if (!card) return ""; + const status = petCardStatus(card); + const publisher = petCardSnapshot.publisher || {}; + const activeError = publisher.active_pet_id === card.id + ? String(publisher.last_error || "") + : ""; + return `
+
当前识别宠物${escapeHtml(card.pet_name)}
${escapeHtml(status.label)}
+
宠物动作
${escapeHtml(card.action)}
卡片 ID
${escapeHtml(card.nfc_id)}
+
${escapeHtml(publisher.topic || "n7/pet")}3 帧 / 1 秒
+
`; + } + function updatePetPublisherIndicators() { + const publisher = petCardSnapshot.publisher || {}; petCards().forEach((card) => { const status = petCardStatus(card); - const badge = $(`[data-pet-publish-state="${card.id}"]`); - if (badge) { + $$(`[data-pet-publish-state="${card.id}"]`).forEach((badge) => { badge.dataset.state = status.state; badge.textContent = status.label; badge.title = publisher.active_pet_id === card.id && publisher.last_error ? String(publisher.last_error) : status.label; - } - const article = $(`[data-pet-card-id="${card.id}"]`); - if (article) article.dataset.petState = status.state; + }); + $$(`[data-pet-card-id="${card.id}"], [data-detected-pet-id="${card.id}"]`).forEach((article) => { + article.dataset.petState = status.state; + }); }); } @@ -3821,44 +3893,25 @@ function bindUnifiedNfcEvents(mount) { mount.addEventListener("click", (event) => { - const addPet = event.target.closest("[data-pet-add]"); - const petArticle = event.target.closest("[data-pet-card-id]"); - const savePet = event.target.closest("[data-pet-save]"); - const deletePet = event.target.closest("[data-pet-delete]"); - if (addPet) void createPetCard(addPet); - else if (savePet) void savePetCard(petArticle, savePet); - else if (deletePet) void deletePetCard(petArticle, deletePet); + if (event.target.closest("[data-open-pet-settings]")) openPetSettingsDialog(); else if (event.target.closest("[data-open-nfc-binding]")) openNfcDialog(); else if (event.target.closest("[data-open-gscu-tcp]")) openGscuTcpDialog(); }); - mount.addEventListener("input", (event) => { - const field = event.target.closest("[data-pet-field]"); - const article = field?.closest("[data-pet-card-id]"); - if (!field || !article) return; - const current = petCardDrafts.get(article.dataset.petCardId) - || petCards().find((card) => card.id === article.dataset.petCardId) - || {}; - petCardDrafts.set(article.dataset.petCardId, { - ...current, - [field.dataset.petField]: field.value, - }); - const title = $("header strong", article); - if (field.dataset.petField === "pet_name" && title) { - title.textContent = field.value.trim() || "未命名宠物"; - } - updatePetPublisherIndicators(); - }); bindGscuTcpDialogEvents(); + bindPetSettingsDialogEvents(); const dialog = $("#extensionNfcDialog"); dialog.addEventListener("click", (event) => { + if (event.target.closest("[data-open-pet-settings]")) { + closeDialog("extensionNfcDialog"); + openPetSettingsDialog(); + return; + } if (event.target.closest("[data-close-nfc-dialog]")) closeDialog("extensionNfcDialog"); if (event.target.closest("[data-nfc-bind-existing]")) bindCurrentNfcToExisting(); if (event.target.closest("[data-nfc-unbind]")) unbindCurrentNfc(); if (event.target.closest("[data-nfc-bind-pet]")) void bindCurrentNfcToPet(); const unbindPet = event.target.closest("[data-nfc-unbind-pet]"); if (unbindPet) void unbindPetCard(unbindPet.dataset.nfcUnbindPet); - const addPet = event.target.closest("[data-pet-add]"); - if (addPet) void createPetCard(addPet); }); dialog.addEventListener("change", (event) => { const petSelect = event.target.closest("[data-nfc-pet-select]"); @@ -3931,10 +3984,6 @@ } } if (health.pet_cards) applyPetCardSnapshot(health.pet_cards, { render: false }); - if (document.activeElement?.closest?.("#vehicleExtensionSync [data-pet-field]")) { - updatePetPublisherIndicators(); - return; - } const physicalConnected = health.transport !== "simulation" && Boolean(health.connected || health.peer_connected); const listening = Boolean(health.listening); @@ -4028,10 +4077,10 @@ ? `${nfcCard.uid} · 点击关联设备 ID` : nfcReaderNote(nfcReader); mount.dataset.hasDevices = String(devices.length > 0); - mount.dataset.hasPets = String(petCards().length > 0); + mount.dataset.hasPets = String(Boolean(nfcPet)); mount.innerHTML = `
-

外设与扩展能力

+

外设与扩展能力

${statusLabel}
@@ -4045,15 +4094,15 @@ ${String(health.transport || "GSCU").toUpperCase()} - ${renderPetCardRegion()}
+ ${nfcPet ? renderDetectedPetCard(nfcPet) : ""} ${devices.length ? devices.map((device) => `
${escapeHtml(device.name)}${escapeHtml(device.type)}${device.cardUid ? " · 已绑定 NFC" : ""}
${device.actions.length} 项能力
${device.actions.slice(0, 8).map((action) => `${escapeHtml(action)}`).join("")}
-
`).join("") : ` + `).join("") : nfcPet ? "" : `
-
${nfcPet ? "当前 NFC 已绑定宠物卡片" : sensedCardUid ? "当前 NFC 暂无已授权命令" : "当前未感应到外设"}${nfcPet ? "本次识别完成 3 帧后保持静默,移开再靠近才会再次发送" : sensedCardUid ? "请检查设备 ID 绑定及命令复选框" : "将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令"}
+
${sensedCardUid ? "当前 NFC 暂无已授权命令" : "当前未感应到外设"}${sensedCardUid ? "请检查设备 ID 绑定及命令复选框" : "将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令"}
`}