Files
ele-HeiXiu/ARCHITECTURE.md
YoungestSongMo d95d155ab5 chore: 文档更新 + 代码去重 + ESLint 修复,准备 0.0.16
- CHANGELOG: 新增 0.0.16 版本条目(模型输入栏重构/统一页面调度/依赖联动)
- TODO: 标记账户信息页为已完成,拆分未完成页面项
- ARCHITECTURE: 新增 PageDispatcher 叠加层说明 + OPEN_PAGE 事件
- 代码去重:runFieldChecks(validators) + createImageBeforeUpload(uploadValidation)
- ESLint: DependentFormItem 中 Form.useWatch 移到组件顶层(修复 rules-of-hooks)
- ESLint: message.success / async handleSubmit 加 void(修复 no-floating-promises)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:12:37 +08:00

251 lines
11 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-09
## 目录结构
```
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 / ForgotPasswordModal / ChangePasswordModal / FormField
│ │ ├── config/ # 字段配置 / 校验规则
│ │ ├── hooks/ # use-auth-state / use-sms-cooldown
│ │ └── types.ts # LoginFormValues / RegisterFormValues / FieldConfig
│ └── settings/ # 设置面板(主题 / 存储 / 账号安全 / 系统信息 / 更新)
│ ├── SettingsPage.tsx # 设置 Modal 主页面
│ ├── index.ts # barrel export
│ └── blocks/ # 设置子区块组件
│ ├── ThemeSetting.tsx
│ ├── StoragePathSetting.tsx
│ ├── SystemInfoSetting.tsx
│ ├── UpdateSetting.tsx
│ └── PasswordSetting.tsx
├── 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` / `OPEN_PAGE`
### 页面叠加层模式QT 多窗口模型映射)
本项目是 Python QT 桌面应用的 Web 重构版。QT 应用以"新开窗口"为常态原窗口不因新窗口打开而销毁。Web 端对应原则:
| QT 原版 | Web 对应 | 实现方式 |
|---------------|----------------|-------------------------------|
| 新开窗口叠加在主窗口之上 | Modal / Drawer | 事件总线驱动,主页面不卸载 |
| Tab 切换 / 视图替换 | React Router | 跨页状态放 Provider 或 localStorage |
**当前叠加层**
- `PageDispatcher` — 统一页面调度器,监听 `OPEN_PAGE` 事件,根据 `modalType` 渲染 Modal/Drawer/FloatingPanel全局互斥同一时刻最多一个面板
- 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` |