1- /**
2- * DigitalWellbeingOverlay — 休息活动建议浮层
3- *
4- * @ai -context: 数字养生守门人 L3 弹出——显示休息活动建议(拉伸/远眺/呼吸/散步),
5- * 15 秒自动消失,framber-motion 淡入淡出。
6- */
7- import { useState , useEffect , useCallback } from 'react' ;
8- import { motion , AnimatePresence } from 'framer-motion' ;
9- import { useReducedMotion } from '@/hooks/useReducedMotion' ;
10-
11- interface RestActivity {
12- id : string ;
13- label : string ;
14- emoji : string ;
15- duration : number ;
16- }
17-
18- interface DigitalWellbeingOverlayProps {
19- show : boolean ;
20- restActivities : RestActivity [ ] ;
21- isLocked : boolean ;
22- onDismiss : ( ) => void ;
23- }
24-
25- export function DigitalWellbeingOverlay ( { show, restActivities, isLocked, onDismiss } : DigitalWellbeingOverlayProps ) {
26- const prefersReduced = useReducedMotion ( ) ;
27- const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
28-
29- // 自动轮换活动建议
30- useEffect ( ( ) => {
31- if ( ! show ) return ;
32- const interval = setInterval ( ( ) => {
33- setCurrentIndex ( prev => ( prev + 1 ) % restActivities . length ) ;
34- } , 30_000 ) ;
35- return ( ) => clearInterval ( interval ) ;
36- } , [ show , restActivities . length ] ) ;
37-
38- const current = restActivities [ currentIndex ] ;
39-
40- return (
41- < AnimatePresence >
42- { show && (
43- < motion . div
44- className = "fixed inset-0 z-50 flex flex-col items-center justify-center"
45- initial = { { opacity : 0 } }
46- animate = { { opacity : 1 } }
47- exit = { { opacity : 0 } }
48- transition = { { duration : prefersReduced ? 0 : 0.6 } }
49- >
50- < div className = "absolute inset-0 bg-black/20 backdrop-blur-sm" onClick = { onDismiss } />
51-
52- < motion . div
53- className = "relative flex flex-col items-center gap-5 px-8 py-8 rounded-2xl bg-bg-secondary/80 backdrop-blur-md border border-border/40 shadow-kb-lg max-w-sm mx-4"
54- initial = { { scale : 0.9 , y : 20 , opacity : 0 } }
55- animate = { { scale : 1 , y : 0 , opacity : 1 } }
56- exit = { { scale : 0.9 , y : 20 , opacity : 0 } }
57- transition = { { duration : prefersReduced ? 0 : 0.4 , ease : 'easeOut' } }
58- >
59- < div className = "text-center" >
60- < span className = "text-4xl block mb-2" > { current ?. emoji || '🧘' } </ span >
61- < h3 className = "text-lg font-medium text-text-primary" >
62- { isLocked ? '休息时间' : '活动建议' }
63- </ h3 >
64- < p className = "text-sm text-text-secondary mt-1" >
65- { current ?. label || '站立拉伸' }
66- </ p >
67- { current && (
68- < p className = "text-xs text-text-tertiary mt-1" >
69- 建议时长:{ current . duration >= 60
70- ? `${ Math . floor ( current . duration / 60 ) } 分钟`
71- : `${ current . duration } 秒` }
72- </ p >
73- ) }
74- </ div >
75-
76- { /* 其他活动预览 */ }
77- < div className = "flex gap-2 flex-wrap justify-center" >
78- { restActivities . map ( ( a , i ) => (
79- < span
80- key = { a . id }
81- className = { `text-xs px-2 py-1 rounded-full border transition-colors ${
82- i === currentIndex
83- ? 'border-brand-500/50 bg-brand-500/10 text-brand-600'
84- : 'border-border/30 text-text-tertiary'
85- } `}
86- >
87- { a . emoji } { a . label }
88- </ span >
89- ) ) }
90- </ div >
91-
92- { /* 锁定提示 */ }
93- { isLocked && (
94- < p className = "text-xs text-text-tertiary text-center px-4 py-2 bg-bg-tertiary/50 rounded-lg" >
95- 5 分钟休息锁定,时间到后自动解锁
96- </ p >
97- ) }
98-
99- < button
100- onClick = { onDismiss }
101- className = "px-6 py-2 rounded-full bg-brand-500/80 hover:bg-brand-500 text-white text-sm font-medium transition-colors"
102- >
103- 已休息好
104- </ button >
105- </ motion . div >
106- </ motion . div >
107- ) }
108- </ AnimatePresence >
109- ) ;
110- }
111-
112- // 纯函数版本:获取默认休息活动列表
113- export function getDefaultRestActivities ( ) : RestActivity [ ] {
114- return [
115- { id : 'stretch' , label : '站立拉伸' , emoji : '🧘' , duration : 120 } ,
116- { id : 'look-far' , label : '远眺 20 秒' , emoji : '🌳' , duration : 20 } ,
117- { id : 'breathe' , label : '深呼吸 4-7-8' , emoji : '🌬️' , duration : 60 } ,
118- { id : 'walk' , label : '散步 5 分钟' , emoji : '🚶' , duration : 300 } ,
119- ] ;
120- }
1+ /**
2+ * DigitalWellbeingOverlay — 休息活动建议浮层
3+ *
4+ * @ai -context: 数字养生守门人 L3 弹出——显示休息活动建议(拉伸/远眺/呼吸/散步),
5+ * 15 秒自动消失,framber-motion 淡入淡出。
6+ */
7+ import { useState , useEffect } from 'react' ;
8+ import { motion , AnimatePresence } from 'framer-motion' ;
9+ import { useReducedMotion } from '@/hooks/useReducedMotion' ;
10+ import type { RestActivity } from './restActivities' ;
11+
12+ // react-refresh: 组件文件只导出组件;getDefaultRestActivities 已移至 ./restActivities,
13+ // 此处 re-export 保持 '@/components/DigitalWellbeingOverlay' 导出签名不变
14+ // oxlint-disable-next-line react/only-export-components
15+ export { getDefaultRestActivities } from './restActivities' ;
16+
17+ interface DigitalWellbeingOverlayProps {
18+ show : boolean ;
19+ restActivities : RestActivity [ ] ;
20+ isLocked : boolean ;
21+ onDismiss : ( ) => void ;
22+ }
23+
24+ export function DigitalWellbeingOverlay ( { show, restActivities, isLocked, onDismiss } : DigitalWellbeingOverlayProps ) {
25+ const prefersReduced = useReducedMotion ( ) ;
26+ const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
27+
28+ // 自动轮换活动建议
29+ useEffect ( ( ) => {
30+ if ( ! show ) return ;
31+ const interval = setInterval ( ( ) => {
32+ setCurrentIndex ( prev => ( prev + 1 ) % restActivities . length ) ;
33+ } , 30_000 ) ;
34+ return ( ) => clearInterval ( interval ) ;
35+ } , [ show , restActivities . length ] ) ;
36+
37+ const current = restActivities [ currentIndex ] ;
38+
39+ return (
40+ < AnimatePresence >
41+ { show && (
42+ < motion . div
43+ className = "fixed inset-0 z-50 flex flex-col items-center justify-center"
44+ initial = { { opacity : 0 } }
45+ animate = { { opacity : 1 } }
46+ exit = { { opacity : 0 } }
47+ transition = { { duration : prefersReduced ? 0 : 0.6 } }
48+ >
49+ < div className = "absolute inset-0 bg-black/20 backdrop-blur-sm" onClick = { onDismiss } />
50+
51+ < motion . div
52+ className = "relative flex flex-col items-center gap-5 px-8 py-8 rounded-2xl bg-bg-secondary/80 backdrop-blur-md border border-border/40 shadow-kb-lg max-w-sm mx-4"
53+ initial = { { scale : 0.9 , y : 20 , opacity : 0 } }
54+ animate = { { scale : 1 , y : 0 , opacity : 1 } }
55+ exit = { { scale : 0.9 , y : 20 , opacity : 0 } }
56+ transition = { { duration : prefersReduced ? 0 : 0.4 , ease : 'easeOut' } }
57+ >
58+ < div className = "text-center" >
59+ < span className = "text-4xl block mb-2" > { current ?. emoji || '🧘' } </ span >
60+ < h3 className = "text-lg font-medium text-text-primary" >
61+ { isLocked ? '休息时间' : '活动建议' }
62+ </ h3 >
63+ < p className = "text-sm text-text-secondary mt-1" >
64+ { current ?. label || '站立拉伸' }
65+ </ p >
66+ { current && (
67+ < p className = "text-xs text-text-tertiary mt-1" >
68+ 建议时长:{ current . duration >= 60
69+ ? `${ Math . floor ( current . duration / 60 ) } 分钟`
70+ : `${ current . duration } 秒` }
71+ </ p >
72+ ) }
73+ </ div >
74+
75+ { /* 其他活动预览 */ }
76+ < div className = "flex gap-2 flex-wrap justify-center" >
77+ { restActivities . map ( ( a , i ) => (
78+ < span
79+ key = { a . id }
80+ className = { `text-xs px-2 py-1 rounded-full border transition-colors ${
81+ i === currentIndex
82+ ? 'border-brand-500/50 bg-brand-500/10 text-brand-600'
83+ : 'border-border/30 text-text-tertiary'
84+ } `}
85+ >
86+ { a . emoji } { a . label }
87+ </ span >
88+ ) ) }
89+ </ div >
90+
91+ { /* 锁定提示 */ }
92+ { isLocked && (
93+ < p className = "text-xs text-text-tertiary text-center px-4 py-2 bg-bg-tertiary/50 rounded-lg" >
94+ 5 分钟休息锁定,时间到后自动解锁
95+ </ p >
96+ ) }
97+
98+ < button
99+ onClick = { onDismiss }
100+ className = "px-6 py-2 rounded-full bg-brand-500/80 hover:bg-brand-500 text-white text-sm font-medium transition-colors"
101+ >
102+ 已休息好
103+ </ button >
104+ </ motion . div >
105+ </ motion . div >
106+ ) }
107+ </ AnimatePresence >
108+ ) ;
109+ }
0 commit comments