44 * 引力场布局:控件围绕生物下缘的固定半圆弧轨道排布,位置由固定角度槽位
55 * 三角函数静态计算(不随悬停漂移),平时低透明度(0.6)常驻可见,悬停浮现至 1。
66 *
7+ * 定位根治:轨道半径使用容器尺寸百分比(ORBIT_RATIO),不依赖 px 测量——
8+ * 容器尺寸任何变化(视口 vmin 波动等)按钮平滑跟随而非跳变,零测量零跳动。
9+ *
710 * 交互收敛(主交互 = 点击时间生物):
811 * - 主按钮已清除:开始/暂停/继续由生物点击承担(热启动直接迈步、专注态暂停/恢复)
9- * - 四颗卫星:重置 / 跳过 / 沉浸 / 白噪音(弧端小图标下拉)
12+ * - 四颗卫星:重置 / 跳过(统一 skipStage) / 沉浸 / 白噪音(弧端小图标下拉)
1013 *
11- * @ai -context: 状态取自 store;唯一外部依赖是 orbitRadius(父组件按生物尺寸传入) 。
14+ * @ai -context: 状态取自 store;定位由百分比驱动,无外部尺寸依赖 。
1215 */
1316import { useState , useRef , useEffect } from 'react' ;
1417import { motion , AnimatePresence } from 'framer-motion' ;
@@ -20,25 +23,23 @@ import { useAudioPrefsStore } from '@/lib/audio/audioPrefsStore';
2023import { audioTracks } from '@/lib/audio/audioConfig' ;
2124import { useEstimatedVolume } from '../hooks/useEstimatedVolume' ;
2225
23- interface PomodoroControlsProps {
24- /** 卫星轨道半径(px)= 生物半径 × 1.45,父组件按生物尺寸计算 */
25- orbitRadius : number ;
26- }
27-
2826/** 固定角度槽位(度):弧顶 90° 让位给生物主交互,四卫星对称分布于两侧弧 */
2927const SAT = {
3028 reset : 135 , skip : 45 , immersive : 165 , noise : 15 ,
3129} as const ;
3230
31+ /** 轨道半径比例:容器尺寸的 72.5%(= 半宽 × 1.45),百分比定位随容器平滑跟随 */
32+ const ORBIT_RATIO = 0.725 ;
33+
3334/** 卫星平时透明度(常驻可见,悬停浮现至 1) */
3435const SAT_IDLE_OPACITY = 0.6 ;
3536
36- /** 角度 → 绝对定位 (圆心 = 容器中心,混合 calc 保持响应式 ) */
37- function satPos ( angleDeg : number , radius : number ) : React . CSSProperties {
37+ /** 角度 → 百分比定位 (圆心 = 容器中心;百分比相对容器,任何尺寸变化平滑跟随 ) */
38+ function satPos ( angleDeg : number ) : React . CSSProperties {
3839 const rad = ( angleDeg * Math . PI ) / 180 ;
3940 return {
40- left : `calc(50% + ${ Math . cos ( rad ) * radius } px )` ,
41- top : `calc(50% + ${ Math . sin ( rad ) * radius } px )` ,
41+ left : `calc(50% + ${ ( Math . cos ( rad ) * ORBIT_RATIO * 100 ) . toFixed ( 3 ) } % )` ,
42+ top : `calc(50% + ${ ( Math . sin ( rad ) * ORBIT_RATIO * 100 ) . toFixed ( 3 ) } % )` ,
4243 } ;
4344}
4445
@@ -47,14 +48,14 @@ function satClass(...classes: string[]): string {
4748 return cn ( 'absolute -translate-x-1/2 -translate-y-1/2 flex items-center justify-center' , ...classes ) ;
4849}
4950
50- export function PomodoroControls ( { orbitRadius } : PomodoroControlsProps ) {
51+ export function PomodoroControls ( ) {
5152 // P0-1 细粒度订阅:整 store 订阅会在任何字段变化时重渲染卫星控制区
5253 const isRunning = usePomodoroStore ( ( s ) => s . isRunning ) ;
5354 const isPaused = usePomodoroStore ( ( s ) => s . isPaused ) ;
5455 const isArmed = usePomodoroStore ( ( s ) => s . isArmed ) ;
5556 // 动作(稳定引用)
5657 const reset = usePomodoroStore ( ( s ) => s . reset ) ;
57- const skip = usePomodoroStore ( ( s ) => s . skip ) ;
58+ const skipStage = usePomodoroStore ( ( s ) => s . skipStage ) ;
5859 const enterImmersive = usePomodoroStore ( ( s ) => s . enterImmersive ) ;
5960 // 沉睡态(未激活未运行):沉浸入口常驻但禁用(先开始专注才能进入沉浸)
6061 const isAsleep = ! isRunning && ! isPaused && ! isArmed ;
@@ -85,9 +86,9 @@ export function PomodoroControls({ orbitRadius }: PomodoroControlsProps) {
8586 } , [ trackPickerOpen ] ) ;
8687
8788 return (
88- < div className = "pointer-events-none" >
89+ < >
8990 { /* ── 白噪音卫星(弧右下)── */ }
90- < div ref = { pickerRef } className = "pointer-events-auto absolute z-10" style = { satPos ( SAT . noise , orbitRadius ) } >
91+ < div ref = { pickerRef } className = "pointer-events-auto absolute z-10" style = { satPos ( SAT . noise ) } >
9192 < Tip text = { whiteNoiseEnabled ? '关闭背景音' : '开启背景音' } >
9293 < motion . button
9394 whileTap = { { scale : 0.9 } }
@@ -148,10 +149,10 @@ export function PomodoroControls({ orbitRadius }: PomodoroControlsProps) {
148149 </ div >
149150
150151 { /* ── 重置卫星(弧左下)── */ }
151- < div className = "pointer-events-auto absolute z-10" style = { satPos ( SAT . reset , orbitRadius ) } >
152+ < div className = "pointer-events-auto absolute z-10" style = { satPos ( SAT . reset ) } >
152153 < Tip text = "重置计时器(回沉睡)" >
153154 < motion . button
154- whileTap = { { scale : 0.9 , rotate : - 180 } }
155+ whileTap = { { scale : 0.9 } }
155156 onClick = { reset }
156157 className = { satClass ( 'w-9 h-9 rounded-full border border-border/30 bg-bg-elevated/60 backdrop-blur-sm text-text-tertiary hover:text-text-secondary transition-colors' ) }
157158 animate = { { opacity : SAT_IDLE_OPACITY } }
@@ -162,12 +163,12 @@ export function PomodoroControls({ orbitRadius }: PomodoroControlsProps) {
162163 </ Tip >
163164 </ div >
164165
165- { /* ── 跳过卫星(弧右下)── */ }
166- < div className = "pointer-events-auto absolute z-10" style = { satPos ( SAT . skip , orbitRadius ) } >
166+ { /* ── 跳过卫星(弧右下):统一 skipStage(呼吸态→跳过呼吸进专注,其他→常规跳过) ── */ }
167+ < div className = "pointer-events-auto absolute z-10" style = { satPos ( SAT . skip ) } >
167168 < Tip text = "跳过当前阶段" >
168169 < motion . button
169- whileTap = { { scale : 0.9 , x : 3 } }
170- onClick = { skip }
170+ whileTap = { { scale : 0.9 } }
171+ onClick = { skipStage }
171172 className = { satClass ( 'w-9 h-9 rounded-full border border-border/30 bg-bg-elevated/60 backdrop-blur-sm text-text-tertiary hover:text-text-secondary transition-colors' ) }
172173 animate = { { opacity : SAT_IDLE_OPACITY } }
173174 whileHover = { { opacity : 1 } }
@@ -178,7 +179,7 @@ export function PomodoroControls({ orbitRadius }: PomodoroControlsProps) {
178179 </ div >
179180
180181 { /* ── 沉浸卫星(弧更左下,常驻显示;沉睡态禁用,先开始专注才能进入沉浸)── */ }
181- < div className = "pointer-events-auto absolute z-10" style = { satPos ( SAT . immersive , orbitRadius ) } >
182+ < div className = "pointer-events-auto absolute z-10" style = { satPos ( SAT . immersive ) } >
182183 < Tip text = { isAsleep ? '先开始专注(点击时间生物)' : '进入专注模式' } >
183184 < motion . button
184185 whileTap = { isAsleep ? undefined : { scale : 0.9 } }
@@ -197,6 +198,6 @@ export function PomodoroControls({ orbitRadius }: PomodoroControlsProps) {
197198 </ motion . button >
198199 </ Tip >
199200 </ div >
200- </ div >
201+ </ >
201202 ) ;
202203}
0 commit comments