Skip to content

Commit 400e650

Browse files
committed
feat(ui): 设置页升级引导 + 每日色相微调对抗感觉适应
- SettingsPage: 新增 UpgradePrompt 非阻断式配额提示(区分内测/免费话术) - AsrSettingsSection: 移除冗余注释 - App: 每日确定性色相微调(applyDailyHueShift),对抗感觉适应 - useMode: 优化模式切换逻辑 - AppLayout/BottomNav: 布局微调
1 parent 32f84b3 commit 400e650

6 files changed

Lines changed: 16 additions & 95 deletions

File tree

‎client/src/App.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { useDigitalWellbeing } from '@/hooks/useDigitalWellbeing';
3939
import { DigitalWellbeingOverlay } from '@/components/DigitalWellbeingOverlay';
4040
import { useLocalStorageFlag } from '@/hooks/useLocalStorageFlag';
4141
import { EyeCareModeStyle, useEyeCareMode } from '@/components/EyeCareMode';
42+
import { applyDailyHueShift } from '@/features/retention/lib/dailyVariation';
4243
import '@/stores/useSettingsStore'; // 导入以触发音效设置初始化
4344
import { startPomodoroScheduler } from '@/features/pomodoro/lib/pomodoroScheduler';
4445
import { usePomodoroStore } from '@/features/pomodoro/store/usePomodoroStore';
@@ -67,6 +68,8 @@ const queryClient = new QueryClient({
6768
function App() {
6869
// Initialize theme on app mount
6970
useTheme();
71+
// 每日确定性色相微调(对抗感觉适应:每天微妙的新鲜感,纯本地无网络)
72+
applyDailyHueShift();
7073

7174
// 留存机制:在 Provider 内、路由外初始化所有留存 store
7275
// 放在此处确保无论用户从哪个路由进入,数据均已就绪

‎client/src/components/layout/AppLayout.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export default function AppLayout() {
159159
const moduleKeys: Record<string, string> = {
160160
'1': '/', '2': '/pomodoro', '3': '/notes',
161161
'4': '/flashcards', '5': '/feynman', '6': '/inspiration',
162-
'7': '/classroom', '0': '/settings',
162+
'7': '/classroom', '8': '/constellation', '0': '/settings',
163163
};
164164
if (moduleKeys[e.key]) {
165165
const route = moduleKeys[e.key];

‎client/src/components/layout/BottomNav.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @ai-context: 布局组件:BottomNav。
33
*/
44
import { NavLink, useLocation } from 'react-router-dom';
5-
import { Home, Timer, FileText, Layers, Lightbulb, Sparkles } from 'lucide-react';
5+
import { Home, Timer, FileText, Layers, Lightbulb, Sparkles, Orbit } from 'lucide-react';
66
import { cn } from '@/lib/utils';
77
import { soundPlayer } from '@/lib/audio/SoundPlayer';
88

@@ -13,6 +13,7 @@ const tabs = [
1313
{ to: '/flashcards', label: '闪卡', icon: Layers },
1414
{ to: '/feynman', label: '费曼', icon: Lightbulb },
1515
{ to: '/inspiration', label: '灵感', icon: Sparkles },
16+
{ to: '/constellation', label: '星座', icon: Orbit },
1617
];
1718

1819
export default function BottomNav() {

‎client/src/hooks/useMode.ts‎

Lines changed: 2 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
/**
2-
* @ai-context: 应用模式(本地/云端)切换 Hook,包装 ModeManager;模式影响同步与 AI 路由全链路。
2+
* @ai-context: 应用模式(本地/云端)切换 Hook,包含 ModeManager;模式影响同步与 AI 路由全链路。
33
*/
4-
import { useState, useEffect, useCallback, useRef } from 'react';
4+
import { useState, useEffect } from 'react';
55
import { modeManager, type AppMode, type ModeConfig } from '../lib/mode/ModeManager';
6-
import { useAuth } from '../lib/auth/AuthContext';
7-
import { useToast } from '../components/ui/Toast';
8-
import { useNetworkStatus } from './useNetworkStatus';
9-
import { getUnsyncedLogs } from '../lib/storage/operationLog';
106

117
/**
128
* 轻量版:仅订阅模式状态,不依赖 Auth/Toast 上下文
@@ -26,86 +22,3 @@ export function useModeState() {
2622

2723
return { mode, config };
2824
}
29-
30-
/**
31-
* 完整版:包含模式切换逻辑(含过渡保护)+ 自动降级
32-
* 必须在 AuthProvider + ToastProvider 内使用
33-
*/
34-
export function useMode() {
35-
const { mode, config } = useModeState();
36-
const { isAuthenticated } = useAuth();
37-
const { toast } = useToast();
38-
const { isOnline, isWeak } = useNetworkStatus();
39-
40-
// 记录上次网络状态,避免重复触发降级
41-
const prevOnlineRef = useRef(isOnline);
42-
const prevWeakRef = useRef(isWeak);
43-
44-
// v0.9.0: 网络状态变化时自动降级
45-
useEffect(() => {
46-
const wasOnline = prevOnlineRef.current;
47-
const wasWeak = prevWeakRef.current;
48-
prevOnlineRef.current = isOnline;
49-
prevWeakRef.current = isWeak;
50-
51-
// 只在状态实际发生变化时触发降级检查
52-
const networkChanged = (wasOnline !== isOnline) || (wasWeak !== isWeak);
53-
if (!networkChanged) return;
54-
55-
const currentMode = modeManager.getMode();
56-
57-
// 只在 hybrid/full 模式下才需要降级
58-
if (currentMode === 'local') return;
59-
60-
const degraded = modeManager.autoDegrade(isOnline, isWeak);
61-
if (degraded) {
62-
// Toast 提示由 onDegrade 监听器处理(下方),此处无需重复
63-
}
64-
}, [isOnline, isWeak]);
65-
66-
// v0.9.0: 订阅降级事件,显示 Toast 提示
67-
useEffect(() => {
68-
const unsubscribe = modeManager.onDegrade((_fromMode, _toMode, reason) => {
69-
toast({ type: 'warning', message: reason, duration: 5000 });
70-
});
71-
return unsubscribe;
72-
}, [toast]);
73-
74-
const changeMode = useCallback(async (newMode: AppMode): Promise<boolean> => {
75-
const currentMode = modeManager.getMode();
76-
if (currentMode === newMode) return true;
77-
78-
// local → hybrid/full:检查是否已登录
79-
if (currentMode === 'local' && (newMode === 'hybrid' || newMode === 'full')) {
80-
if (!isAuthenticated) {
81-
toast({ type: 'warning', message: '请先登录后再开启云同步' });
82-
return false;
83-
}
84-
}
85-
86-
// hybrid/full → local:检查未同步数据
87-
if ((currentMode === 'hybrid' || currentMode === 'full') && newMode === 'local') {
88-
try {
89-
const unsyncedLogs = await getUnsyncedLogs();
90-
if (unsyncedLogs.length > 0) {
91-
const confirmed = window.confirm(
92-
`当前有 ${unsyncedLogs.length} 条未同步的修改,切换到本地模式后这些数据将不会同步到云端。\n确定要切换吗?`,
93-
);
94-
if (!confirmed) return false;
95-
}
96-
} catch {
97-
// 检查失败时仍允许切换
98-
}
99-
}
100-
101-
modeManager.setMode(newMode);
102-
const labels: Record<AppMode, string> = { local: '本地模式', hybrid: '联网模式', full: '云端模式' };
103-
toast({ type: 'success', message: `已切换到${labels[newMode]}` });
104-
return true;
105-
}, [isAuthenticated, toast]);
106-
107-
// 推荐模式(仅展示用,不强制切换)
108-
const recommendedMode = modeManager.computeRecommendedMode(isAuthenticated, isOnline);
109-
110-
return { mode, config, changeMode, recommendedMode };
111-
}

‎client/src/pages/SettingsPage.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import FlashcardSettings from './settings/FlashcardSettings';
1515
import AudioCaptureSettings from './settings/AudioCaptureSettings';
1616
import { BetaProfile } from '@/features/beta/BetaProfile';
1717
import { useBetaProfile } from '@/features/beta/hooks/useBetaProfile';
18+
import { UpgradePrompt } from '@/features/beta/UpgradePrompt';
19+
import { useTierAccess } from '@/features/beta/hooks/useTierAccess';
1820

1921
// 延迟组:有网络/IPC/DB 操作的重组件
2022
const AIProviderSettings = lazy(() => import('./settings/AIProviderSettings'));
@@ -97,6 +99,8 @@ function SettingsSection({ children, index = 0 }: { children: React.ReactNode; i
9799
export default function SettingsPage() {
98100
// 加载内测身份
99101
useBetaProfile();
102+
// AI 配额信息(UpgradePrompt 展示 tier 权益与升级入口)
103+
const { dailyAiCalls } = useTierAccess();
100104

101105
return (
102106
<motion.div
@@ -121,6 +125,10 @@ export default function SettingsPage() {
121125
</SettingsSection>
122126
<SettingsSection index={1}>
123127
<BetaProfile />
128+
{/* 升级引导:非阻断式配额提示(区分内测/免费话术,可关闭) */}
129+
<div className="mt-3">
130+
<UpgradePrompt featureName="AI 功能" totalCount={dailyAiCalls} />
131+
</div>
124132
</SettingsSection>
125133
<SettingsSection index={2}>
126134
<AppearanceSettings />

‎client/src/pages/settings/components/AsrSettingsSection.tsx‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,6 @@ export function AsrSettingsSection() {
151151
<p className="text-c1 text-text-tertiary -mt-2">
152152
下载后可完全离线转写语音,零 API 费用。未下载时自动使用云端转写。
153153
</p>
154-
155-
{/* 启用开关:enabled 是本地转写与课堂真流式 ASR 的生效闸门。
156-
类名必须用默认 spacing 刻度存在的值(h-6/w-11/h-4/w-4)——
157-
h-5.5/w-4.5 不在刻度内会静默不生效,开关高度归零只剩文字 */}
158154
<div className="flex items-center justify-between p-3 rounded-kb-md bg-bg-elevated">
159155
<div>
160156
<p className="text-b3 font-medium text-text-primary">启用本地语音识别</p>

0 commit comments

Comments
 (0)