Skip to content

Commit b0ffd91

Browse files
committed
refactor: 组件解耦,使用新的 hooks 和服务入口
- VisionMirrorPanel: 使用 useVisionMirror hook 替代直接调用 visionService - VoiceInteractionPanel: 使用 useVoiceInteraction hook 替代直接调用 asrService/ttsService - SettingsDrawer: 通过 props 接收回调,移除对 digitalHumanEngine 的直接依赖 - 页面组件: 从 core/services 导入服务,从 hooks 导入控制器
1 parent b88328d commit b0ffd91

5 files changed

Lines changed: 79 additions & 217 deletions

File tree

src/components/SettingsDrawer.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { Settings, X, Sun, Moon } from 'lucide-react';
22
import { useDigitalHumanStore } from '../store/digitalHumanStore';
3-
import { digitalHumanEngine } from '../core/avatar';
4-
import { toast } from 'sonner';
53
import { useFocusTrap, useTheme } from '../hooks';
64
import ControlPanel from './ControlPanel';
75
import ExpressionControlPanel from './ExpressionControlPanel';
86
import BehaviorControlPanel from './BehaviorControlPanel';
97
import VisionMirrorPanel from './VisionMirrorPanel';
108
import VoiceInteractionPanel from './VoiceInteractionPanel';
9+
import type { UserEmotion } from '../core/vision/visionMapper';
1110

1211
interface SettingsDrawerProps {
1312
show: boolean;
@@ -23,6 +22,8 @@ interface SettingsDrawerProps {
2322
onChatSend: (text?: string) => void;
2423
onExpressionChange: (expression: string, intensity: number) => void;
2524
onBehaviorChange: (behavior: string, params: Record<string, unknown>) => void;
25+
onEmotionChange: (emotion: UserEmotion) => void;
26+
onHeadMotion: (motion: 'nod' | 'shakeHead' | 'raiseHand' | 'waveHand') => void;
2627
}
2728

2829
const TABS = ['basic', 'expression', 'behavior', 'vision', 'voice'] as const;
@@ -41,6 +42,8 @@ export default function SettingsDrawer({
4142
onChatSend,
4243
onExpressionChange,
4344
onBehaviorChange,
45+
onEmotionChange,
46+
onHeadMotion,
4447
}: SettingsDrawerProps) {
4548
const isPlaying = useDigitalHumanStore((s) => s.isPlaying);
4649
const isRecording = useDigitalHumanStore((s) => s.isRecording);
@@ -147,15 +150,7 @@ export default function SettingsDrawer({
147150
{activeTab === 'vision' && (
148151
<div className="rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-gray-400">
149152
Vision Mirror Module requires camera access.
150-
<VisionMirrorPanel
151-
onEmotionChange={(emotion) => {
152-
digitalHumanEngine.setEmotion(emotion);
153-
}}
154-
onHeadMotion={(motion) => {
155-
digitalHumanEngine.playAnimation(motion);
156-
toast(`Motion Detected: ${motion}`, { icon: '📸' });
157-
}}
158-
/>
153+
<VisionMirrorPanel onEmotionChange={onEmotionChange} onHeadMotion={onHeadMotion} />
159154
</div>
160155
)}
161156
{activeTab === 'voice' && (

src/components/VisionMirrorPanel.tsx

Lines changed: 33 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { useEffect, useRef, useState, useCallback } from 'react';
2-
import { visionService } from '../core/vision/visionService';
1+
import { useVisionMirror } from '../hooks/useVisionMirror';
32
import type { UserEmotion } from '../core/vision/visionMapper';
43
import { Camera, CameraOff, ScanFace, AlertCircle, Loader2 } from 'lucide-react';
54
import { toast } from 'sonner';
@@ -31,145 +30,80 @@ export default function VisionMirrorPanel({
3130
onEmotionChange,
3231
onHeadMotion,
3332
}: VisionMirrorPanelProps) {
34-
const videoRef = useRef<HTMLVideoElement | null>(null);
35-
const [isCameraOn, setIsCameraOn] = useState(false);
36-
const [isLoading, setIsLoading] = useState(false);
37-
const [error, setError] = useState<string | null>(null);
38-
const [currentEmotion, setCurrentEmotion] = useState<UserEmotion>('neutral');
39-
const [lastMotion, setLastMotion] = useState<string | null>(null);
40-
const [fps, setFps] = useState(0);
41-
42-
// 清理函数
43-
useEffect(() => {
44-
return () => {
45-
visionService.stop();
46-
};
47-
}, []);
48-
49-
// 动作检测提示自动消失
50-
useEffect(() => {
51-
if (lastMotion) {
52-
const timer = setTimeout(() => setLastMotion(null), 2000);
53-
return () => clearTimeout(timer);
54-
}
55-
}, [lastMotion]);
56-
57-
// FPS 更新
58-
useEffect(() => {
59-
if (!isCameraOn) return;
60-
const interval = setInterval(() => {
61-
setFps(visionService.getFps());
62-
}, 1000);
63-
return () => clearInterval(interval);
64-
}, [isCameraOn]);
65-
66-
// 处理摄像头开关
67-
const handleToggleCamera = useCallback(async () => {
68-
if (isCameraOn) {
69-
visionService.stop();
70-
setIsCameraOn(false);
71-
setCurrentEmotion('neutral');
72-
onEmotionChange('neutral');
73-
setLastMotion(null);
74-
setFps(0);
75-
setError(null);
76-
} else {
77-
if (videoRef.current) {
78-
setIsLoading(true);
79-
setError(null);
80-
81-
const success = await visionService.start(
82-
videoRef.current,
83-
(emotion) => {
84-
setCurrentEmotion(emotion);
85-
onEmotionChange(emotion);
86-
},
87-
(motion) => {
88-
setLastMotion(motion);
89-
toast.info(`检测到动作: ${motion}`);
90-
onHeadMotion?.(motion);
91-
},
92-
);
93-
94-
setIsLoading(false);
95-
96-
if (success) {
97-
setIsCameraOn(true);
98-
toast.success('摄像头已启动');
99-
} else {
100-
setError('摄像头启动失败,请检查权限设置');
101-
toast.error('摄像头启动失败');
102-
}
103-
}
104-
}
105-
}, [isCameraOn, onEmotionChange, onHeadMotion]);
33+
const vision = useVisionMirror({
34+
onEmotionChange,
35+
onHeadMotion: (motion) => {
36+
toast.info(`检测到动作: ${motion}`);
37+
onHeadMotion?.(motion);
38+
},
39+
});
10640

10741
return (
10842
<div className="space-y-4">
10943
<div className="flex items-center justify-between">
11044
<h3 className="text-sm font-medium text-white">视觉镜像</h3>
11145
<div className="flex items-center space-x-3">
112-
{isCameraOn && fps > 0 && (
113-
<span className="text-[10px] text-white/40 font-mono">{fps} FPS</span>
46+
{vision.isCameraOn && vision.fps > 0 && (
47+
<span className="text-[10px] text-white/40 font-mono">{vision.fps} FPS</span>
11448
)}
11549
<div className="flex items-center space-x-2">
11650
<div
11751
className={`w-1.5 h-1.5 rounded-full ${
118-
isLoading
52+
vision.isLoading
11953
? 'bg-yellow-500 animate-pulse'
120-
: isCameraOn
54+
: vision.isCameraOn
12155
? 'bg-red-500 animate-pulse'
12256
: 'bg-white/20'
12357
}`}
12458
/>
12559
<span className="text-xs text-white/60">
126-
{isLoading ? '启动中' : isCameraOn ? 'LIVE' : '离线'}
60+
{vision.isLoading ? '启动中' : vision.isCameraOn ? 'LIVE' : '离线'}
12761
</span>
12862
</div>
12963
</div>
13064
</div>
13165

13266
<div className="relative aspect-video bg-black/50 rounded-xl overflow-hidden border border-white/10 shadow-inner">
13367
{/* 加载状态 */}
134-
{isLoading && (
68+
{vision.isLoading && (
13569
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/60 z-10">
13670
<Loader2 size={32} className="text-blue-400 animate-spin mb-2" />
13771
<span className="text-xs text-white/60">正在启动摄像头...</span>
13872
</div>
13973
)}
14074

14175
{/* 错误状态 */}
142-
{error && !isLoading && (
76+
{vision.error && !vision.isLoading && (
14377
<div className="absolute inset-0 flex flex-col items-center justify-center bg-red-900/20 z-10">
14478
<AlertCircle size={32} className="text-red-400 mb-2" />
145-
<span className="text-xs text-red-300 text-center px-4">{error}</span>
79+
<span className="text-xs text-red-300 text-center px-4">{vision.error}</span>
14680
</div>
14781
)}
14882

14983
{/* 离线状态 */}
150-
{!isCameraOn && !isLoading && !error && (
84+
{!vision.isCameraOn && !vision.isLoading && !vision.error && (
15185
<div className="absolute inset-0 flex flex-col items-center justify-center text-white/20">
15286
<ScanFace size={48} className="mb-2" />
15387
<span className="text-xs uppercase tracking-widest">摄像头未开启</span>
15488
</div>
15589
)}
15690

15791
<video
158-
ref={videoRef}
159-
className={`w-full h-full object-cover transition-opacity ${isCameraOn ? 'opacity-100' : 'opacity-0'} transform scale-x-[-1]`}
92+
ref={vision.videoRef}
93+
className={`w-full h-full object-cover transition-opacity ${vision.isCameraOn ? 'opacity-100' : 'opacity-0'} transform scale-x-[-1]`}
16094
autoPlay
16195
playsInline
16296
muted
16397
/>
16498

165-
{isCameraOn && (
99+
{vision.isCameraOn && (
166100
<>
167101
<div className="absolute top-2 right-2 px-2 py-1 bg-black/60 backdrop-blur rounded text-[10px] text-white/80 border border-white/10">
168102
AI 追踪中
169103
</div>
170-
{lastMotion && (
104+
{vision.lastMotion && (
171105
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 px-3 py-1 bg-blue-500/80 backdrop-blur rounded-full text-xs text-white font-medium animate-fade-in-up">
172-
检测到: {lastMotion.toUpperCase()}
106+
检测到: {vision.lastMotion.toUpperCase()}
173107
</div>
174108
)}
175109
</>
@@ -179,29 +113,31 @@ export default function VisionMirrorPanel({
179113
<div className="flex justify-between items-center">
180114
<div className="text-xs text-white/60">
181115
检测情感:
182-
<span className={`ml-2 font-medium ${EMOTION_COLORS[currentEmotion]}`}>
183-
{EMOTION_LABELS[currentEmotion]}
116+
<span className={`ml-2 font-medium ${EMOTION_COLORS[vision.currentEmotion]}`}>
117+
{EMOTION_LABELS[vision.currentEmotion]}
184118
</span>
185119
</div>
186120

187121
<button
188-
onClick={handleToggleCamera}
189-
disabled={isLoading}
122+
onClick={vision.toggleCamera}
123+
disabled={vision.isLoading}
190124
className={`flex items-center space-x-2 px-4 py-2 rounded-lg text-xs font-medium transition-all border disabled:opacity-50 disabled:cursor-not-allowed ${
191-
isCameraOn
125+
vision.isCameraOn
192126
? 'bg-red-500/20 text-red-400 border-red-500/50 hover:bg-red-500/30'
193127
: 'bg-blue-500/20 text-blue-400 border-blue-500/50 hover:bg-blue-500/30'
194128
}`}
195-
aria-label={isCameraOn ? '关闭摄像头' : '开启摄像头'}
129+
aria-label={vision.isCameraOn ? '关闭摄像头' : '开启摄像头'}
196130
>
197-
{isLoading ? (
131+
{vision.isLoading ? (
198132
<Loader2 size={14} className="animate-spin" />
199-
) : isCameraOn ? (
133+
) : vision.isCameraOn ? (
200134
<CameraOff size={14} />
201135
) : (
202136
<Camera size={14} />
203137
)}
204-
<span>{isLoading ? '启动中...' : isCameraOn ? '关闭摄像头' : '开启摄像头'}</span>
138+
<span>
139+
{vision.isLoading ? '启动中...' : vision.isCameraOn ? '关闭摄像头' : '开启摄像头'}
140+
</span>
205141
</button>
206142
</div>
207143
</div>

0 commit comments

Comments
 (0)