Skip to content

Commit 9958306

Browse files
committed
fix: App启动初始化番茄钟+设置页返回按钮+API本地模式401不踢登录+批量选择hook
1 parent af9a867 commit 9958306

5 files changed

Lines changed: 112 additions & 7 deletions

File tree

‎client/src/App.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,19 @@ import { WorldRecap } from '@/features/retention/components/WorldRecap';
3535
import { WorldSoundscape } from '@/lib/audio/WorldSoundscape';
3636
import '@/stores/useSettingsStore'; // 导入以触发音效设置初始化
3737
import { startPomodoroScheduler } from '@/features/pomodoro/lib/pomodoroScheduler';
38+
import { usePomodoroStore } from '@/features/pomodoro/store/usePomodoroStore';
3839

3940
// 启动时预加载所有音效(不阻塞渲染)
4041
soundPlayer.preloadAll();
4142

4243
// 番茄钟全局计时调度器:tick 驱动与页面生命周期解耦(切页后计时不停摆)
4344
startPomodoroScheduler();
4445

46+
// 番茄钟持久化初始化:此前 initialize 仅在设置页挂载时触发(PomodoroSettingsPage),
47+
// 深潜主页启动不加载——用户设置的时长/预设刷新后回退默认值(25 分钟),
48+
// 且完成的番茄按默认时长落库(学习脉搏数据失真)。App 启动即初始化,幂等安全。
49+
usePomodoroStore.getState().initialize().catch(() => {});
50+
4551
const queryClient = new QueryClient({
4652
defaultOptions: {
4753
queries: {

‎client/src/features/beta/LicenseActivation.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Key, Check, AlertTriangle, Sparkles, ExternalLink } from 'lucide-react'
1010
import { Button, Input, useToast } from '@/components/ui';
1111
import { cn } from '@/lib/utils';
1212
import { useBetaStore } from '@/features/beta/betaStore';
13-
import { type License, type LicenseType } from '@/types/beta';
13+
import { type License, type LicenseType, type UserTier } from '@/types/beta';
1414
import { useAuth } from '@/lib/auth/AuthContext';
1515

1616
/** 激活码格式校验正则 */

‎client/src/features/pomodoro/pages/PomodoroSettingsPage.tsx‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { useState, useEffect, useRef } from 'react';
55
import { useNavigate } from 'react-router-dom';
66
import { motion } from 'framer-motion';
7-
import { Timer, Zap, Bell, Save, RotateCcw, GraduationCap, Sparkles, CheckCircle2, Music, Volume2, Brain, Plus, Pencil, Trash2, Layers, ChevronUp, ChevronDown } from 'lucide-react';
7+
import { Timer, Zap, Bell, Save, RotateCcw, GraduationCap, Sparkles, CheckCircle2, Music, Volume2, Brain, Plus, Pencil, Trash2, Layers, ChevronUp, ChevronDown, ArrowLeft } from 'lucide-react';
88
import { AIThinkingIndicator } from '@/components/ui/AIThinkingIndicator';
99
import { Button, Card, Input, useToast } from '@/components/ui';
1010
import { cn } from '@/lib/utils';
@@ -196,10 +196,25 @@ export default function PomodoroSettingsPage() {
196196
animate="visible"
197197
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.07, delayChildren: 0.06 } } }}
198198
>
199-
<motion.h1
200-
className="text-h1 font-semibold text-text-primary mb-kb-lg"
199+
<motion.div
200+
className="flex items-center gap-2 mb-kb-lg"
201201
variants={{ hidden: { opacity: 0, y: -12, scale: 0.97 }, visible: { opacity: 1, y: 0, transition: { duration: 0.35 } } }}
202-
>深潜设置</motion.h1>
202+
>
203+
{/* 返回深潜模块(内测反馈:设置页此前无出口,只能 Esc 退到 3D 场景) */}
204+
<Tip text="返回深潜">
205+
<button
206+
onClick={() => navigate('/pomodoro')}
207+
aria-label="返回深潜"
208+
className="p-1.5 rounded-kb-full text-text-tertiary hover:text-text-primary hover:bg-bg-tertiary transition-all duration-kb-fast"
209+
>
210+
<ArrowLeft className="w-icon-md h-icon-md" strokeWidth={1.5} />
211+
</button>
212+
</Tip>
213+
<motion.h1
214+
className="text-h1 font-semibold text-text-primary"
215+
variants={{ hidden: { opacity: 0, y: -12, scale: 0.97 }, visible: { opacity: 1, y: 0, transition: { duration: 0.35 } } }}
216+
>深潜设置</motion.h1>
217+
</motion.div>
203218

204219
{/* Duration settings */}
205220
<motion.div variants={{ hidden: { opacity: 0, y: 16, scale: 0.97 }, visible: { opacity: 1, y: 0, transition: { duration: 0.35 } } }}>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* 批量选择状态 hook(萤火海沟批量整理模式的通用化)
3+
*
4+
* @ai-context: 统一 batchMode + selectedIds 状态机,供笔记列表 / 闪卡列表等
5+
* 需要批量操作的模块复用。退出批量模式时自动清空选中;toggle 支持
6+
* shiftKey 语义由调用方自行决定(默认单选切换)。
7+
* @ai-context: Shared batch-selection state machine, extracted from the
8+
* Inspiration batch mode; auto-clears selection when batch mode is exited.
9+
*/
10+
import { useCallback, useEffect, useMemo, useState } from 'react';
11+
12+
interface UseBatchSelectionOptions<T extends { id: string }> {
13+
/** 参与全选的候选列表(全选基于当前可见/过滤结果) */
14+
items: T[];
15+
/** 进入批量模式时是否自动全选 */
16+
selectAllOnEnter?: boolean;
17+
}
18+
19+
export function useBatchSelection<T extends { id: string }>({
20+
items,
21+
selectAllOnEnter = false,
22+
}: UseBatchSelectionOptions<T>) {
23+
const [batchMode, setBatchMode] = useState(false);
24+
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
25+
26+
// 退出批量模式时清空选中(与萤火海沟行为一致)
27+
useEffect(() => {
28+
if (!batchMode) setSelectedIds(new Set());
29+
}, [batchMode]);
30+
31+
// 进入批量模式时可选全选
32+
useEffect(() => {
33+
if (batchMode && selectAllOnEnter) {
34+
setSelectedIds(new Set(items.map((i) => i.id)));
35+
}
36+
// eslint-disable-next-line react-hooks/exhaustive-deps
37+
}, [batchMode, selectAllOnEnter]);
38+
39+
const toggle = useCallback((id: string) => {
40+
setSelectedIds((prev) => {
41+
const next = new Set(prev);
42+
if (next.has(id)) {
43+
next.delete(id);
44+
} else {
45+
next.add(id);
46+
}
47+
return next;
48+
});
49+
}, []);
50+
51+
const selectAll = useCallback(() => {
52+
setSelectedIds(new Set(items.map((i) => i.id)));
53+
}, [items]);
54+
55+
const clear = useCallback(() => {
56+
setSelectedIds(new Set());
57+
}, []);
58+
59+
const enter = useCallback(() => setBatchMode(true), []);
60+
const exit = useCallback(() => setBatchMode(false), []);
61+
62+
const count = useMemo(() => selectedIds.size, [selectedIds]);
63+
64+
return {
65+
batchMode,
66+
setBatchMode,
67+
selectedIds,
68+
toggle,
69+
selectAll,
70+
clear,
71+
enter,
72+
exit,
73+
count,
74+
};
75+
}

‎client/src/lib/http/apiClient.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ function createClient(baseUrlOrGetter: string | (() => string)) {
6666

6767
// 仅当刷新本身失败(refresh token 真正过期/失效)才判定为登录过期
6868
if (refreshError || !refreshed?.access_token) {
69-
window.dispatchEvent(new CustomEvent('kb:session-expired'));
69+
// 仅当 Supabase 存在会话(云端模式)才判定登录过期;本地优先/未登录
70+
// 用户无会话时,网关 401(凭证缺失/失效)只抛普通错误,不踢登录
71+
const { data: { session: current } } = await supabase.auth.getSession();
72+
if (current) {
73+
window.dispatchEvent(new CustomEvent('kb:session-expired'));
74+
}
7075
throw new Error('HTTP 401: 登录已过期');
7176
}
7277

@@ -146,7 +151,11 @@ function createClient(baseUrlOrGetter: string | (() => string)) {
146151
if (response.status === 401) {
147152
const { data: { session: refreshed }, error: refreshError } = await refreshSessionShared();
148153
if (refreshError || !refreshed?.access_token) {
149-
window.dispatchEvent(new CustomEvent('kb:session-expired'));
154+
// 同上:无 Supabase 会话时网关 401 不触发登录过期(本地模式)
155+
const { data: { session: current } } = await supabase.auth.getSession();
156+
if (current) {
157+
window.dispatchEvent(new CustomEvent('kb:session-expired'));
158+
}
150159
throw new Error('HTTP 401: 登录已过期');
151160
}
152161
headers.set('Authorization', `Bearer ${refreshed.access_token}`);

0 commit comments

Comments
 (0)