Files
ele-HeiXiu/ARCHITECTURE.md
YoungestSongMo 72ec1a5459 fix: 修复主题切换后首页状态重置 + 首页 UX 优化 + 文档同步
## 根因
  设置页 `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: 更新日期 + 新增叠加层模式章节
2026-06-07 05:21:47 +08:00

240 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 船长 · 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 重试 │
├─────────────────────────────────────────────────┤
│ 存储层 │
│ localStorageaccess_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.tsToken 生命周期)
- 职责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 — DrawerNavBar 内部 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` |