## 根因
设置页 `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: 更新日期 + 新增叠加层模式章节
240 lines
10 KiB
Markdown
240 lines
10 KiB
Markdown
# 船长 · HeiXiu — 前端架构文档
|
||
|
||
> 最后更新:2026-06-07
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
src/
|
||
├── components/ # 全局组件(Provider、导航栏、通用组件)
|
||
│ ├── AppProvider.tsx # 应用根 Provider:登录态 + 平台/版本 + 自动登录
|
||
│ ├── ThemeProvider.tsx # 主题 Provider:亮/暗切换 + Ant Design ConfigProvider
|
||
│ ├── navbar/ # 自定义导航栏(替代系统菜单)
|
||
│ └── common/ # 通用组件(LegalTextModal 等)
|
||
├── contexts/ # React Context 定义 + Consumer Hook
|
||
│ └── app-context.ts # AppContextValue 类型 + useAppContext()
|
||
├── hooks/ # 通用 Hook
|
||
│ ├── use-theme.ts # 主题切换
|
||
│ └── use-updater.ts # 版本更新
|
||
├── pages/ # 页面
|
||
│ └── login/ # 登录/注册
|
||
│ ├── index.tsx # LoginPage — Modal 弹层
|
||
│ ├── components/ # LoginForm / RegisterForm / FormField
|
||
│ ├── config/ # 字段配置 / 校验规则
|
||
│ ├── hooks/ # use-auth-state(记住账号偏好)
|
||
│ └── types.ts # LoginFormValues / RegisterFormValues
|
||
├── services/ # HTTP 层 + API 模块
|
||
│ ├── request.ts # Axios 封装(通用 HTTP 层,不知晓 auth)
|
||
│ ├── auth-token.ts # Token 生命周期(refresh_token 存取 + 401 刷新回调)
|
||
│ ├── types.ts # ApiResponse / RequestError / 分页类型
|
||
│ └── modules/ # API 模块(一个模块一个文件)
|
||
│ ├── index.ts # barrel 统一导出
|
||
│ ├── auth.ts # AuthAPI class + DTO 类型 + AuthUrlObj
|
||
│ └── announcement.ts
|
||
├── theme/ # Tailwind + Ant Design 主题
|
||
├── utils/ # 工具函数
|
||
│ ├── event-bus.ts # 发布-订阅事件总线
|
||
│ ├── device.ts # getDeviceId() — 设备 UUID
|
||
│ ├── platform.ts # 平台检测
|
||
│ └── ipc.ts # Electron IPC 安全封装
|
||
└── assets/ # 静态资源
|
||
```
|
||
|
||
---
|
||
|
||
## 分层架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ UI 层(React 组件) │
|
||
│ AppProvider / LoginPage / NavBar / ... │
|
||
│ 职责:渲染 + 用户交互 + 消费 Context │
|
||
├─────────────────────────────────────────────────┤
|
||
│ 状态层(Context + Hooks) │
|
||
│ AppContext / useAuthState / event-bus │
|
||
│ 职责:跨组件共享状态 + 跨模块事件通信 │
|
||
├─────────────────────────────────────────────────┤
|
||
│ 服务层(API + Token) │
|
||
│ AuthAPI / auth-token / request.ts │
|
||
│ 职责:HTTP 通信 + Token 生命周期 + 401 重试 │
|
||
├─────────────────────────────────────────────────┤
|
||
│ 存储层 │
|
||
│ localStorage(access_token / refresh_token / │
|
||
│ device-id / auth 偏好) │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 层间依赖规则
|
||
|
||
- **UI 层** → 可以 import 状态层 + 服务层
|
||
- **状态层** → 可以 import 服务层
|
||
- **服务层** → 可以 import 工具层(event-bus),不 import UI 或状态层
|
||
- **request.ts** → 通用 HTTP 层,**不 import 任何 auth 模块**(通过 `setTokenRefreshHandler` 解耦)
|
||
|
||
---
|
||
|
||
## 核心流程
|
||
|
||
### 1. 登录
|
||
|
||
```
|
||
LoginForm.onSubmit(values)
|
||
→ LoginPage.handleLogin
|
||
→ AuthAPI.login({ username, password, device_id, user_ip })
|
||
→ AppProvider.login(auth)
|
||
├─ setToken(access_token) → localStorage
|
||
├─ setStoredRefreshToken(...) → localStorage
|
||
├─ setUser(auth) → state
|
||
├─ setIsLoggedIn(true) → state
|
||
└─ emit(LOGIN_SUCCESS, auth) → 事件总线
|
||
→ onClose() → 关闭登录弹窗
|
||
```
|
||
|
||
### 2. 自动登录(静默 refresh)
|
||
|
||
```
|
||
App 启动 → AppProvider useEffect
|
||
→ getAutoLoginFlag() === true ?
|
||
→ 是 → getStoredRefreshToken()
|
||
→ AuthAPI.refreshToken({ refresh_token })
|
||
✅ 成功 → setToken + setUser + emit(LOGIN_SUCCESS) (无弹窗)
|
||
❌ 失败 → clearStoredRefreshToken + clearSavedAuth (下次需手动登录)
|
||
→ 否 → 跳过
|
||
```
|
||
|
||
### 3. 401 → 刷新 Token → 自动重试
|
||
|
||
```
|
||
任意请求发起
|
||
→ 服务端返回 401(业务码 401 或 HTTP 401)
|
||
→ request.ts handle401(config)
|
||
→ refreshHandler 已注册?
|
||
→ 是 → 调 refreshHandler()
|
||
→ auth-token.ts 回调
|
||
→ AuthAPI.refreshToken({ refresh_token })
|
||
✅ 成功 → 用新 token 重试原请求(用户完全无感)
|
||
❌ 失败 → clearToken + emit(AUTH_REQUIRED) → 弹出登录框
|
||
|
||
→ 否 → clearToken + emit(AUTH_REQUIRED) → 弹出登录框
|
||
|
||
并发 401 处理:
|
||
请求 A 收到 401 → 开始 refresh
|
||
请求 B 同时收到 401 → 发现 isRefreshing=true → 加入队列等待
|
||
refresh 完成 → 新 token 广播给所有排队请求 → 全部重试
|
||
```
|
||
|
||
### 4. 退出登录
|
||
|
||
```
|
||
NavBar / 设置页 → AppProvider.logout()
|
||
├─ clearToken()
|
||
├─ clearStoredRefreshToken()
|
||
├─ setUser(null)
|
||
├─ setIsLoggedIn(false)
|
||
└─ emit(LOGOUT)
|
||
```
|
||
|
||
---
|
||
|
||
## 关键模块说明
|
||
|
||
### request.ts(通用 HTTP 层)
|
||
|
||
- 职责:Axios 实例 + 拦截器 + `get/post/put/del` + 错误分类
|
||
- **不依赖**任何 auth 模块
|
||
- 暴露 `setTokenRefreshHandler(fn)` 供 auth-token.ts 注册 401 回调
|
||
- 暴露 `getToken()/setToken()/clearToken()` 供外部存取 access_token
|
||
- 401 拦截逻辑内置重试队列,支持并发请求去重
|
||
|
||
### auth-token.ts(Token 生命周期)
|
||
|
||
- 职责:refresh_token 的存取 + 注册 `handle401` 回调
|
||
- `initAuthRefresh()` 在 AppProvider 挂载时调用一次
|
||
- 回调内部调用 `AuthAPI.refreshToken()`,成则更新双 token,败则清除
|
||
|
||
### event-bus.ts(事件总线)
|
||
|
||
- 纯发布-订阅,不依赖任何框架
|
||
- 用于跨模块解耦通信(如 axios 拦截器无法直接用 React Context)
|
||
- 预定义事件:`AUTH_REQUIRED` / `SHOW_LOGIN` / `LOGIN_SUCCESS` / `LOGOUT` / `TASK_SUBMITTED` / `OPEN_SETTINGS`
|
||
|
||
### 页面叠加层模式(QT 多窗口模型映射)
|
||
|
||
本项目是 Python QT 桌面应用的 Web 重构版。QT 应用以"新开窗口"为常态,原窗口不因新窗口打开而销毁。Web 端对应原则:
|
||
|
||
| QT 原版 | Web 对应 | 实现方式 |
|
||
|---------|---------|---------|
|
||
| 新开窗口叠加在主窗口之上 | Modal / Drawer | 事件总线驱动,主页面不卸载 |
|
||
| Tab 切换 / 视图替换 | React Router | 跨页状态放 Provider 或 localStorage |
|
||
|
||
**当前叠加层**:
|
||
- LoginPage — Modal,事件总线 `SHOW_LOGIN` / `AUTH_REQUIRED` 触发
|
||
- SettingsPage — Modal,事件总线 `OPEN_SETTINGS` 触发
|
||
- AnnouncementDrawer — Drawer,NavBar 内部 state 控制
|
||
|
||
**反模式警告**:叠加层绝不应通过路由跳转实现。`navigate('/xxx')` + `useLocation` 判断显隐会导致原有页面卸载,丢失所有组件状态。
|
||
|
||
### API 模块规范
|
||
|
||
每个 `services/modules/*.ts` 遵循统一范式:
|
||
|
||
1. `XxxUrlObj` — 模块路由常量(不 export,模块私有)
|
||
2. DTO 类型 — 请求体/响应体 interface
|
||
3. `XxxAPI` class — 静态方法,调用方式 `XxxAPI.method()`
|
||
|
||
示例:
|
||
|
||
```ts
|
||
// auth.ts
|
||
const AuthUrlObj = {login: '/api/v1/auth/login', ...} as const;
|
||
|
||
export interface LoginRequestBody {}
|
||
|
||
export interface LoginResponseBody {}
|
||
|
||
export class AuthAPI {
|
||
static login(data: LoginRequestBody): Promise<LoginResponseBody> {
|
||
return post(AuthUrlObj.login, data);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 数据存储(localStorage)
|
||
|
||
| Key | 类型 | 说明 |
|
||
|----------------------------|-------------------------------------|-------------------|
|
||
| `ele-heixiu-token` | `string` | JWT access_token |
|
||
| `ele-heixiu-refresh-token` | `string` | JWT refresh_token |
|
||
| `ele-heixiu-device-id` | `string`(UUID v4) | 设备唯一标识,首次自动生成 |
|
||
| `ele-heixiu-auth` | `{ username, remember, autoLogin }` | 记住账号偏好(**不存密码**) |
|
||
|
||
---
|
||
|
||
## 环境变量
|
||
|
||
### 主进程加载顺序(由高到低优先级)
|
||
|
||
1. **OS 环境变量** — `set KEY=VALUE && .\app.exe`,不覆盖已有值
|
||
2. **`.env.local`** — 不进 git,个人覆盖(打包后连测试服务器等特殊场景)
|
||
3. **`.env.{mode}`** — 开发:`.env.development`,构建:`.env.production`
|
||
4. **代码默认值** — `updater.ts` 内 `getApiBaseUrl()` 的 fallback
|
||
|
||
> **安全说明**:`UPDATE_API_URL` 是公开的 API 端点地址,不是密钥。反编译 ASAR 可看到,但这不影响安全——应用本身就要连到这个地址。真正的敏感信息(JWT
|
||
> token、密码)是运行时获取的,不写死在代码中。
|
||
|
||
### 渲染进程(Vite 编译时注入)
|
||
|
||
| 变量 | 开发值 | 生产值 |
|
||
|---------------------|----------------------------|---------------------------|
|
||
| `VITE_API_BASE_URL` | `http://8.160.179.64:8000` | `https://www.heixiu.net` |
|
||
| `VITE_EDITION` | `personal` | `personal` / `enterprise` |
|
||
|
||
### 主进程(Node.js 运行时读取)
|
||
|
||
| 变量 | 开发值(.env.development) | 生产值(.env.production) | 默认值 |
|
||
|------------------|-------------------------|--------------------------|--------------------------|
|
||
| `UPDATE_API_URL` | `http://127.0.0.1:8000` | `https://www.heixiu.com` | `https://www.heixiu.com` |
|
||
| `EDITION` | `personal` | `personal` | `personal` |
|