1- import { useEffect , useRef , useState , useCallback } from 'react' ;
2- import { visionService } from '../core/vision/visionService' ;
1+ import { useVisionMirror } from '../hooks/useVisionMirror' ;
32import type { UserEmotion } from '../core/vision/visionMapper' ;
43import { Camera , CameraOff , ScanFace , AlertCircle , Loader2 } from 'lucide-react' ;
54import { 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