## 根因
设置页 `navigate('/settings')` 导致 React Router `/*` 的 index
路由不匹配,HomePage 被卸载重挂载,所有 useState 回到默认值。
## 修复(6 文件)
- event-bus.ts: 新增 OPEN_SETTINGS 事件
- nav-config.ts: 设置按钮 action: 'navigate' → 'button',去路由化
- NavBar.tsx: button case 中 emit(OPEN_SETTINGS)
- SettingsPage.tsx: 去路由化,改为 Modal 居中叠加 + open/onClose props,
使用 antd 6.x 正确 API destroyOnHidden(非废弃的 destroyOnClose),
mask={{ closable: false }} + keyboard={false} 仅允许 X 按钮关闭
- router/index.tsx: 移除 SettingsPage 渲染
- App.tsx: 添加 SettingsPage Modal,事件驱动(与 LoginPage 模式一致)
## UX 优化(2 文件)
- HomeContent.tsx: 右侧分隔条移除多余 marginLeft,左右视觉对称
- BannerCarousel.tsx: 新增关闭按钮,session 级别 useState 隐藏,
不做 localStorage 持久化,确保每次启动重新展示
## 架构原则
确立 QT 多窗口模型 → Web 映射:叠加层走 Modal/Drawer + 事件总线,
主页面不卸载;真正的视图切换才用 React Router。
## 文档同步
- TODO.md: 移除已完成项详细记录,精简为未完成待办
- WORKLOG.md: 追加 2026-06-07 完整工作日志
- PROJECT.md: 更新路由/叠加层架构说明
- CHANGELOG.md: 追加 v0.0.13 条目
- ARCHITECTURE.md: 更新日期 + 新增叠加层模式章节
18 KiB
项目概览
- 本项目使用 Vite + React 19 + TypeScript + Electron 重构现有 Python Qt 桌面应用,打造跨平台(Windows/macOS)的多窗口桌面程序。
- 核心界面为单页应用(SPA),同时支持通过 Electron 打开独立窗口以承载子功能。
核心需求
-
完整还原原有 Qt 应用的业务逻辑与视觉体验,并对交互进行现代化改造。
-
必须支持 多窗口(主窗口 + 可动态创建的辅助窗口),窗口间需通过事件/消息进行低耦合通信。
-
适配 Windows 与 macOS,处理好快捷键、托盘、菜单、窗口边框等平台差异。
-
采用 离线优先 架构,保障本地数据处理性能,合理使用本地存储(如 electron-store 或 SQLite)。
-
所有对系统底层能力的调用必须通过 Electron 主进程,严禁在渲染进程直接使用 Node.js API(除非通过 preload 安全暴露)。
技术架构
整体分层
-
主进程(Main Process):Electron 主进程,管理窗口生命周期、系统菜单、托盘、IPC 事件调度、本地文件/数据库操作。
-
预加载脚本(Preload):桥接渲染进程与主进程,使用 contextBridge 安全暴露有限 API。
-
渲染进程(Renderer):基于 React 19 的 SPA(主窗口)与独立窗口的 React 入口,负责 UI 展示与用户交互。
-
共享层(Shared):进程间共享的 TypeScript 类型、常量、工具函数、事件名称枚举。
前端架构原则
-
组件化:以函数组件 + Hooks 为唯一组件形式,UI 拆分为展示组件与容器组件。
-
单向数据流:全局状态使用 Context + Provider 模式(AppContext / ThemeContext / SettingsContext), 遵循 Provider → Context → Comsumer 单向流动。组件内部状态使用 useState/useReducer,不污染全局 Context。
-
事件驱动(EDA):
-
主进程与渲染进程通信基于 Electron IPC(ipcMain/ipcRenderer),定义明确的事件通道与载荷类型。
-
渲染进程内跨组件通信优先使用 Context;需要解耦的场景使用自定义事件总线(src/utils/event-bus.ts), 所有 emit 调用自动记录到日志文件用于审计追踪。
-
自定义错误体系(RequestError / RefreshError)通过事件总线解耦:请求拦截器抛出自定义错误 → 全局捕获 → emit 事件 → UI 层响应。
-
窗口间通信统一通过主进程转发(或使用 MessagePort 等 Electron 支持的机制),保持窗口独立性。
-
-
路由与导航:主窗口为单页应用,使用 React Router v7+ 管理页面切换。遵循 QT 多窗口模型映射原则:
- 叠加层(设置、公告、素材库等)→ Modal/Drawer + 事件总线驱动,主页面不卸载
- 页面跳转(真正的视图切换)→ React Router,需跨页保持的状态放 Provider 或 localStorage
-
辅助窗口(设置、预览、导入导出等)以 Modal/Drawer 叠加在主窗口之上,不与主窗口共享 DOM,但保持在同一个 React 组件树中以确保状态一致性。
目录结构
├── electron/ # Electron 主进程
│ ├── main.ts # 入口:窗口创建 / IPC 注册 / 生命周期
│ ├── preload.ts # 预加载脚本(contextBridge)
│ └── main/
│ ├── updater.ts # 自定义更新(API 版本检查 + OSS 下载 + NSIS 安装)
│ ├── logger.ts # 主进程日志核心(JSON Lines 写入 / 轮转 / 清理)
│ ├── log-ipc.ts # 渲染进程日志 IPC 通道接收
│ ├── net-request.ts # Electron net.request 的 axios 风格封装
│ ├── menu/index.ts # 系统菜单
│ └── utils/ # 平台判断 / 图标路径
├── src/ # 渲染进程(React 19 SPA)
│ ├── main.tsx # React 挂载点 + 全局错误捕获
│ ├── App.tsx # 根组件(路由 + 登录弹窗)
│ ├── components/ # 通用 UI 组件 + Provider
│ │ ├── AppProvider.tsx # 全局状态(platform / edition / login)
│ │ ├── ThemeProvider.tsx # 主题状态(light / dark)
│ │ ├── SettingsProvider.tsx # 设置状态(存储路径等)
│ │ ├── Layout.tsx # 页面壳(NavBar + Outlet)
│ │ └── navbar/ # 导航栏 + 公告抽屉
│ ├── contexts/ # Context 定义(app / settings / theme)
│ ├── hooks/ # 自定义 Hooks(useUpdater / useTheme)
│ ├── pages/ # 页面组件
│ │ ├── home/ # 首页仪表盘
│ │ ├── login/ # 登录 / 注册页
│ │ └── settings/ # 设置面板(主题 / 存储 / 更新)
│ ├── router/index.tsx # HashRouter 路由配置
│ ├── services/ # HTTP 请求封装
│ │ ├── request.ts # Axios 封装 + 拦截器 + Token 刷新
│ │ ├── types.ts # 自定义错误类型(RequestError / RefreshError)
│ │ ├── auth-token.ts # refresh_token 管理
│ │ └── modules/ # API 模块(auth / announcement)
│ ├── theme/ # 主题系统
│ │ ├── tokens.ts # 设计令牌
│ │ └── globals.css # Tailwind + 全局样式
│ └── utils/ # 纯函数工具
│ ├── event-bus.ts # 事件总线(on / off / emit + 日志钩子)
│ ├── logger.ts # 渲染进程日志器(IPC 转发 / DEV 控制台)
│ ├── ipc.ts # 安全 IPC 守卫(safeIpcOn / send / invoke)
│ ├── platform.ts # 平台判定
│ └── device.ts # 设备 ID 生成
├── shared/ # 主进程 & 渲染进程共享
│ ├── types/ # 类型定义(API / IPC / update / logging)
│ ├── constants/ # 常量(IPC 通道 / version / app)
│ └── utils/ # 共享工具
│ ├── index.ts # debounce / throttle / compareVersions
│ └── sanitize.ts # 日志脱敏工具(ID / 邮箱 / 手机 / 名称)
├── scripts/ # 构建脚本
│ ├── build.mjs # 构建总管
│ ├── clean.mjs # 清理产物
│ ├── upload-oss.mjs # OSS 上传
│ ├── publish-release.mjs # 发布到 API
│ └── generate-update-info.mjs # 生成更新元信息
├── package.json # 含 electron-builder 打包配置
├── vite.config.ts
├── tsconfig.json
└── tailwind.config.ts
多窗口与平台判定
-
windowManager.ts 负责统一创建 BrowserWindow,根据环境变量/参数加载不同入口(如 src/main/index.html 或 src/sub/settings/index.html)。
-
src/utils/platform.ts 提供 isMacOS、isWindows 等函数(基于 navigator.userAgent 或 preload 暴露的 process.platform)。在主进程的 electron/main/utils/platform.ts 中直接使用 Node.js API。
-
打包配置 (package.json build 字段) 须针对平台设置不同的图标、签名策略、安装包格式。
代码风格
TypeScript
-
所有语句必须以分号 ; 结尾,缺失分号将被视为错误。
-
启用 strict 模式,禁止隐式 any,优先使用 interface 定义对象类型,复杂类型可适度使用 type。
-
文件名使用小写短横线连接(kebab-case),如 use-window-state.ts;组件文件名与组件名保持一致(PascalCase),如 ThemeAdapter.tsx。
-
导入顺序:第三方库 → 项目内共享模块 → 相对路径模块,中间用空行分隔。
React
-
只允许函数组件与 Hooks,禁止使用类组件。
-
每个组件必须有明确的 Props 类型,使用 React.FC 或直接标注函数参数类型(推荐直接标注)。
-
事件处理函数命名以 handle 开头(如 handleClick),自定义 Hook 以 use 开头。
-
避免直接操作 DOM,除非封装在自定义 Hook 中(如焦点管理)。
-
使用 React.memo、useMemo、useCallback 优化性能,但不要过早优化。
状态管理
-
使用 React Context + Provider 模式管理全局状态,按领域拆分:AppContext(平台/登录)、ThemeContext(主题)、 SettingsContext(设置项)。避免引入 Redux 等重量级方案。
-
Provider 组件在 main.tsx 中按层级嵌套挂载: AppProvider → ThemeProvider → SettingsProvider → AntdApp → App
-
各模块通过自定义 Hook 消费(useAppContext / useTheme / useSettings),Hook 内部做 null 检查。
日志系统
-
双端日志:主进程直接写文件(electron/main/logger.ts),渲染进程通过 IPC send 上报(src/utils/logger.ts)。 两端暴露一致的 API:
logger.debug / info / warn / error(category, message, error?, context?)。 -
文件存储:JSON Lines 格式(每行一个 JSON),路径
{userData}/logs/app-YYYY-MM-DD.log, 每日轮转 + 7 天自动清理。 -
事件总线联动:event-bus.ts 暴露
setEventLogListener钩子,logger 注册后所有 emit 自动记录 DEBUG 日志。 -
错误日志:request.ts 拦截器根据错误类型自动分级记录(AUTH_EXPIRED → debug,5xx → error,其余 → warn)。
-
401 判断策略:HTTP 401 拦截处双重校验——同时检查 HTTP 状态码和响应体中的业务码
data.code。 仅status === 401 && data?.code === 401才走 token 刷新流程;其他业务码(如密码错误)穿透到正常错误处理, 透传后端的data.message。这确保登录接口返回的"账号或密码错误"不被 RefreshError("登录已过期") 覆盖。 -
脱敏:shared/utils/sanitize.ts 在日志写入前自动处理 ID(哈希)、邮箱/手机/名称/Token 等字段。
安全与认证
Token 加密存储
access_token和refresh_token通过 ElectronsafeStorageAPI 加密存储,不落明文:- macOS → Keychain
- Windows → DPAPI
- Linux → libsecret
- 实现文件:
electron/main/safe-storage-ipc.ts— 主进程加密/解密 IPC 处理(BIDIRECTIONAL.SAFESTORAGE_ENCRYPT / DECRYPT)src/utils/safe-storage.ts— 渲染进程桥接层(initSafeStore / getSafeStore / setSafeStore / clearSafeStore)src/services/request.ts— token 存取通过getSafeStore / setSafeStore,无需感知加密细节src/services/auth-token.ts— refresh_token 同理通过 safeStorage 存取
双 Token 机制
access_token(短效)+refresh_token(长效),登录成功同时下发- 请求拦截器自动附加
Authorization: Bearer <access_token> - 401 响应 → 双重校验(HTTP
status+ 业务data.code)→ 仅code === 401时触发刷新 - 刷新期间并发请求排队(
failedQueue),刷新完成后批量重试 - refresh 接口自身 401 直接拒绝(防死循环),login 接口 401 穿透透传后端
message - 启动时
refresh_token静默续期(自动登录),失败仅清除标记不影响用户名回填 - 登录/退出通过事件总线解耦(
LOGIN_SUCCESS/LOGOUT/AUTH_REQUIRED)
日志脱敏
shared/utils/sanitize.ts在日志写入前自动脱敏处理- 覆盖字段:用户 ID(SHA256 哈希)、邮箱(
***@domain)、手机号(掩码)、用户名称、Token(截断) - 主进程
electron/main/logger.ts和渲染进程src/utils/logger.ts均写入前调用sanitizeLogEntry()
样式与主题
-
优先使用 Tailwind 工具类 完成布局与常规样式,自定义设计令牌应映射为 Tailwind 扩展(在 tailwind.config.ts 中配置 theme.extend)。
-
Ant Design 组件样式通过 ConfigProvider 的 theme 属性覆盖,确保与 Tailwind 主题令牌同步。
ConfigProvider同时配置locale={zhCN}实现全局中文(分页器"条/页"等无需逐个组件设置)。 在 src/theme/ 中创建 antd-theme.ts,导出主题配置对象,引用 tokens.ts 中的设计变量。 组件级 token(如 Pagination.itemActiveColor / itemActiveBg)在 sharedComponents 中统一管理, 亮色/暗色主题共享,避免 CSS !important hack。 -
禁止在组件中硬编码颜色、字号等设计令牌,一律使用主题变量或 Tailwind 类名。
-
全局样式文件 globals.css 只引入 Tailwind 指令和必要的重置样式,不堆积组件样式。
事件与 IPC
-
所有 IPC 通道名称定义在 shared/constants/ipc-channels.ts 中,分为三类: MAIN_TO_RENDERER / RENDERER_TO_MAIN / BIDIRECTIONAL。
-
渲染进程通过 src/utils/ipc.ts 中的安全守卫访问 IPC(hasIpc 检查 → safeIpcOn / send / invoke), 非 Electron 环境下静默跳过,保证浏览器预览不崩溃。
-
主进程事件处理函数需做好错误捕获与日志记录,渲染进程调用 IPC 时使用 try-catch 并处理超时。
-
窗口间通信走主进程转发:发送窗口 safeIpcSend,主进程接收后定位目标窗口并 webContents.send。
开发与构建命令
-
开发模式:npm run dev 或 yarn dev(同时启动 Vite 与 Electron,支持热重载)。
-
构建:npm run build 编译渲染进程 → 打包 Electron 应用。
-
代码检查:npm run lint(ESLint + Prettier,规则已配置为强制分号、单引号等)。
-
类型检查:npm run typecheck(tsc --noEmit)。
自动更新系统
架构
客户端 (Electron 主进程)
checkForUpdates()
├─ ① HeiXiuProvider.getLatestVersion() → 调用自有版本检查 API
│ └─ hasUpdate=false → 直接 IPC 通知 UI(不经过 electron-updater)
└─ ② hasUpdate=true → electron-updater.checkForUpdates()
├─ HeiXiuProvider.getLatestVersion() → TTL 缓存命中,不重复请求
├─ emit 'update-available'(含 releaseNotes)→ UI 展示版本信息
└─ 用户点击"下载更新" → downloadUpdate()
├─ 差分下载(对比 .blockmap,仅下载变更区块)
├─ SHA512 校验
└─ 下载完成 → 用户点击"安装更新" → NSIS/DMG/AppImage 执行
| 模块 | 位置 | 职责 |
|---|---|---|
HeiXiuProvider |
electron/main/updater.ts |
自定义 Provider,调用自有 API,无更新返回 APP_VERSION |
checkForUpdates |
electron/main/updater.ts |
预检版本 → 无更新直发 IPC / 有更新交 electron-updater |
downloadUpdate |
electron/main/updater.ts |
用户确认后触发下载(从 checkForUpdates 分离) |
netRequest |
electron/main/net-request.ts |
Electron net.request 的 axios 风格封装 |
useUpdater |
src/hooks/use-updater.ts |
渲染进程更新状态 Hook(单例 IPC 监听) |
UpdateSetting |
src/pages/settings/UpdateSetting.tsx |
设置页更新区块(含"查看详情"弹窗) |
server.py |
test-server/server.py |
本地测试服务器,扫描 release/ 目录,语义版本排序 |
增量更新(差分下载)
package.json配置"differentialPackage": true,electron-builder 打包时自动生成.exe.blockmap文件- 自定义 Provider 模式:
HeiXiuNsisUpdater extends NsisUpdater/HeiXiuMacUpdater extends MacUpdater/HeiXiuAppImageUpdater extends AppImageUpdater - 三平台 Updater 均覆盖
getUpdateInfoAndProvider()注入HeiXiuProvider(自定义 API 版本检查),差分下载由 electron-updater 父类原生处理 - blockmap 文件流程:
scripts/generate-update-info.mjs— 扫描 blockmap 文件,写blockmap.url+blockmap.size到update-info.jsonscripts/upload-oss.mjs— 安装包 + blockmap 文件一并上传 OSS(支持 Node.js 直传和 ossutil 两种模式)scripts/publish-release.mjs— 通过--blockmap-url参数将 blockmap 信息发布到后端 API
- 效果:日常更新仅下载变更区块,预计节省 60%~90% 流量
环境变量加载机制(主进程)
加载顺序(后者覆盖前者,OS 环境变量始终优先):
① OS 环境变量(set KEY=VALUE && .\app.exe) ← 最高优先级
② .env.local(不进 git,个人覆盖用)
③ .env.{mode}(dev→.env.development,build→.env.production)
④ updater.ts 硬编码默认值(https://www.heixiu.com)
| 文件 | 何时生效 | 进 git | 用途 |
|---|---|---|---|
.env.development |
npm run dev |
✅ | 本地开发(API → 测试服务器) |
.env.production |
npm run build:xxx |
❌ | 构建时参考(API → 生产服务器) |
.env.local |
始终(后加载) | ❌ | 个人特殊配置(打包后连本地测试) |
.env.production和.env.local不打包进 ASAR。打包后无这些文件时自动使用代码默认值。
测试工作流
# 1. 构建两个版本
npm run build:win # → release/0.0.3/
# 改 package.json version = "0.0.4"
npm run build:win # → release/0.0.4/
# 2. 启动测试服务器
cd test-server && python server.py --release-dir ../release --port 8000
# 3. 启动旧版本客户端
set UPDATE_API_URL=http://127.0.0.1:8000 && .\release\0.0.3\win-unpacked\船长·HeiXiu.exe
# → 5 秒后自动检测到 0.0.4 → 差分下载 → 提示安装
相关文档:UpdateA.md、test-server/README.md
与其他 AI 协作的补充说明
-
所有生成的代码片段必须放在完整的文件上下文中,并注明所属文件路径。
-
若涉及跨平台逻辑,必须同时给出 Windows 与 macOS 的兼容实现或判断分支。
-
新增窗口时,需同步说明窗口入口、预加载脚本、主进程创建逻辑及 IPC 通道注册。
-
修改主题令牌时,需同步更新 tokens.ts、tailwind.config.ts 及 antd-theme.ts,确保三处一致。