Skip to content

Commit 1da6383

Browse files
committed
feat: v0.14.0 — 官网项目上线、课堂模块与引导系统新增
新增官网项目(website/): - Next.js 静态导出官网(download/story 页面) - Tailwind + Framer Motion 动效 - 独立 .gitignore / tsconfig / eslint 配置 新增课堂模块(features/classroom/): - 课堂学习功能组件集 新增引导系统(components/onboarding/): - 新用户引导流程组件 新增快捷方式设置: - ShortcutSettings.tsx 快捷键配置面板 3D 系统优化: - SceneProvider / OrbitalStore / SpatialNav 交互增强 - AuroraModuleEntity / ModuleEntity 视觉重构 布局与路由: - AppLayout / Sidebar / routes 结构调整 - commandPalette 默认命令更新 - tailwind.config 样式配置优化 番茄钟与笔记: - ImmersiveTimer / PomodoroPage 交互增强 - NoteEditPage 编辑器优化 Electron 主进程: - main.ts / windowManager 窗口管理调整 .gitignore 新增 .cursor/ 排除
1 parent 756be99 commit 1da6383

63 files changed

Lines changed: 11050 additions & 93 deletions

Some content is hidden

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

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ client/release/
1818
# IDE
1919
.vscode/
2020
.idea/
21+
.cursor/
2122
*.swp
2223
*.swo
2324
.qoder/

‎client/electron/main.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { initAutoUpdater, checkForUpdate, downloadUpdate, installUpdate, destroy
2020
import { createMainWindow, saveCloseChoice, completeSyncBeforeQuit } from './windowManager.js';
2121
import { destroyTray } from './trayManager.js';
2222
import { registerCaptureHandlers, disposeCaptureHandlers } from './captureHandlers.js';
23+
import { mcpManager } from './mcpManager.js';
2324
import { initialize, getConnection, close as closeDb, checkpointAndClose, reinitialize, getDbPath } from './db/sqliteService.js';
2425
import { initializeSchema } from './db/schema.js';
2526
import { saveCustomStoragePath, resolveDbPath } from './db/storageConfig.js';
@@ -700,6 +701,12 @@ if (!gotTheLock) {
700701
destroyTray();
701702
closeDb();
702703
mainWindow = null;
703-
app.quit();
704+
705+
// 关闭 MCP Bridge 子进程(防止孤儿进程阻塞退出)
706+
mcpManager.shutdown().catch((err) => {
707+
logger.error('[MCP] Shutdown error during quit', err);
708+
}).finally(() => {
709+
app.quit();
710+
});
704711
});
705712
}

‎client/electron/windowManager.ts‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ let cachedCloseChoice: string | null = null;
3030
let syncBeforeQuitRequested = false;
3131
let syncBeforeQuitCompleted = false;
3232

33+
/** 同步超时保护定时器(防止渲染进程无响应导致退出卡死) */
34+
let syncTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
35+
36+
/** 同步超时时间(毫秒):渲染进程未响应则强制继续退出 */
37+
const SYNC_BEFORE_QUIT_TIMEOUT_MS = 3000;
38+
3339
/** 读取保存的关闭选择 */
3440
export async function getCloseChoice(): Promise<string | null> {
3541
try {
@@ -175,14 +181,15 @@ export function createMainWindow(
175181
/**
176182
* 退出前同步流程入口
177183
* 不立即设置退出标志,而是先触发渲染进程同步
184+
* 附带超时保护:若渲染进程在 SYNC_BEFORE_QUIT_TIMEOUT_MS 内未响应,强制继续退出
178185
*/
179186
function syncAndQuit(
180187
win: BrowserWindow,
181188
isQuittingRef: { value: boolean },
182189
onQuit: () => void,
183190
): void {
184191
if (syncBeforeQuitRequested && !syncBeforeQuitCompleted) {
185-
// 同步已在进行中,等待完成
192+
// 同步已在进行中,等待完成(超时保护已启动)
186193
return;
187194
}
188195
if (syncBeforeQuitCompleted) {
@@ -195,13 +202,31 @@ function syncAndQuit(
195202
syncBeforeQuitRequested = true;
196203
logger.info('[Window] Sync before quit: requesting renderer sync');
197204
win.webContents.send('sync:before-quit');
205+
206+
// 超时保护:渲染进程未响应时强制完成同步流程
207+
syncTimeoutTimer = setTimeout(() => {
208+
syncTimeoutTimer = null;
209+
if (!syncBeforeQuitCompleted) {
210+
logger.warn('[Window] Sync before quit: timeout reached, forcing quit');
211+
completeSyncBeforeQuit();
212+
}
213+
}, SYNC_BEFORE_QUIT_TIMEOUT_MS);
198214
}
199215

200216
/**
201217
* 渲染进程同步完成后调用,允许窗口关闭
218+
* 也可由超时保护触发(渲染进程无响应时强制继续)
202219
*/
203220
export function completeSyncBeforeQuit(): void {
221+
if (syncBeforeQuitCompleted) return; // 幂等保护
204222
syncBeforeQuitCompleted = true;
223+
224+
// 清除超时定时器(若由渲染进程正常完成)
225+
if (syncTimeoutTimer) {
226+
clearTimeout(syncTimeoutTimer);
227+
syncTimeoutTimer = null;
228+
}
229+
205230
logger.info('[Window] Sync before quit: completed, proceeding with quit');
206231
const win = BrowserWindow.getAllWindows()[0];
207232
if (win) {

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

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useState } from 'react';
22
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
33
import { AnimatePresence, motion } from 'framer-motion';
4+
import { Home } from 'lucide-react';
45
import CommandPalette from '../ui/CommandPalette';
56
import { CloseConfirmDialog } from '../ui/CloseConfirmDialog';
67
import { CustomTitlebar } from './CustomTitlebar';
@@ -12,11 +13,16 @@ import { SceneTransition } from '@/lib/3d/scenes/SceneTransition';
1213
import { SpatialNav } from '@/lib/3d/navigation/SpatialNav';
1314
import { FunctionalOverlay } from '@/components/overlay/FunctionalOverlay';
1415
import { useOrbitalStore } from '@/lib/3d/navigation/OrbitalStore';
16+
import { OnboardingOverlay } from '@/components/onboarding/OnboardingOverlay';
17+
import { ModuleTourToast } from '@/components/onboarding/ModuleTourToast';
18+
import { HelpCenter } from '@/components/onboarding/HelpCenter';
19+
import { useOnboardingStore } from '@/components/onboarding/useOnboardingStore';
1520

1621
export default function AppLayout() {
1722
const { pathname } = useLocation();
1823
const navigate = useNavigate();
19-
const { isInModule, currentModule, exitModule, syncWithRoute } = useOrbitalStore();
24+
const { isInModule, currentModule, enterModule, exitModule, syncWithRoute } = useOrbitalStore();
25+
const openHelp = useOnboardingStore((s) => s.openHelp);
2026
const [showCloseDialog, setShowCloseDialog] = useState(false);
2127
const { sync } = useSync();
2228

@@ -62,33 +68,45 @@ export default function AppLayout() {
6268
return;
6369
}
6470

65-
if (e.key === 'Escape' && isInModule) {
66-
exitModule();
67-
navigate('/');
71+
if (e.key === 'Escape') {
72+
if (isInModule) {
73+
exitModule();
74+
} else {
75+
// 在 3D 场景模式下按 Esc 返回仪表盘
76+
enterModule('dashboard');
77+
navigate('/');
78+
}
6879
}
6980

7081
// 数字键 1-6 快捷导航(需无修饰键)
7182
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
7283
const moduleKeys: Record<string, string> = {
7384
'1': '/', '2': '/pomodoro', '3': '/notes',
7485
'4': '/flashcards', '5': '/feynman', '6': '/inspiration',
86+
'7': '/classroom',
7587
};
7688
if (moduleKeys[e.key]) {
7789
navigate(moduleKeys[e.key]);
7890
}
7991
}
92+
93+
// Ctrl+/ 或 Cmd+/ 打开帮助中心
94+
if ((e.ctrlKey || e.metaKey) && e.key === '/') {
95+
e.preventDefault();
96+
openHelp();
97+
}
8098
};
8199
window.addEventListener('keydown', handleKeyDown);
82100
return () => window.removeEventListener('keydown', handleKeyDown);
83-
}, [isInModule, exitModule, navigate]);
101+
}, [isInModule, enterModule, exitModule, navigate, openHelp]);
84102

85103
return (
86104
<div className="flex flex-col h-screen overflow-hidden">
87105
{/* Layer 2: 始终最顶层 — Electron标题栏 */}
88106
<CustomTitlebar />
89107

90108
{/* Layer 0: 3D场景全屏背景 */}
91-
<SceneProvider>
109+
<SceneProvider interactive={!isInModule}>
92110
<SceneTransition />
93111
<SpatialNav />
94112
</SceneProvider>
@@ -102,23 +120,41 @@ export default function AppLayout() {
102120
)}
103121
</AnimatePresence>
104122

105-
{/* 非模块内时显示简洁的状态提示 */}
123+
{/* 非模块内时显示简洁的状态提示 + 返回按钮 */}
106124
{!isInModule && (
107-
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-20">
108-
<motion.div
109-
initial={{ opacity: 0, y: 20 }}
110-
animate={{ opacity: 1, y: 0 }}
111-
className="px-6 py-3 rounded-full bg-black/30 backdrop-blur-xl border border-white/10 text-white/70 text-sm"
125+
<>
126+
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-20">
127+
<motion.div
128+
initial={{ opacity: 0, y: 20 }}
129+
animate={{ opacity: 1, y: 0 }}
130+
className="px-6 py-3 rounded-full bg-black/30 backdrop-blur-xl border border-white/10 text-white/70 text-sm"
131+
>
132+
点击3D物体进入模块 · 按 Esc 返回仪表盘 · 数字键 1-6 快捷跳转
133+
</motion.div>
134+
</div>
135+
{/* 浮动返回仪表盘按钮 */}
136+
<motion.button
137+
initial={{ opacity: 0, scale: 0.8 }}
138+
animate={{ opacity: 1, scale: 1 }}
139+
transition={{ delay: 0.2 }}
140+
whileHover={{ scale: 1.1 }}
141+
whileTap={{ scale: 0.9 }}
142+
onClick={() => { enterModule('dashboard'); navigate('/'); }}
143+
className="fixed bottom-8 right-8 z-20 w-12 h-12 rounded-full bg-black/30 backdrop-blur-xl border border-white/15 flex items-center justify-center text-white/70 hover:text-white hover:bg-black/50 transition-colors"
144+
aria-label="返回仪表盘"
112145
>
113-
点击3D物体进入模块 · 按 Ctrl+K 命令面板 · 数字键 1-6 快捷跳转
114-
</motion.div>
115-
</div>
146+
<Home className="w-5 h-5" strokeWidth={1.5} />
147+
</motion.button>
148+
</>
116149
)}
117150

118151
{/* 移动端底部标签栏 — 置于功能覆盖层之上,避免被 3D 场景或模块遮罩覆盖 */}
119152
<BottomNav />
120153

121154
{/* 全局组件 */}
155+
<OnboardingOverlay />
156+
<ModuleTourToast moduleId={currentModule} />
157+
<HelpCenter />
122158
<CommandPalette />
123159
<CloseConfirmDialog open={showCloseDialog} onClose={() => setShowCloseDialog(false)} />
124160
</div>

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

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ const navSection2 = [
2828

2929
const navSection3 = [
3030
{ to: '/analytics', label: '数据分析', icon: BarChart3, shortcut: '⌘ 6' },
31-
{ to: '/inspiration', label: '萤火海沟', icon: Sparkles, shortcut: '⌘ 7' },
31+
{ to: '/inspiration', label: '萤火海沟', icon: Sparkles, shortcut: '⌘ 6' },
32+
{ to: '/classroom', label: '回声定位', icon: Clapperboard, shortcut: '⌘ 7', dotColor: 'bg-classroom' },
3233
];
3334

3435
/* ── 蔡格尼克效应:待继续任务提示池 ── */
@@ -84,11 +85,6 @@ export default function Sidebar() {
8485

8586
const isOnNotes = location.pathname.startsWith('/notes');
8687

87-
const handleCaptureClick = () => {
88-
if (!isOnNotes) navigate('/notes');
89-
setCaptureOpen(true);
90-
};
91-
9288
const meta = user?.user_metadata as Record<string, unknown> | undefined;
9389
const displayName = (meta?.['display_name'] as string) || (meta?.['full_name'] as string) || user?.email?.split('@')[0] || '未登录';
9490
const avatarUrl = meta?.['avatar_url'] as string | undefined;
@@ -202,40 +198,18 @@ export default function Sidebar() {
202198
/>
203199
))}
204200

205-
{/* 回声定位 */}
206-
<motion.button
207-
custom={5}
208-
variants={sidebarItemVariants}
209-
initial="hidden"
210-
animate="visible"
211-
whileTap={{ scale: 0.98 }}
212-
onClick={handleCaptureClick}
213-
title={collapsed ? '回声定位' : undefined}
214-
className={cn(
215-
'flex items-center gap-2 w-full rounded-[var(--kb-radius-sm)] transition-all duration-200',
216-
collapsed ? 'justify-center px-0 py-1.5 mx-0' : 'px-2.5 py-[7px]',
217-
captureOpen && isOnNotes
218-
? 'bg-brand-50 text-brand-600 dark:bg-brand-900/20 dark:text-brand-400'
219-
: 'text-text-secondary/60 hover:text-text-primary hover:bg-brand-500/5 hover:scale-[1.02]',
220-
)}
221-
style={{ transition: `all ${BEAT.xs}ms ease` }}
222-
>
223-
<Clapperboard className="w-[18px] h-[18px] flex-shrink-0" strokeWidth={1.5} />
224-
{!collapsed && <span className="text-[13px] flex-1 text-left">回声定位</span>}
225-
</motion.button>
226-
227201
{/* Section: 更多 */}
228202
{!collapsed && (
229203
<div className="text-[10px] text-text-tertiary font-medium tracking-[0.08em] uppercase px-2.5 pt-4 pb-1 opacity-50">
230204
更多
231205
</div>
232206
)}
233207
{collapsed && <div className="my-1 mx-1.5 border-t border-border/20" />}
234-
{navSection3.map(({ to, label, icon: Icon, shortcut }, i) => (
208+
{navSection3.map(({ to, label, icon: Icon, shortcut, dotColor }, i) => (
235209
<SidebarItem
236210
key={to} to={to} label={label} icon={Icon}
237211
shortcut={collapsed ? undefined : shortcut}
238-
collapsed={collapsed} index={i + 6}
212+
dotColor={dotColor} collapsed={collapsed} index={i + 6}
239213
/>
240214
))}
241215

0 commit comments

Comments
 (0)