diff --git a/frontend/src/App.css b/frontend/src/App.css index a9f8ac0..b6260b9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -19,6 +19,20 @@ --radius-sm: 6px; } +[data-theme="light"] { + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-bg: #f1f5f9; + --color-surface: #ffffff; + --color-surface-2: #f8fafc; + --color-text: #0f172a; + --color-text-muted: #64748b; + --color-border: #e2e8f0; + --color-success: #16a34a; + --color-warning: #d97706; + --color-error: #dc2626; +} + * { margin: 0; padding: 0; @@ -140,7 +154,7 @@ body { /* ---- 左侧视频面板 ---- */ .video-panel { - width: 340px; + width: 400px; flex-shrink: 0; display: flex; flex-direction: column; @@ -151,7 +165,7 @@ body { .video-container { position: relative; aspect-ratio: 4 / 3; - background: #0a0e17; + background: var(--color-surface-2); border-radius: var(--radius); overflow: hidden; border: 1px solid var(--color-border); @@ -167,7 +181,7 @@ body { width: 100%; height: 100%; object-fit: cover; - background: #000; + background: #1a1a2e; } .video-placeholder { @@ -392,44 +406,107 @@ body { border: 1px solid rgba(245, 158, 11, 0.2); } -/* ---- Config Panel ---- */ +/* ---- Drawer (Config Panel) ---- */ -.config-panel { +.drawer-overlay { + position: fixed; + inset: 0; + z-index: 100; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(2px); + animation: fadeIn 0.2s ease; +} + +.drawer { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 320px; background: var(--color-surface); - border-bottom: 1px solid var(--color-border); - padding: 16px 24px; + border-left: 1px solid var(--color-border); + display: flex; + flex-direction: column; + animation: slideInRight 0.25s ease; + box-shadow: -8px 0 24px rgba(0, 0, 0, 0.3); +} + +.drawer__header { display: flex; align-items: center; - gap: 24px; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid var(--color-border); flex-shrink: 0; } -.config-panel__header { - display: flex; - align-items: center; - gap: 8px; +.drawer__title { font-weight: 600; - font-size: 0.85rem; + font-size: 0.9rem; } -.config-panel__close { +.drawer__close { background: none; border: none; color: var(--color-text-muted); cursor: pointer; - font-size: 0.9rem; - padding: 2px 6px; + font-size: 1rem; + padding: 4px 8px; + border-radius: var(--radius-sm); + transition: all 0.15s; } -.config-panel__close:hover { +.drawer__close:hover { + background: var(--color-surface-2); color: var(--color-text); } +.drawer__body { + flex: 1; + overflow-y: auto; + padding: 16px; +} + +/* ---- Config Group ---- */ + +.config-group { + margin-bottom: 20px; +} + +.config-group__title { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--color-text-muted); + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--color-border); +} + .config-row { display: flex; align-items: center; - gap: 8px; + justify-content: space-between; + padding: 10px 0; font-size: 0.82rem; + color: var(--color-text); + cursor: pointer; +} + +.config-row__info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.config-row__label { + font-weight: 500; + color: var(--color-text); +} + +.config-row__desc { + font-size: 0.72rem; color: var(--color-text-muted); } @@ -439,8 +516,15 @@ body { color: var(--color-text); border: 1px solid var(--color-border); border-radius: var(--radius-sm); - padding: 4px 8px; + padding: 5px 10px; font-size: 0.8rem; + cursor: pointer; +} + +.config-row input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; } /* ---- Toast ---- */ @@ -559,6 +643,16 @@ body { to { opacity: 1; transform: translateX(0); } } +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideInRight { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} + /* ---- Scrollbar ---- */ ::-webkit-scrollbar { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7c5918d..bde81ef 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,16 +2,29 @@ // CamTalk — 主应用组件(Web 端双栏布局) // ============================================================ -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useVisionSession } from "./hooks/useVisionSession"; import { VideoPreview } from "./components/VideoPreview"; import { ChatPanel } from "./components/ChatPanel"; import { ConfigPanel } from "./components/ConfigPanel"; import { ToastContainer } from "./components/Toast"; +import { loadTheme, saveTheme } from "./lib/storage"; +import type { Theme } from "./types"; import "./App.css"; function App() { const [showConfig, setShowConfig] = useState(false); + const [theme, setTheme] = useState(loadTheme); + + // 切换主题时更新 的 data-theme 属性 + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + }, [theme]); + + const handleThemeChange = useCallback((t: Theme) => { + setTheme(t); + saveTheme(t); + }, []); const { messages, @@ -54,22 +67,22 @@ function App() { {isConnected ? "已连接" : connectionStatus === "connecting" ? "连接中..." : "未连接"} - {isConnected && ( - - )} + {showConfig && ( setShowConfig(false)} /> )} diff --git a/frontend/src/components/ConfigPanel/index.tsx b/frontend/src/components/ConfigPanel/index.tsx index a56306f..3844e6d 100644 --- a/frontend/src/components/ConfigPanel/index.tsx +++ b/frontend/src/components/ConfigPanel/index.tsx @@ -1,55 +1,92 @@ // ============================================================ -// ConfigPanel — 会话配置面板 -// 职责:TTS 开关、detail level 切换、语言选择 +// ConfigPanel — 右侧抽屉式配置面板 +// 职责:主题切换、TTS 开关、detail level 切换、语言选择 // ============================================================ -import type { SessionConfig } from "../../types"; +import type { SessionConfig, Theme } from "../../types"; interface ConfigPanelProps { config: SessionConfig; + theme: Theme; onUpdate: (partial: Partial) => void; + onThemeChange: (theme: Theme) => void; onClose: () => void; } -export function ConfigPanel({ config, onUpdate, onClose }: ConfigPanelProps) { +export function ConfigPanel({ config, theme, onUpdate, onThemeChange, onClose }: ConfigPanelProps) { return ( -
-
- 设置 - +
+
e.stopPropagation()}> +
+ 设置 + +
+ +
+
+
外观
+ + +
+ +
+
会话
+ + + + + + +
+
- - - - - -
); } diff --git a/frontend/src/lib/storage.ts b/frontend/src/lib/storage.ts index 34fc369..fb12324 100644 --- a/frontend/src/lib/storage.ts +++ b/frontend/src/lib/storage.ts @@ -3,9 +3,10 @@ // 职责:会话配置持久化 // ============================================================ -import type { SessionConfig } from "../types"; +import type { SessionConfig, Theme } from "../types"; const CONFIG_KEY = "camtalk:config"; +const THEME_KEY = "camtalk:theme"; const DEFAULT_CONFIG: SessionConfig = { ttsEnabled: true, @@ -33,3 +34,19 @@ export function saveConfig(config: SessionConfig): void { // localStorage 不可用时静默失败 } } + +/** 加载主题偏好 */ +export function loadTheme(): Theme { + try { + const raw = localStorage.getItem(THEME_KEY); + if (raw === "light" || raw === "dark") return raw; + } catch { /* ignore */ } + return "dark"; +} + +/** 保存主题偏好 */ +export function saveTheme(theme: Theme): void { + try { + localStorage.setItem(THEME_KEY, theme); + } catch { /* ignore */ } +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 4cad08a..0f29ec5 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -11,6 +11,8 @@ export interface SessionConfig { language: string; } +export type Theme = "dark" | "light"; + export interface Session { sessionId: string; createdAt: string;