Merge pull request '添加计时器,麦克风,摄像头开关' (#45) from develop-frontend8 into develop 33分钟前 #46
@@ -19,6 +19,20 @@
|
|||||||
--radius-sm: 6px;
|
--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;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -151,7 +165,7 @@ body {
|
|||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
aspect-ratio: 4 / 3;
|
aspect-ratio: 4 / 3;
|
||||||
background: #0a0e17;
|
background: var(--color-surface-2);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
@@ -167,7 +181,7 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
background: #000;
|
background: #1a1a2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-placeholder {
|
.video-placeholder {
|
||||||
|
|||||||
@@ -2,16 +2,29 @@
|
|||||||
// CamTalk — 主应用组件(Web 端双栏布局)
|
// CamTalk — 主应用组件(Web 端双栏布局)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useVisionSession } from "./hooks/useVisionSession";
|
import { useVisionSession } from "./hooks/useVisionSession";
|
||||||
import { VideoPreview } from "./components/VideoPreview";
|
import { VideoPreview } from "./components/VideoPreview";
|
||||||
import { ChatPanel } from "./components/ChatPanel";
|
import { ChatPanel } from "./components/ChatPanel";
|
||||||
import { ConfigPanel } from "./components/ConfigPanel";
|
import { ConfigPanel } from "./components/ConfigPanel";
|
||||||
import { ToastContainer } from "./components/Toast";
|
import { ToastContainer } from "./components/Toast";
|
||||||
|
import { loadTheme, saveTheme } from "./lib/storage";
|
||||||
|
import type { Theme } from "./types";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [showConfig, setShowConfig] = useState(false);
|
const [showConfig, setShowConfig] = useState(false);
|
||||||
|
const [theme, setTheme] = useState<Theme>(loadTheme);
|
||||||
|
|
||||||
|
// 切换主题时更新 <html> 的 data-theme 属性
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const handleThemeChange = useCallback((t: Theme) => {
|
||||||
|
setTheme(t);
|
||||||
|
saveTheme(t);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
@@ -67,7 +80,9 @@ function App() {
|
|||||||
{showConfig && (
|
{showConfig && (
|
||||||
<ConfigPanel
|
<ConfigPanel
|
||||||
config={config}
|
config={config}
|
||||||
|
theme={theme}
|
||||||
onUpdate={updateConfig}
|
onUpdate={updateConfig}
|
||||||
|
onThemeChange={handleThemeChange}
|
||||||
onClose={() => setShowConfig(false)}
|
onClose={() => setShowConfig(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// ConfigPanel — 右侧抽屉式配置面板
|
// ConfigPanel — 右侧抽屉式配置面板
|
||||||
// 职责:TTS 开关、detail level 切换、语言选择
|
// 职责:主题切换、TTS 开关、detail level 切换、语言选择
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import type { SessionConfig } from "../../types";
|
import type { SessionConfig, Theme } from "../../types";
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
config: SessionConfig;
|
config: SessionConfig;
|
||||||
|
theme: Theme;
|
||||||
onUpdate: (partial: Partial<SessionConfig>) => void;
|
onUpdate: (partial: Partial<SessionConfig>) => void;
|
||||||
|
onThemeChange: (theme: Theme) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfigPanel({ config, onUpdate, onClose }: ConfigPanelProps) {
|
export function ConfigPanel({ config, theme, onUpdate, onThemeChange, onClose }: ConfigPanelProps) {
|
||||||
return (
|
return (
|
||||||
<div className="drawer-overlay" onClick={onClose}>
|
<div className="drawer-overlay" onClick={onClose}>
|
||||||
<div className="drawer" onClick={(e) => e.stopPropagation()}>
|
<div className="drawer" onClick={(e) => e.stopPropagation()}>
|
||||||
@@ -21,6 +23,24 @@ export function ConfigPanel({ config, onUpdate, onClose }: ConfigPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="drawer__body">
|
<div className="drawer__body">
|
||||||
|
<div className="config-group">
|
||||||
|
<div className="config-group__title">外观</div>
|
||||||
|
|
||||||
|
<label className="config-row">
|
||||||
|
<div className="config-row__info">
|
||||||
|
<span className="config-row__label">主题</span>
|
||||||
|
<span className="config-row__desc">切换明暗主题</span>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={theme}
|
||||||
|
onChange={(e) => onThemeChange(e.target.value as Theme)}
|
||||||
|
>
|
||||||
|
<option value="dark">深色</option>
|
||||||
|
<option value="light">浅色</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="config-group">
|
<div className="config-group">
|
||||||
<div className="config-group__title">会话</div>
|
<div className="config-group__title">会话</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
// 职责:会话配置持久化
|
// 职责:会话配置持久化
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import type { SessionConfig } from "../types";
|
import type { SessionConfig, Theme } from "../types";
|
||||||
|
|
||||||
const CONFIG_KEY = "camtalk:config";
|
const CONFIG_KEY = "camtalk:config";
|
||||||
|
const THEME_KEY = "camtalk:theme";
|
||||||
|
|
||||||
const DEFAULT_CONFIG: SessionConfig = {
|
const DEFAULT_CONFIG: SessionConfig = {
|
||||||
ttsEnabled: true,
|
ttsEnabled: true,
|
||||||
@@ -33,3 +34,19 @@ export function saveConfig(config: SessionConfig): void {
|
|||||||
// localStorage 不可用时静默失败
|
// 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 */ }
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface SessionConfig {
|
|||||||
language: string;
|
language: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Theme = "dark" | "light";
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user