Skip to content

Commit 807a068

Browse files
committed
refactor(client): lint 清零并收敛 store 订阅与工具函数
1 parent c7b185d commit 807a068

122 files changed

Lines changed: 944 additions & 562 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎client/src/App.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ startPomodoroScheduler();
5454
// 番茄钟持久化初始化:此前 initialize 仅在设置页挂载时触发(PomodoroSettingsPage),
5555
// 深潜主页启动不加载——用户设置的时长/预设刷新后回退默认值(25 分钟),
5656
// 且完成的番茄按默认时长落库(学习脉搏数据失真)。App 启动即初始化,幂等安全。
57-
usePomodoroStore.getState().initialize().catch(() => {});
57+
usePomodoroStore.getState().initialize().catch((err) => {
58+
console.debug('[App] pomodoro store initialize failed', err);
59+
});
5860

5961
const queryClient = new QueryClient({
6062
defaultOptions: {
@@ -121,7 +123,9 @@ function App() {
121123
// 同步网关地址到主进程(主进程无法访问 localStorage,需显式传递)
122124
const aiConfig = getAIConfig();
123125
if (aiConfig.gatewayUrl && window.electronAPI) {
124-
window.electronAPI.invoke('ai:set-gateway-url', aiConfig.gatewayUrl).catch(() => {});
126+
window.electronAPI.invoke('ai:set-gateway-url', aiConfig.gatewayUrl).catch((err) => {
127+
console.debug('[App] set gateway url failed', err);
128+
});
125129
}
126130
import('./hooks/useAIGatewayHealth').then(({ precheckGatewayHealth }) => {
127131
precheckGatewayHealth();
Lines changed: 109 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,109 @@
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+
}
Lines changed: 38 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,38 @@
1-
/**
2-
* EyeCareMode — 护眼模式 CSS 滤镜覆盖层
3-
*
4-
* @ai-context: 数字养生守门人护眼模式——CSS 滤镜覆盖(sepia/warm tint),
5-
* 可开关切换,通过 CSS custom properties 实现,不影响交互。
6-
*/
7-
import { useEffect } from 'react';
8-
9-
/** 护眼模式 CSS 变量名 */
10-
const EYE_CARE_VAR = '--ed-eye-care-filter';
11-
12-
/**
13-
* 护眼模式效果:暖色调 sepia 滤镜
14-
* 通过 CSS 自定义属性应用到根元素,避免 DOM 侵入
15-
*/
16-
export function applyEyeCareFilter(enabled: boolean): void {
17-
const root = document.documentElement;
18-
if (enabled) {
19-
root.style.setProperty(EYE_CARE_VAR, 'sepia(0.15) hue-rotate(-10deg) brightness(0.95)');
20-
} else {
21-
root.style.removeProperty(EYE_CARE_VAR);
22-
}
23-
}
24-
25-
/**
26-
* 检查护眼模式当前是否启用
27-
*/
28-
export function isEyeCareActive(): boolean {
29-
const root = document.documentElement;
30-
return root.style.getPropertyValue(EYE_CARE_VAR) !== '';
31-
}
32-
33-
/**
34-
* 护眼模式管理 Hook
35-
* 在 body 上应用 CSS 滤镜,toggle 开关
36-
*/
37-
export function useEyeCareMode(enabled: boolean): void {
38-
useEffect(() => {
39-
applyEyeCareFilter(enabled);
40-
return () => {
41-
applyEyeCareFilter(false);
42-
};
43-
}, [enabled]);
44-
}
45-
46-
/**
47-
* EyeCareMode 样式注入组件
48-
* 在应用根组件挂载一次,提供 CSS 变量到 body 的映射
49-
*/
50-
export function EyeCareModeStyle() {
51-
useEffect(() => {
52-
// 注入 CSS 规则:将 CSS 变量映射到 body 滤镜
53-
const style = document.createElement('style');
54-
style.id = 'eye-care-mode-style';
55-
style.textContent = `
56-
body {
57-
filter: var(${EYE_CARE_VAR}, none);
58-
transition: filter 0.8s ease;
59-
}
60-
`;
61-
document.head.appendChild(style);
62-
return () => {
63-
const existing = document.getElementById('eye-care-mode-style');
64-
if (existing) existing.remove();
65-
};
66-
}, []);
67-
68-
return null;
69-
}
1+
/**
2+
* EyeCareMode — 护眼模式 CSS 滤镜覆盖层
3+
*
4+
* @ai-context: 数字养生守门人护眼模式——CSS 滤镜覆盖(sepia/warm tint),
5+
* 可开关切换,通过 CSS custom properties 实现,不影响交互。
6+
*/
7+
import { useEffect } from 'react';
8+
import { EYE_CARE_VAR } from './eyeCareModeUtils';
9+
10+
// react-refresh: 组件文件只导出组件;工具/hook 已移至 ./eyeCareModeUtils,
11+
// 此处 re-export 保持 '@/components/EyeCareMode' 导出签名不变(App.tsx 直接 import)
12+
// oxlint-disable-next-line react/only-export-components
13+
export { applyEyeCareFilter, isEyeCareActive, useEyeCareMode } from './eyeCareModeUtils';
14+
15+
/**
16+
* EyeCareMode 样式注入组件
17+
* 在应用根组件挂载一次,提供 CSS 变量到 body 的映射
18+
*/
19+
export function EyeCareModeStyle() {
20+
useEffect(() => {
21+
// 注入 CSS 规则:将 CSS 变量映射到 body 滤镜
22+
const style = document.createElement('style');
23+
style.id = 'eye-care-mode-style';
24+
style.textContent = `
25+
body {
26+
filter: var(${EYE_CARE_VAR}, none);
27+
transition: filter 0.8s ease;
28+
}
29+
`;
30+
document.head.appendChild(style);
31+
return () => {
32+
const existing = document.getElementById('eye-care-mode-style');
33+
if (existing) existing.remove();
34+
};
35+
}, []);
36+
37+
return null;
38+
}

‎client/src/components/InfographicRenderer.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* @ai-context: 渲染标题、带要点的段落、关系连接节点图。
55
* 支持 3 种主题:academic(学术衬线)、tech(技术等宽蓝)、warm(圆润手绘感)。
66
*/
7-
import { useMemo } from 'react';
87
import { cn } from '@/lib/utils';
98
import type { InfographicData } from '@/lib/ai/types';
109

‎client/src/components/ReadingGuide.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function ReadingGuide({
9191
window.removeEventListener('resize', updatePosition);
9292
if (rafRef.current) cancelAnimationFrame(rafRef.current);
9393
};
94-
}, [enabled, updatePosition]);
94+
}, [enabled, updatePosition, containerSelector]);
9595

9696
// 如果减弱动效,不显示引导线
9797
if (prefersReduced) return null;

0 commit comments

Comments
 (0)