From 3becdb85d439b9904576e15bb4404aa0399c05d5 Mon Sep 17 00:00:00 2001 From: YoungestSongMo <2130460579@qq.com> Date: Wed, 3 Jun 2026 19:25:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=20+=20=E8=B7=AF=E7=94=B1=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=20+=20JWT=E8=AE=A4=E8=AF=81=E4=BD=93=E7=B3=BB=20+=20=E5=85=AC?= =?UTF-8?q?=E5=91=8A=E4=BC=98=E5=8C=96=20+=20=E9=94=99=E8=AF=AF=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 新增功能 ### 设置页面(Router + Modal 叠加) - HashRouter 路由系统,Electron file:// 协议兼容 - 设置页以居中 Modal 叠加首页,仅 X 按钮关闭 - 主题切换 Segmented 控件(与首页 Switch 共享 Context) - 大模型产物存储仓库路径配置(原生文件夹选择对话框) - 企业版额外:团队创作存储仓库路径 - 系统信息展示(平台/版本/版本类型/环境) - 版本更新检查(状态机:idle→checking→no-update/available→downloading→downloaded→error) ### JWT 认证体系 - 双 Token 机制:access_token + refresh_token - Axios 拦截器 401 自动刷新 + 并发请求去重队列 - 启动时 refresh_token 静默续期(自动登录) - 记住密码:Base64 编码存储,表单自动回填 - setTokenRefreshHandler 解耦模式:request.ts 不知晓 auth ### 自定义错误类型系统 - RefreshError 继承 RequestError,type = AUTH_EXPIRED - 全局拦截器捕获 RefreshError → 自动 clearToken + AUTH_REQUIRED - 任何位置 throw new RefreshError() 即可触发登录 Modal ### 公告卡片优化 - 长文本 Tooltip 悬浮预览 - 点击卡片弹出详情 Modal(全文保留换行) ### SettingsContext(类似 Vue Pinia) - 集中式设置状态管理 - localStorage 持久化:ele-heixiu-output-path / ele-heixiu-team-repo-path - useSettings() 全局消费 ## 架构变更 - App.tsx → HashRouter + AppRoutes + LoginPage 弹层 - Layout.tsx = NavBar + 页面壳 - HomePage.tsx 从 App.tsx 提取 - NavBar 使用 useNavigate() 真实路由导航 - useUpdater 改为模块级单例 IPC 监听器(修复 MaxListenersExceededWarning) - electron/main.ts 新增 FILE_DIALOG IPC handler ## 依赖 - 新增 react-router-dom --- ARCHITECTURE.md | 203 +++ UpdateA.md | 148 +- electron/main.ts | 13 +- index.html | 8 +- package-lock.json | 1356 +----------------- package.json | 3 +- src/App.tsx | 291 +--- src/components/AppProvider.tsx | 89 +- src/components/Layout.tsx | 21 + src/components/SettingsProvider.tsx | 64 + src/components/navbar/AnnouncementDrawer.tsx | 185 ++- src/components/navbar/NavBar.tsx | 6 +- src/contexts/app-context.ts | 9 +- src/contexts/settings-context.ts | 94 ++ src/hooks/use-updater.ts | 167 ++- src/main.tsx | 12 +- src/pages/home/HomePage.tsx | 277 ++++ src/pages/login/hooks/use-auth-state.ts | 132 +- src/pages/login/index.tsx | 50 +- src/pages/settings/SettingsPage.tsx | 55 + src/pages/settings/StoragePathSetting.tsx | 188 +++ src/pages/settings/SystemInfoSetting.tsx | 65 + src/pages/settings/ThemeSetting.tsx | 34 + src/pages/settings/UpdateSetting.tsx | 122 ++ src/pages/settings/index.ts | 9 + src/router/index.tsx | 26 + src/services/auth-token.ts | 70 + src/services/modules/announcement.ts | 10 +- src/services/modules/auth.ts | 190 +++ src/services/modules/index.ts | 15 +- src/services/request.ts | 135 +- src/services/types.ts | 22 + src/utils/device.ts | 29 + vite.config.ts | 35 + 34 files changed, 2253 insertions(+), 1880 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 src/components/Layout.tsx create mode 100644 src/components/SettingsProvider.tsx create mode 100644 src/contexts/settings-context.ts create mode 100644 src/pages/home/HomePage.tsx create mode 100644 src/pages/settings/SettingsPage.tsx create mode 100644 src/pages/settings/StoragePathSetting.tsx create mode 100644 src/pages/settings/SystemInfoSetting.tsx create mode 100644 src/pages/settings/ThemeSetting.tsx create mode 100644 src/pages/settings/UpdateSetting.tsx create mode 100644 src/pages/settings/index.ts create mode 100644 src/router/index.tsx create mode 100644 src/services/auth-token.ts create mode 100644 src/services/modules/auth.ts create mode 100644 src/utils/device.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..ab19117 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,203 @@ +# 船长 · HeiXiu — 前端架构文档 + +> 最后更新:2026-06-03 + +## 目录结构 + +``` +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` + +### 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 { + 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 }` | 记住账号偏好(**不存密码**) | + +--- + +## 环境变量 + +| 变量 | 开发值 | 生产值 | +|------|--------|--------| +| `VITE_API_BASE_URL` | `http://8.160.179.64:8000` | `https://www.heixiu.net` | +| `VITE_EDITION` | `personal` | `personal` / `enterprise` | +| `UPDATE_API_URL` | — | `https://www.heixiu.com` | diff --git a/UpdateA.md b/UpdateA.md index 9ad1944..b44b46d 100644 --- a/UpdateA.md +++ b/UpdateA.md @@ -22,11 +22,11 @@ └──────────────────┘ └──────────────────┘ └──────────────────┘ ``` -| 角色 | 技术 | 职责 | -| ------ | -------------------------- | ------------------------------------------ | -| 客户端 | Electron + `net.request()` | 调用 API → 下载安装包 → 校验 SHA512 → 安装 | -| ECS | 任意后端语言 | 提供版本检查 API | -| OSS | 阿里云对象存储 | 托管 .exe / .dmg / .AppImage | +| 角色 | 技术 | 职责 | +|-----|--------------------|---------------------------------| +| 客户端 | Electron + `axios` | 调用 API → 下载安装包 → 校验 SHA512 → 安装 | +| ECS | 任意后端语言 | 提供版本检查 API | +| OSS | 阿里云对象存储 | 托管 .exe / .dmg / .AppImage | --- @@ -60,14 +60,14 @@ node scripts/publish-release.mjs <平台> <版本类型> [版本号] [--url { app.setName(WINDOW_TITLE); registerUpdateIpcHandlers(); + // 文件对话框 IPC 处理(供渲染进程选择文件夹) + ipcMain.handle(BIDIRECTIONAL.FILE_DIALOG, async (_event, options: Electron.OpenDialogOptions) => { + if (!mainWindow) return { canceled: true, filePaths: [] }; + return dialog.showOpenDialog(mainWindow, { + title: options?.title || '选择文件夹', + defaultPath: options?.defaultPath || app.getPath('home'), + properties: options?.properties || ['openDirectory'], + }); + }); + // 始终打开主窗口 // 登录/注册由渲染进程内的 Modal 弹层处理,不再新开窗口 const win = createMainWindow(); diff --git a/index.html b/index.html index aaf19c3..e76a48c 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,13 @@ - + 船长·HeiXiu diff --git a/package-lock.json b/package-lock.json index b1f82ec..d5b5841 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,10 @@ "antd": "^6.4.3", "axios": "^1.16.1", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-router-dom": "^7.16.0" }, "devDependencies": { - "@commitlint/cli": "^21.0.2", - "@commitlint/config-conventional": "^21.0.2", "@playwright/test": "^1.60.0", "@tailwindcss/vite": "^4.3.0", "@types/electron": "^1.4.38", @@ -33,8 +32,6 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", - "husky": "^9.1.7", - "lint-staged": "^17.0.7", "postcss": "^8.5.15", "prettier": "^3.8.3", "rollup-plugin-visualizer": "^7.0.1", @@ -424,435 +421,6 @@ "node": ">=6.9.0" } }, - "node_modules/@commitlint/cli": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.0.2.tgz", - "integrity": "sha512-YMmfLbqBg+ZRvvmPhc+cilSQFrh/AgzVgCT1U/OifmUZEwPbvCtA8rN//YNaF9d5eoZphxVMGYtmwA2QgQORgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/format": "^21.0.1", - "@commitlint/lint": "^21.0.2", - "@commitlint/load": "^21.0.2", - "@commitlint/read": "^21.0.2", - "@commitlint/types": "^21.0.1", - "tinyexec": "^1.0.0", - "yargs": "^18.0.0" - }, - "bin": { - "commitlint": "cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/cli/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@commitlint/cli/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@commitlint/cli/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@commitlint/cli/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@commitlint/cli/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@commitlint/cli/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@commitlint/cli/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@commitlint/cli/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@commitlint/cli/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@commitlint/config-conventional": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.0.2.tgz", - "integrity": "sha512-P/ZRhryQmkj0Z0dY9FOoRwe3xkwJyyAdtXwt01NT2kuZttcG2CNYp1q5Ci3u+nDT2jcbJRw2kt13Czl1qKNPfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-conventionalcommits": "^9.2.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/config-validator": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.0.1.tgz", - "integrity": "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^21.0.1", - "ajv": "^8.11.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/config-validator/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@commitlint/ensure": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.0.1.tgz", - "integrity": "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^21.0.1", - "es-toolkit": "^1.46.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/execute-rule": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-21.0.1.tgz", - "integrity": "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/format": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.0.1.tgz", - "integrity": "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^21.0.1", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/is-ignored": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.0.2.tgz", - "integrity": "sha512-H5z4t8PC9tUsmZ/o+EptM3Nq8sTFtskAShdcqxCoyzklW5eaVT5xbrDAET2uypzir9Vsj4ZZmBtyKjYe2XqgeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^21.0.1", - "semver": "^7.6.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/lint": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.0.2.tgz", - "integrity": "sha512-PnUmLYGeGLfW8oVatR9KpNxSHYAnJOEWlMZzfdeFOUq6WUrFx1fGQaWCWJqMoIll/xPM+GdfJV+tKHZVHhl0Fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/is-ignored": "^21.0.2", - "@commitlint/parse": "^21.0.2", - "@commitlint/rules": "^21.0.2", - "@commitlint/types": "^21.0.1" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/load": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.0.2.tgz", - "integrity": "sha512-lwUE70hN0/qE/ZRROhbaX65ly/FF12DrqfReLCESo37M0OQCFAf2jRS+2tSCSORq+bm4Kdju7qNDj46uc1QzTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/config-validator": "^21.0.1", - "@commitlint/execute-rule": "^21.0.1", - "@commitlint/resolve-extends": "^21.0.1", - "@commitlint/types": "^21.0.1", - "cosmiconfig": "^9.0.1", - "cosmiconfig-typescript-loader": "^6.1.0", - "es-toolkit": "^1.46.0", - "is-plain-obj": "^4.1.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/message": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.0.2.tgz", - "integrity": "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/parse": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.0.2.tgz", - "integrity": "sha512-QVZJhGHTm+oiuWyEKOCTQ0ZM3mfJ0eGWFeHuj7WzSKEth+UukcCHac9GD8pgdFlg/qGkFWOtyaNd1T8REgagaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^21.0.1", - "conventional-changelog-angular": "^8.2.0", - "conventional-commits-parser": "^6.3.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/read": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.0.2.tgz", - "integrity": "sha512-BtsrnLVycSSKf4Q0gMch4giCj5NNlmcbhc8ra5vONgGtP2IjRDo33bEFtr5Pm+2N+5fXGWb2MksWPrspPfdhdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/top-level": "^21.0.2", - "@commitlint/types": "^21.0.1", - "git-raw-commits": "^5.0.0", - "tinyexec": "^1.0.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/resolve-extends": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", - "integrity": "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/config-validator": "^21.0.1", - "@commitlint/types": "^21.0.1", - "es-toolkit": "^1.46.0", - "global-directory": "^5.0.0", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/rules": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.0.2.tgz", - "integrity": "sha512-k6tQ69Td7t2qUSIbik8D3TL1q3ZJpkEbV+yLogDzCRAdOxJm4ndhtBNREsLA1/puRfWvzS9eioF2w43WT+hHgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/ensure": "^21.0.1", - "@commitlint/message": "^21.0.2", - "@commitlint/to-lines": "^21.0.1", - "@commitlint/types": "^21.0.1" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/to-lines": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-21.0.1.tgz", - "integrity": "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/top-level": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.0.2.tgz", - "integrity": "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@commitlint/types": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.0.1.tgz", - "integrity": "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "conventional-commits-parser": "^6.3.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@conventional-changelog/git-client": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", - "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-libs/child-process-utils": "^1.0.0", - "@simple-libs/stream-utils": "^1.2.0", - "semver": "^7.5.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.4.0" - }, - "peerDependenciesMeta": { - "conventional-commits-filter": { - "optional": true - }, - "conventional-commits-parser": { - "optional": true - } - } - }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -2662,35 +2230,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@simple-libs/child-process-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", - "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-libs/stream-utils": "^1.2.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://ko-fi.com/dangreen" - } - }, - "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://ko-fi.com/dangreen" - } - }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -3556,22 +3095,6 @@ "ajv": "^6.9.1" } }, - "node_modules/ansi-escapes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "environment": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3842,13 +3365,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true, - "license": "MIT" - }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -4212,16 +3728,6 @@ "node": ">= 0.4" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -4293,22 +3799,6 @@ "node": ">=8" } }, - "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-truncate": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", @@ -4406,17 +3896,6 @@ "node": ">= 6" } }, - "node_modules/compare-func": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, "node_modules/compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", @@ -4447,49 +3926,6 @@ "dev": true, "license": "MIT" }, - "node_modules/conventional-changelog-angular": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", - "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", - "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-commits-parser": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", - "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-libs/stream-utils": "^1.2.0", - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4497,6 +3933,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -4505,61 +3954,6 @@ "license": "MIT", "optional": true }, - "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", - "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jiti": "2.6.1" - }, - "engines": { - "node": ">=v18" - }, - "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=9", - "typescript": ">=5" - } - }, - "node_modules/cosmiconfig-typescript-loader/node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -4898,19 +4292,6 @@ "node": ">=8" } }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -5148,19 +4529,6 @@ "node": ">=6" } }, - "node_modules/environment": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -5168,16 +4536,6 @@ "dev": true, "license": "MIT" }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5223,17 +4581,6 @@ "node": ">= 0.4" } }, - "node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", - "dev": true, - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -5538,13 +4885,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -5612,23 +4952,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -5918,23 +5241,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/git-raw-commits": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", - "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@conventional-changelog/git-client": "^2.6.0", - "meow": "^13.0.0" - }, - "bin": { - "git-raw-commits": "src/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6013,22 +5319,6 @@ "node": ">=10.0" } }, - "node_modules/global-directory": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", - "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "6.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -6234,22 +5524,6 @@ "node": ">= 14" } }, - "node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "dev": true, - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -6313,33 +5587,6 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6369,23 +5616,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -6473,29 +5703,6 @@ "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", "license": "MIT" }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -6613,13 +5820,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6978,182 +6178,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lint-staged": { - "version": "17.0.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.7.tgz", - "integrity": "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "listr2": "^10.2.1", - "picomatch": "^4.0.4", - "string-argv": "^0.3.2", - "tinyexec": "^1.2.4" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": ">=22.22.1" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - }, - "optionalDependencies": { - "yaml": "^2.9.0" - } - }, - "node_modules/listr2": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", - "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^5.2.0", - "eventemitter3": "^5.0.4", - "log-update": "^6.1.0", - "rfdc": "^1.4.1", - "wrap-ansi": "^10.0.0" - }, - "engines": { - "node": ">=22.13.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/cli-truncate": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", - "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^8.0.0", - "string-width": "^8.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", - "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "string-width": "^8.2.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/local-pkg": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", @@ -7195,144 +6219,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -7389,19 +6275,6 @@ "node": ">= 0.4" } }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -7436,19 +6309,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -7728,22 +6588,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/open": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", @@ -7825,38 +6669,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8248,6 +7060,44 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-router": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz", + "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.16.0.tgz", + "integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==", + "license": "MIT", + "dependencies": { + "react-router": "7.16.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", @@ -8271,16 +7121,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resedit": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", @@ -8306,16 +7146,6 @@ "dev": true, "license": "MIT" }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -8329,36 +7159,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -8369,13 +7169,6 @@ "node": ">= 4" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -8705,6 +7498,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8825,16 +7624,6 @@ "node": ">= 6" } }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -9019,16 +7808,6 @@ "semver": "bin/semver" } }, - "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -9438,23 +8217,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 95b05fc..1978c62 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "antd": "^6.4.3", "axios": "^1.16.1", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-router-dom": "^7.16.0" }, "build": { "appId": "com.heixiu.electron", diff --git a/src/App.tsx b/src/App.tsx index 80a2dcd..ffff8f1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,56 +1,21 @@ import { useState, useEffect, useCallback } from 'react'; -import { - Button, - Card, - Space, - Tag, - Typography, - Switch as AntSwitch, - Slider, - Progress, - theme as antTheme, -} from 'antd'; -import { - ApiOutlined, - ThunderboltOutlined, - BulbOutlined, - BulbFilled, - DownloadOutlined, - SyncOutlined, - CheckCircleOutlined, - ExclamationCircleOutlined, - RocketOutlined, -} from '@ant-design/icons'; +import { HashRouter } from 'react-router-dom'; -import { getPlatformName, isDev } from './utils/platform'; -import { gradientPrimary } from './theme/tokens'; -import { useTheme } from './hooks/use-theme'; -import { useUpdater } from './hooks/use-updater'; import { useAppContext } from './contexts/app-context'; -import { NavBar } from './components/navbar'; import { LoginPage } from './pages/login'; import { on, off, EVENTS } from './utils/event-bus'; - -const { Title, Text } = Typography; +import { AppRoutes } from './router'; /** * 根组件 — 主窗口 - * 未登录时弹出登录 Modal;已登录显示主页 - * axios 返回 401 → 事件总线触发 → Modal 再次弹出 + * + * 层级: + * HashRouter + * ├── LoginPage(Modal 弹层,事件总线驱动) + * ├── AppRoutes(页面路由 + SettingsPage Drawer 叠加) */ function App() { - const { isDark, toggleTheme } = useTheme(); - const { platform, edition, isLoggedIn, logout } = useAppContext(); - const { token } = antTheme.useToken(); - const [progress, setProgress] = useState(65); - const { - status: updateStatus, - progress: updateProgress, - info: updateInfo, - error: updateError, - checkForUpdates, - installUpdate, - } = useUpdater(); + const { isLoggedIn } = useAppContext(); // 登录 Modal 状态 const [loginOpen, setLoginOpen] = useState(false); @@ -78,245 +43,13 @@ function App() { }, []); return ( - <> + {/* 登录 Modal — 可关闭,关闭后主页可操作 */} - {/* ========== 主页 ========== */} -
- {/* ======== 自定义导航栏(替代系统菜单) ======== */} - - - {/* ======== 页面内容 ======== */} -
-
- {/* ---- 标题区 + 主题开关 ---- */} -
-
- - HeiXiu · 桌面工作台 - - } - unCheckedChildren={} - title={isDark ? '切换亮色模式' : '切换暗色模式'} - /> -
- - Electron + React 19 + TypeScript + Ant Design + Tailwind CSS - -
- - - {isDark ? '🌙 暗色' : '☀️ 亮色'} - - - {edition === 'enterprise' ? '企业版' : '个人版'} - - {isLoggedIn ? ( - <> - 已登录 - - - ) : ( - 未登录 - )} - -
- - {/* ---- 导航栏状态提示 ---- */} - - - 💡 当前使用 H5 自定义导航栏 替代系统菜单。 - {platform === 'darwin' - ? ' macOS 保留了最小 Edit 菜单以支持 Cmd+C/V 等快捷键。' - : ' Windows/Linux 已完全移除系统菜单栏。'} - - - - {/* ---- 主题色预览 ---- */} - - - {/* 渐变展示 */} -
- 品牌渐变 -
-
- 135° 渐变 -
-
- 90° 渐变 -
-
-
- - {/* 组件示例 */} -
- Ant Design 组件 - - - - - - - -
- - {/* 滑块 + 进度条 */} -
- 交互组件 -
- - 进度: - - - -
-
-
-
- - {/* ---- 平台信息 ---- */} - - -
- - 运行平台: - {getPlatformName()} -
-
- - 运行环境: - - {isDev() ? '开发模式' : '生产模式'} - -
-
- - 当前导航: - - H5 自定义导航栏({edition === 'enterprise' ? '企业版' : '个人版'} - 布局) - -
-
-
- - {/* ---- 版本更新 ---- */} - - - {/* 状态提示 */} - {updateStatus === 'checking' && ( -
- - 正在检查更新... -
- )} - {updateStatus === 'no-update' && ( -
- - 已是最新版本 -
- )} - {updateStatus === 'error' && ( -
- - {updateError?.message || '检查更新失败'} -
- )} - - {/* 发现新版本 */} - {(updateStatus === 'available' || updateStatus === 'downloading') && ( - <> -
- - 发现新版本:{updateInfo?.version} - {updateInfo?.forceUpdate && 强制更新} -
- {updateInfo?.releaseNotes && ( - - {updateInfo.releaseNotes} - - )} - - )} - - {/* 下载进度 */} - {updateStatus === 'downloading' && ( - - )} - - {/* 下载完成 */} - {updateStatus === 'downloaded' && ( - <> -
- - 更新已下载完毕,点击按钮安装并重启 -
- - - )} - - {/* 操作按钮 */} -
- - {isDev() && ( - - (生产环境启动 5 秒后自动检查) - - )} -
-
-
-
-
-
- + {/* 页面路由 + SettingsPage Drawer 叠加 */} + +
); } diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx index cda796f..7905eea 100644 --- a/src/components/AppProvider.tsx +++ b/src/components/AppProvider.tsx @@ -5,11 +5,22 @@ import { useState, useCallback, useEffect, type ReactNode } from 'react'; import type { Platform, Edition } from '@shared/types'; -import { getPlatform, isDev } from '../utils/platform'; +import { getPlatform, isDev } from '@/utils/platform'; import { parseEdition } from '@shared/constants/app'; -import { safeIpcOn, safeIpcOff } from '../utils/ipc'; -import { AppContext } from '../contexts/app-context'; -import type { AppContextValue } from '../contexts/app-context'; +import { safeIpcOn, safeIpcOff } from '@/utils/ipc'; +import { AppContext } from '@/contexts/app-context'; +import type { AppContextValue } from '@/contexts/app-context'; +import type { LoginResponseBody } from '@/services/modules'; +import { setToken, clearToken } from '@/services/request'; +import { emit, EVENTS } from '@/utils/event-bus'; +import { AuthAPI } from '@/services/modules'; +import { getAutoLoginFlag, clearAutoLoginFlag } from '@/pages/login/hooks/use-auth-state'; +import { + initAuthRefresh, + getStoredRefreshToken, + setStoredRefreshToken, + clearStoredRefreshToken, +} from '@/services/auth-token'; interface AppProviderProps { children: ReactNode; @@ -21,18 +32,79 @@ export function AppProvider({ children }: AppProviderProps) { return parseEdition(import.meta.env.VITE_EDITION); }); const [isLoggedIn, setIsLoggedIn] = useState(false); + const [user, setUser] = useState(null); - const login = useCallback(() => { - // TODO: 接入真实登录流程 + // ---------- 登录 / 退出 ---------- + + const login = useCallback((auth: LoginResponseBody) => { + setToken(auth.access_token); + setStoredRefreshToken(auth.refresh_token); + setUser(auth); setIsLoggedIn(true); + emit(EVENTS.LOGIN_SUCCESS, auth); }, []); const logout = useCallback(() => { - // TODO: 清理 token、用户状态等 + clearToken(); + clearStoredRefreshToken(); + setUser(null); setIsLoggedIn(false); + emit(EVENTS.LOGOUT); }, []); - // 监听主进程推送的平台/版本信息 + // ---------- 初始化 token 刷新回调(给 request.ts 401 拦截器用) ---------- + + useEffect(() => { + initAuthRefresh(); + }, []); + + // ---------- 启动时自动登录(refresh_token 续期) ---------- + + useEffect(() => { + if (!getAutoLoginFlag()) return; + + const refreshToken = getStoredRefreshToken(); + if (!refreshToken) return; + + AuthAPI.refreshToken({ refresh_token: refreshToken }) + .then((res) => { + setToken(res.access_token); + setStoredRefreshToken(res.refresh_token); + setIsLoggedIn(true); + // 补全用户信息 + AuthAPI.getUserInfo().then((info) => { + const u: LoginResponseBody = { + access_token: res.access_token, + refresh_token: res.refresh_token, + token_type: res.token_type, + expires_in: res.expires_in, + refresh_expires_in: res.refresh_expires_in, + user_id: info.id, + username: info.username, + role: info.role, + email: info.email, + balance_cent: info.balance_cent, + account_type: info.account_type, + display_name: info.display_name, + company_id: info.company_id, + real_name: info.real_name, + phone: info.phone, + license_code: '', + admin_permissions: info.admin_permissions, + }; + setUser(u); + emit(EVENTS.LOGIN_SUCCESS, u); + }); + }) + .catch(() => { + clearStoredRefreshToken(); + // 仅清除自动登录标记,保留用户名+密码供下次表单回填 + clearAutoLoginFlag(); + }); + }, []); + + // ---------- 监听主进程推送的平台/版本信息 ---------- + useEffect(() => { const handler = (_event: unknown, info: unknown) => { const data = info as { platform?: string; edition?: string } | undefined; @@ -52,6 +124,7 @@ export function AppProvider({ children }: AppProviderProps) { platform, edition, isLoggedIn, + user, login, logout, isDevMode: isDev(), diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..e3417f1 --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,21 @@ +// ============================================================ +// Layout — 页面布局壳(NavBar + 页面内容 Outlet) +// ============================================================ + +import { Outlet } from 'react-router-dom'; +import { theme as antTheme } from 'antd'; +import { NavBar } from './navbar'; + +export function Layout() { + const { token } = antTheme.useToken(); + + return ( +
+ + +
+ ); +} diff --git a/src/components/SettingsProvider.tsx b/src/components/SettingsProvider.tsx new file mode 100644 index 0000000..378bbdf --- /dev/null +++ b/src/components/SettingsProvider.tsx @@ -0,0 +1,64 @@ +// ============================================================ +// SettingsProvider — 设置状态 Provider 组件 +// +// 职责: +// - 初始化时从 localStorage 读取已存储的设置项 +// - 提供 SettingsContext 给所有子组件 +// - 写操作同步更新 state + localStorage +// ============================================================ + +import { useState, useCallback, type ReactNode } from 'react'; +import { + SettingsContext, + readStoredOutputPath, + writeStoredOutputPath, + clearStoredOutputPath, + readStoredTeamRepoPath, + writeStoredTeamRepoPath, + clearStoredTeamRepoPath, +} from '@/contexts/settings-context'; +import type { SettingsContextValue } from '@/contexts/settings-context'; + +interface SettingsProviderProps { + children: ReactNode; +} + +export function SettingsProvider({ children }: SettingsProviderProps) { + const [outputPath, setOutputPathState] = useState(readStoredOutputPath); + const [teamRepoPath, setTeamRepoPathState] = useState(readStoredTeamRepoPath); + + // ---------- outputPath ---------- + + const setOutputPath = useCallback((path: string) => { + setOutputPathState(path); + writeStoredOutputPath(path); + }, []); + + const clearOutputPath = useCallback(() => { + setOutputPathState(''); + clearStoredOutputPath(); + }, []); + + // ---------- teamRepoPath ---------- + + const setTeamRepoPath = useCallback((path: string) => { + setTeamRepoPathState(path); + writeStoredTeamRepoPath(path); + }, []); + + const clearTeamRepoPath = useCallback(() => { + setTeamRepoPathState(''); + clearStoredTeamRepoPath(); + }, []); + + const value: SettingsContextValue = { + outputPath, + teamRepoPath, + setOutputPath, + setTeamRepoPath, + clearOutputPath, + clearTeamRepoPath, + }; + + return {children}; +} diff --git a/src/components/navbar/AnnouncementDrawer.tsx b/src/components/navbar/AnnouncementDrawer.tsx index fe8fd11..e9e3424 100644 --- a/src/components/navbar/AnnouncementDrawer.tsx +++ b/src/components/navbar/AnnouncementDrawer.tsx @@ -4,7 +4,7 @@ // ============================================================ import { useEffect, useState, useCallback } from 'react'; -import { Drawer, List, Typography, Empty, Tag, Spin } from 'antd'; +import { Drawer, Card, Typography, Empty, Tag, Spin, Modal, Tooltip } from 'antd'; import { NotificationOutlined } from '@ant-design/icons'; import { fetchAnnouncements, type Announcement } from '@/services/modules'; @@ -29,13 +29,15 @@ const typeLabelMap: Record = { maintenance: '维护', }; -/** 格式化日期(YYYY-MM-DD) */ +/** 格式化日期(YYYY-MM-DD HH:mm) */ function formatDate(iso: string): string { try { return new Date(iso).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', + minute: '2-digit', }); } catch { return iso; @@ -54,6 +56,9 @@ export function AnnouncementDrawer({ open, onClose }: AnnouncementDrawerProps) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // 详情 Modal + const [detail, setDetail] = useState(null); + // 打开抽屉时请求公告列表 const loadAnnouncements = useCallback(async () => { setLoading(true); @@ -79,69 +84,121 @@ export function AnnouncementDrawer({ open, onClose }: AnnouncementDrawerProps) { // ---- 渲染 ---- return ( - - - 公告 - - } - placement="right" - size={380} - open={open} - onClose={onClose} - styles={{ body: { padding: 0 } }} - > - {loading ? ( -
- -
- ) : error ? ( -
- -
- ) : list.length === 0 ? ( -
- -
- ) : ( - ( - -
- {/* 标题 + 类型标签 */} -
- - {item.pinned && '📌 '} - {item.title} - - - {typeLabelMap[item.type] || item.type} - -
- - {/* 内容 */} - + + + 公告 + + } + placement="right" + size={380} + open={open} + onClose={onClose} + styles={{ body: { padding: 0 } }} + > + {loading ? ( +
+ +
+ ) : error ? ( +
+ +
+ ) : list.length === 0 ? ( +
+ +
+ ) : ( +
+ {list.map((item) => ( + 100 + ? item.content.slice(0, 200) + '…' + : undefined + } + placement="left" + > + + {item.pinned && '📌 '} + {item.title} + + } + extra={ + + {typeLabelMap[item.type] || item.type} + + } + onClick={() => setDetail(item)} > - {item.content} - + + {item.content} + + + {formatDate(item.created_at)} + + + + ))} +
+ )} +
- {/* 日期 */} - - {formatDate(item.created_at)} - -
-
- )} - /> - )} -
+ {/* ======== 公告详情 Modal ======== */} + setDetail(null)} + footer={null} + width={480} + centered + title={ + detail && ( + + {detail.pinned && '📌 '} + {detail.title} + + ) + } + > + {detail && ( +
+
+ + {typeLabelMap[detail.type] || detail.type} + + + {formatDate(detail.created_at)} + +
+
+ + {detail.content} + +
+
+ )} +
+ ); } diff --git a/src/components/navbar/NavBar.tsx b/src/components/navbar/NavBar.tsx index 0e6b427..ce570cb 100644 --- a/src/components/navbar/NavBar.tsx +++ b/src/components/navbar/NavBar.tsx @@ -8,6 +8,7 @@ // ============================================================ import { useState, useCallback, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { App } from 'antd'; import { useAppContext } from '@/contexts/app-context'; @@ -24,6 +25,7 @@ export function NavBar() { const { platform, edition, isLoggedIn, logout } = useAppContext(); const { isDark } = useTheme(); const { message } = App.useApp(); + const navigate = useNavigate(); const [drawerOpen, setDrawerOpen] = useState(false); @@ -90,8 +92,8 @@ export function NavBar() { case 'spa': if (item.key === 'auth') { emit(EVENTS.SHOW_LOGIN); - } else { - console.log('[NavBar] 导航至:', item.target); + } else if (item.target) { + navigate(item.target); } break; } diff --git a/src/contexts/app-context.ts b/src/contexts/app-context.ts index a92832b..07100e5 100644 --- a/src/contexts/app-context.ts +++ b/src/contexts/app-context.ts @@ -4,6 +4,7 @@ import { createContext, useContext } from 'react'; import type { Platform, Edition } from '@shared/types'; +import type { LoginResponseBody } from '@/services/modules'; // ---------- Context 类型 ---------- @@ -14,9 +15,11 @@ export interface AppContextValue { edition: Edition; /** 用户是否已登录 */ isLoggedIn: boolean; - /** 登录(TODO: 接入真实认证) */ - login: () => void; - /** 退出登录 */ + /** 当前登录用户信息(未登录时为 null,结构与 LoginResponseBody 一致) */ + user: LoginResponseBody | null; + /** 登录:保存 Token 并更新登录态(auth 为登录接口返回值) */ + login: (auth: LoginResponseBody) => void; + /** 退出登录:清除 Token 和用户状态 */ logout: () => void; /** 是否为开发环境 */ isDevMode: boolean; diff --git a/src/contexts/settings-context.ts b/src/contexts/settings-context.ts new file mode 100644 index 0000000..2249815 --- /dev/null +++ b/src/contexts/settings-context.ts @@ -0,0 +1,94 @@ +// ============================================================ +// SettingsContext — 设置项集中状态管理(类似 Vue Pinia store) +// +// 职责: +// - 存储路径的集中管理(read/write/clear) +// - 持久化到 localStorage,遵循 ele-heixiu-* 键名规范 +// - 全局组件通过 useSettings() 消费 +// ============================================================ + +import { createContext, useContext } from 'react'; + +// ---------- localStorage 键 ---------- + +const OUTPUT_PATH_KEY = 'ele-heixiu-output-path'; +const TEAM_REPO_PATH_KEY = 'ele-heixiu-team-repo-path'; + +// ---------- Context 类型 ---------- + +export interface SettingsContextValue { + /** 大模型产物存储仓库路径 */ + outputPath: string; + /** 团队创作存储仓库路径(企业版) */ + teamRepoPath: string; + /** 设置产物输出路径 */ + setOutputPath: (path: string) => void; + /** 设置团队创作仓库路径 */ + setTeamRepoPath: (path: string) => void; + /** 清除产物输出路径 */ + clearOutputPath: () => void; + /** 清除团队创作仓库路径 */ + clearTeamRepoPath: () => void; +} + +export const SettingsContext = createContext(null); + +// ---------- 工具函数(localStorage 读写) ---------- + +export function readStoredOutputPath(): string { + try { + return localStorage.getItem(OUTPUT_PATH_KEY) || ''; + } catch { + return ''; + } +} + +export function writeStoredOutputPath(path: string): void { + try { + localStorage.setItem(OUTPUT_PATH_KEY, path); + } catch { + /* 静默忽略 */ + } +} + +export function clearStoredOutputPath(): void { + try { + localStorage.removeItem(OUTPUT_PATH_KEY); + } catch { + /* 静默忽略 */ + } +} + +export function readStoredTeamRepoPath(): string { + try { + return localStorage.getItem(TEAM_REPO_PATH_KEY) || ''; + } catch { + return ''; + } +} + +export function writeStoredTeamRepoPath(path: string): void { + try { + localStorage.setItem(TEAM_REPO_PATH_KEY, path); + } catch { + /* 静默忽略 */ + } +} + +export function clearStoredTeamRepoPath(): void { + try { + localStorage.removeItem(TEAM_REPO_PATH_KEY); + } catch { + /* 静默忽略 */ + } +} + +// ---------- Consumer Hook ---------- + +export function useSettings(): SettingsContextValue { + const ctx = useContext(SettingsContext); + if (!ctx) { + throw new Error('useSettings() 必须在 内部调用'); + } + return ctx; +} diff --git a/src/hooks/use-updater.ts b/src/hooks/use-updater.ts index 398ab66..cb5fb71 100644 --- a/src/hooks/use-updater.ts +++ b/src/hooks/use-updater.ts @@ -1,26 +1,31 @@ // ============================================================ -// useUpdater — 渲染进程中的更新状态管理 +// useUpdater — 渲染进程中的更新状态管理(单例 IPC 监听) // // 用法: // const { status, progress, checkForUpdates, installUpdate } = useUpdater(); // // 状态机:idle → checking → (no-update | available → downloading → downloaded → idle) +// +// 关键设计: +// - IPC 监听器在模块级别只注册一次(单例),避免多个组件同时 +// 调用 useUpdater() 导致 MaxListenersExceededWarning +// - 所有 useUpdater() 实例通过订阅机制共享同一份状态 // ============================================================ import { useState, useEffect, useCallback, useRef } from 'react'; -import { safeIpcInvoke, safeIpcOn, safeIpcOff, safeIpcSend } from '@/utils/ipc'; +import { safeIpcInvoke, safeIpcOn, safeIpcSend } from '@/utils/ipc'; // ---------- 类型 ---------- export type UpdaterStatus = - | 'idle' // 空闲 - | 'checking' // 正在检查 - | 'no-update' // 已是最新 - | 'available' // 发现新版本,准备下载 - | 'downloading' // 下载中 - | 'downloaded' // 下载完成,待安装 - | 'error'; // 出错 + | 'idle' + | 'checking' + | 'no-update' + | 'available' + | 'downloading' + | 'downloaded' + | 'error'; export interface UpdateInfo { version: string; @@ -40,6 +45,43 @@ export interface UpdateError { message: string; } +// ---------- 模块级共享状态 ---------- + +interface UpdaterState { + status: UpdaterStatus; + info: UpdateInfo | null; + progress: UpdateProgress; + error: UpdateError | null; +} + +type Subscriber = (state: UpdaterState) => void; + +let sharedState: UpdaterState = { + status: 'idle', + info: null, + progress: { percent: 0 }, + error: null, +}; + +const subscribers = new Set(); +let listenersRegistered = false; + +function notifyAll(): void { + const snapshot = { ...sharedState }; + subscribers.forEach((fn) => { + try { + fn(snapshot); + } catch { + /* 单个订阅者异常不影响其他 */ + } + }); +} + +function setSharedState(patch: Partial): void { + sharedState = { ...sharedState, ...patch }; + notifyAll(); +} + // ---------- IPC 通道名(与主进程 updater.ts 保持一致)---------- const CH = { @@ -52,62 +94,73 @@ const CH = { CHECK_FOR_UPDATE: 'check-for-update', } as const; +// ---------- 注册 IPC 监听器(模块级,仅一次)---------- + +function ensureListeners(): void { + if (listenersRegistered) return; + listenersRegistered = true; + + safeIpcOn(CH.UPDATE_AVAILABLE, (_event: unknown, ...args: unknown[]) => { + setSharedState({ info: args[0] as UpdateInfo, status: 'available' }); + }); + + safeIpcOn(CH.UPDATE_PROGRESS, (_event: unknown, ...args: unknown[]) => { + setSharedState({ status: 'downloading', progress: args[0] as UpdateProgress }); + }); + + safeIpcOn(CH.UPDATE_DOWNLOADED, () => { + setSharedState({ status: 'downloaded' }); + }); + + safeIpcOn(CH.UPDATE_ERROR, (_event: unknown, ...args: unknown[]) => { + setSharedState({ error: args[0] as UpdateError, status: 'error' }); + }); + + safeIpcOn(CH.UPDATE_NOT_AVAILABLE, () => { + setSharedState({ status: 'no-update' }); + }); +} + // ---------- Hook ---------- export function useUpdater() { - const [status, setStatus] = useState('idle'); - const [info, setInfo] = useState(null); - const [progress, setProgress] = useState({ percent: 0 }); - const [error, setError] = useState(null); - - // 防止 StrictMode 双重监听 - const registered = useRef(false); + const [state, setState] = useState(sharedState); + const subscribed = useRef(false); + // 确保 IPC 监听器只注册一次 useEffect(() => { - if (registered.current) return; - registered.current = true; + ensureListeners(); + }, []); - safeIpcOn(CH.UPDATE_AVAILABLE, (_event: unknown, ...args: unknown[]) => { - setInfo(args[0] as UpdateInfo); - setStatus('available'); - }); + // 订阅模块级状态变更 + useEffect(() => { + if (subscribed.current) return; + subscribed.current = true; - safeIpcOn(CH.UPDATE_PROGRESS, (_event: unknown, ...args: unknown[]) => { - setStatus('downloading'); - setProgress(args[0] as UpdateProgress); - }); - - safeIpcOn(CH.UPDATE_DOWNLOADED, () => { - setStatus('downloaded'); - }); - - safeIpcOn(CH.UPDATE_ERROR, (_event: unknown, ...args: unknown[]) => { - setError(args[0] as UpdateError); - setStatus('error'); - }); - - safeIpcOn(CH.UPDATE_NOT_AVAILABLE, () => { - setStatus('no-update'); - }); + const sub: Subscriber = (newState) => setState(newState); + subscribers.add(sub); return () => { - safeIpcOff(CH.UPDATE_AVAILABLE, () => {}); - safeIpcOff(CH.UPDATE_PROGRESS, () => {}); - safeIpcOff(CH.UPDATE_DOWNLOADED, () => {}); - safeIpcOff(CH.UPDATE_ERROR, () => {}); - safeIpcOff(CH.UPDATE_NOT_AVAILABLE, () => {}); + subscribers.delete(sub); + subscribed.current = false; }; }, []); + // 同步初始状态(防止在订阅之前状态已变更) + useEffect(() => { + setState(sharedState); + }, []); + /** 手动检查更新 */ const checkForUpdates = useCallback(async () => { - setStatus('checking'); - setError(null); + setSharedState({ status: 'checking', error: null }); try { await safeIpcInvoke(CH.CHECK_FOR_UPDATE); } catch (err) { - setStatus('error'); - setError({ message: (err as Error).message || '检查更新失败' }); + setSharedState({ + status: 'error', + error: { message: (err as Error).message || '检查更新失败' }, + }); } }, []); @@ -118,17 +171,19 @@ export function useUpdater() { /** 重置状态 */ const reset = useCallback(() => { - setStatus('idle'); - setInfo(null); - setProgress({ percent: 0 }); - setError(null); + setSharedState({ + status: 'idle', + info: null, + progress: { percent: 0 }, + error: null, + }); }, []); return { - status, - info, - progress, - error, + status: state.status, + info: state.info, + progress: state.progress, + error: state.error, checkForUpdates, installUpdate, reset, diff --git a/src/main.tsx b/src/main.tsx index 514c635..a936af6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,7 @@ import { App as AntdApp } from 'antd'; import App from './App'; import { ThemeProvider } from './components/ThemeProvider'; import { AppProvider } from './components/AppProvider'; +import { SettingsProvider } from './components/SettingsProvider'; // 全局样式(包含 Tailwind 指令 + 主题扩展 + 重置) import './theme/globals.css'; @@ -14,10 +15,13 @@ ReactDOM.createRoot(document.getElementById('root')!).render( {/* ThemeProvider 内部管理 ConfigProvider(亮色/暗色切换) */} - {/* AntdApp 提供 message / notification / modal 的静态方法上下文 */} - - - + {/* SettingsProvider — 集中管理设置项(存储路径等) */} + + {/* AntdApp 提供 message / notification / modal 的静态方法上下文 */} + + + + , diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx new file mode 100644 index 0000000..c27df5e --- /dev/null +++ b/src/pages/home/HomePage.tsx @@ -0,0 +1,277 @@ +// ============================================================ +// HomePage — 首页仪表盘(从 App.tsx 提取) +// ============================================================ + +import { useState } from 'react'; +import { + Button, + Card, + Space, + Tag, + Typography, + Switch as AntSwitch, + Slider, + Progress, + theme as antTheme, +} from 'antd'; +import { + ApiOutlined, + ThunderboltOutlined, + BulbOutlined, + BulbFilled, + DownloadOutlined, + SyncOutlined, + CheckCircleOutlined, + ExclamationCircleOutlined, + RocketOutlined, +} from '@ant-design/icons'; + +import { getPlatformName, isDev } from '@/utils/platform'; +import { gradientPrimary } from '@/theme/tokens'; +import { useTheme } from '@/hooks/use-theme'; +import { useUpdater } from '@/hooks/use-updater'; +import { useAppContext } from '@/contexts/app-context'; + +const { Title, Text } = Typography; + +export function HomePage() { + const { isDark, toggleTheme } = useTheme(); + const { platform, edition, isLoggedIn, logout } = useAppContext(); + const { token } = antTheme.useToken(); + const [progress, setProgress] = useState(65); + const { + status: updateStatus, + progress: updateProgress, + info: updateInfo, + error: updateError, + checkForUpdates, + installUpdate, + } = useUpdater(); + + return ( +
+
+ {/* ---- 标题区 + 主题开关 ---- */} +
+
+ + HeiXiu · 桌面工作台 + + } + unCheckedChildren={} + title={isDark ? '切换亮色模式' : '切换暗色模式'} + /> +
+ + Electron + React 19 + TypeScript + Ant Design + Tailwind CSS + +
+ + + {isDark ? '🌙 暗色' : '☀️ 亮色'} + + + {edition === 'enterprise' ? '企业版' : '个人版'} + + {isLoggedIn ? ( + <> + 已登录 + + + ) : ( + 未登录 + )} + +
+ + {/* ---- 导航栏状态提示 ---- */} + + + 💡 当前使用 H5 自定义导航栏 替代系统菜单。 + {platform === 'darwin' + ? ' macOS 保留了最小 Edit 菜单以支持 Cmd+C/V 等快捷键。' + : ' Windows/Linux 已完全移除系统菜单栏。'} + + + + {/* ---- 主题色预览 ---- */} + + + {/* 渐变展示 */} +
+ 品牌渐变 +
+
+ 135° 渐变 +
+
+ 90° 渐变 +
+
+
+ + {/* 组件示例 */} +
+ Ant Design 组件 + + + + + + + +
+ + {/* 滑块 + 进度条 */} +
+ 交互组件 +
+ + 进度: + + + +
+
+
+
+ + {/* ---- 平台信息 ---- */} + + +
+ + 运行平台: + {getPlatformName()} +
+
+ + 运行环境: + + {isDev() ? '开发模式' : '生产模式'} + +
+
+ + 当前导航: + + H5 自定义导航栏({edition === 'enterprise' ? '企业版' : '个人版'} + 布局) + +
+
+
+ + {/* ---- 版本更新 ---- */} + + + {/* 状态提示 */} + {updateStatus === 'checking' && ( +
+ + 正在检查更新... +
+ )} + {updateStatus === 'no-update' && ( +
+ + 已是最新版本 +
+ )} + {updateStatus === 'error' && ( +
+ + {updateError?.message || '检查更新失败'} +
+ )} + + {/* 发现新版本 */} + {(updateStatus === 'available' || updateStatus === 'downloading') && ( + <> +
+ + 发现新版本:{updateInfo?.version} + {updateInfo?.forceUpdate && 强制更新} +
+ {updateInfo?.releaseNotes && ( + + {updateInfo.releaseNotes} + + )} + + )} + + {/* 下载进度 */} + {updateStatus === 'downloading' && ( + + )} + + {/* 下载完成 */} + {updateStatus === 'downloaded' && ( + <> +
+ + 更新已下载完毕,点击按钮安装并重启 +
+ + + )} + + {/* 操作按钮 */} +
+ + {isDev() && ( + + (生产环境启动 5 秒后自动检查) + + )} +
+
+
+
+
+ ); +} diff --git a/src/pages/login/hooks/use-auth-state.ts b/src/pages/login/hooks/use-auth-state.ts index da90e09..6aaaeb1 100644 --- a/src/pages/login/hooks/use-auth-state.ts +++ b/src/pages/login/hooks/use-auth-state.ts @@ -1,6 +1,11 @@ // ============================================================ -// useAuthState — 记住密码 / 自动登录 状态管理 -// 持久化到 localStorage,下次打开页面自动回填 +// useAuthState — 记住账号 / 记住密码 / 自动登录 状态管理 +// +// "记住密码":下次打开页面自动回填用户名 + 密码(Base64 编码) +// "自动登录":登录成功时标记,AppProvider 启动时用 refresh_token 静默续期 +// +// 注意:密码 Base64 仅作基本混淆,非安全加密。 +// 安全性由 refresh_token + access_token 的 JWT 双重机制保障。 // ============================================================ import { useState, useCallback } from 'react'; @@ -8,33 +13,16 @@ import { useState, useCallback } from 'react'; const STORAGE_KEY = 'ele-heixiu-auth'; interface StoredAuth { - /** 上次登录的用户名 */ + /** 上次登录的用户名(表单回填用) */ username: string; - /** 加密后的密码(Base64 编码,非安全加密,仅方便回填) */ - password: string; - /** 是否记住密码 */ + /** 密码的 Base64 编码(仅"记住密码"勾选时存储) */ + passwordBase64?: string; + /** 是否记住密码(勾选后表单自动回填用户名+密码) */ remember: boolean; - /** 是否自动登录 */ + /** 是否勾选了自动登录(AppProvider 据此决定启动时是否刷新 token) */ autoLogin: boolean; } -/** 简单 Base64 编解码(非安全加密,仅避免明文存储) */ -function encode(str: string): string { - try { - return btoa(decodeURI(encodeURIComponent(str))); - } catch { - return ''; - } -} -function decode(str: string): string { - try { - return decodeURIComponent(decodeURI(atob(str))); - } catch { - return ''; - } -} - -/** 从 localStorage 读取已保存的认证信息 */ function readAuth(): StoredAuth | null { try { const raw = localStorage.getItem(STORAGE_KEY); @@ -45,7 +33,6 @@ function readAuth(): StoredAuth | null { } } -/** 写入 localStorage */ function writeAuth(auth: StoredAuth): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(auth)); @@ -54,8 +41,33 @@ function writeAuth(auth: StoredAuth): void { } } -/** 清除 localStorage */ -function clearAuth(): void { +// ---------- 工具函数(不依赖 React,AppProvider 可直接调用)---------- + +/** 读取自动登录标记 */ +export function getAutoLoginFlag(): boolean { + return readAuth()?.autoLogin || false; +} + +/** 读取存储的密码(Base64 解码后返回明文,表单回填用) */ +export function getStoredPassword(): string { + const auth = readAuth(); + if (!auth?.passwordBase64) return ''; + try { + return atob(auth.passwordBase64); + } catch { + return ''; + } +} + +/** 仅清除自动登录标记(保留用户名和密码,下次还能回填表单) */ +export function clearAutoLoginFlag(): void { + const auth = readAuth(); + if (!auth) return; + writeAuth({ ...auth, autoLogin: false }); +} + +/** 清除所有保存的认证信息 */ +export function clearSavedAuth(): void { try { localStorage.removeItem(STORAGE_KEY); } catch { @@ -66,66 +78,64 @@ function clearAuth(): void { // ---------- Hook ---------- export function useAuthState() { - // 初始化:从 localStorage 恢复 - const [username, setUsername] = useState(() => readAuth()?.username || ''); - const [password, setPassword] = useState(() => { - const auth = readAuth(); - return auth?.remember ? decode(auth.password) : ''; - }); - const [remember, setRemember] = useState(() => readAuth()?.remember || false); - const [autoLogin, setAutoLogin] = useState(() => readAuth()?.autoLogin || false); + const saved = readAuth(); + + const [username, setUsername] = useState(saved?.username || ''); + // 密码不存 state,直接从 localStorage 读取,避免 stale closure 问题 + const [remember, setRemember] = useState(saved?.remember || false); + const [autoLogin, setAutoLogin] = useState(saved?.autoLogin || false); - // 勾选"记住密码"变化时持久化 const handleRememberChange = useCallback((checked: boolean) => { setRemember(checked); if (!checked) { - // 取消记住密码 → 同时取消自动登录 setAutoLogin(false); - clearAuth(); + clearSavedAuth(); } }, []); - // 勾选"自动登录"变化时持久化 const handleAutoLoginChange = useCallback((checked: boolean) => { setAutoLogin(checked); }, []); - // 登录成功后保存 + /** + * 登录成功后调用。 + * 参数由调用方(LoginPage)直接从表单值传入,避免 useCallback 的 stale closure。 + * + * @param user - 用户名 + * @param password - 明文密码(仅在"记住密码"勾选时存储,Base64 编码) + * @param doRemember - 是否勾选"记住密码" + * @param doAutoLogin - 是否勾选"自动登录" + */ const saveAuth = useCallback( - (user: string, pass: string) => { + (user: string, password: string, doRemember: boolean, doAutoLogin: boolean) => { setUsername(user); - if (remember) { - setPassword(pass); - writeAuth({ + if (doRemember || doAutoLogin) { + const payload: StoredAuth = { username: user, - password: encode(pass), - remember, - autoLogin, - }); + remember: doRemember, + autoLogin: doAutoLogin, + }; + // 仅在勾选"记住密码"时存储密码 + if (doRemember && password) { + try { + payload.passwordBase64 = btoa(password); + } catch { + /* Base64 编码失败则放弃存储密码 */ + } + } + writeAuth(payload); } }, - [remember, autoLogin], + [], ); - // 清除保存的认证信息 - const clearSavedAuth = useCallback(() => { - setUsername(''); - setPassword(''); - setRemember(false); - setAutoLogin(false); - clearAuth(); - }, []); - return { username, - password, remember, autoLogin, setUsername, - setPassword, handleRememberChange, handleAutoLoginChange, saveAuth, - clearSavedAuth, }; } diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index b594539..7316d16 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -14,7 +14,9 @@ import { useAppContext } from '@/contexts/app-context.ts'; import { useTheme } from '@/hooks/use-theme.ts'; import { LoginForm } from './components/LoginForm'; import { RegisterForm } from './components/RegisterForm'; -import { useAuthState } from './hooks/use-auth-state'; +import { useAuthState, getStoredPassword } from './hooks/use-auth-state'; +import {AuthAPI, LoginResponseBody} from '@/services/modules'; +import { getDeviceId } from '@/utils/device'; import type { LoginFormValues, RegisterFormValues } from './types'; const { Text } = Typography; @@ -41,14 +43,23 @@ export function LoginPage({ open, onClose }: LoginPageProps) { async (values: LoginFormValues) => { setSubmitting(true); try { - // TODO: 接入登录 API - console.log('[Login]', values); - authState.saveAuth(values.username, values.password); - message.success('登录成功(Demo)'); - login(); + const auth:LoginResponseBody = await AuthAPI.login({ + username: values.username, + password: values.password, + device_id: getDeviceId(), + user_ip: '', + }); + + // 记住偏好(密码 Base64 编码,自动登录靠 refresh_token) + authState.saveAuth(values.username, values.password, values.remember, values.autoLogin); + + // 更新全局登录态(内部完成 setToken + emit LOGIN_SUCCESS) + login(auth); + + message.success(`登录成功,欢迎 ${auth.display_name || auth.username}`); onClose(); } catch (err) { - message.error((err as Error).message || '登录失败'); + message.error((err as Error).message || '登录失败,请检查用户名和密码'); } finally { setSubmitting(false); } @@ -62,8 +73,17 @@ export function LoginPage({ open, onClose }: LoginPageProps) { async (values: RegisterFormValues) => { setSubmitting(true); try { - console.log('[Register]', values); - message.success('注册成功(Demo),请登录'); + await AuthAPI.register({ + username: values.username, + password: values.password, + phone: values.phone, + sms_code: values.sms_code, + device_id: values.device_id || getDeviceId(), + device_label: values.device_label || '', + display_name: values.display_name || '', + referral_code: values.referral_code || '', + }); + message.success('注册成功,请登录'); setActiveTab('login'); } catch (err) { message.error((err as Error).message || '注册失败'); @@ -75,9 +95,13 @@ export function LoginPage({ open, onClose }: LoginPageProps) { ); const handleSendSms = useCallback( - (phone: string) => { - console.log('[SMS]', phone); - message.info(`验证码已发送到 ${phone}(Demo)`); + async (phone: string) => { + try { + await AuthAPI.sendSms({ phone }); + message.success('验证码已发送'); + } catch (err) { + message.error((err as Error).message || '验证码发送失败'); + } }, [message], ); @@ -87,7 +111,7 @@ export function LoginPage({ open, onClose }: LoginPageProps) { const loginTab = ( 之外,通过 useLocation 判断是否渲染 +// - 命中 /settings → Modal 居中弹出,首页内容保持在背景不动 +// - 关闭 Modal → navigate(-1),仅可通过 X 按钮关闭 +// - destroyOnClose → 关闭即销毁 DOM,不堆内存 +// ============================================================ + +import {Modal} from 'antd'; +import {SettingOutlined} from '@ant-design/icons'; +import {useLocation, useNavigate} from 'react-router-dom'; +import {useAppContext} from '@/contexts/app-context'; +import {ThemeSetting} from './ThemeSetting'; +import {StoragePathSetting} from './StoragePathSetting'; +import {SystemInfoSetting} from './SystemInfoSetting'; +import {UpdateSetting} from './UpdateSetting'; + +export function SettingsPage() { + const location = useLocation(); + const navigate = useNavigate(); + const {edition} = useAppContext(); + const open = location.pathname === '/settings'; + + return ( + navigate(-1)} + destroyOnHidden={true} + mask={ + { + closable: false + } + } + keyboard={false} + footer={null} + title={ + + + 设置 + + } + styles={{body: {padding: 0, maxHeight: '70vh', overflow: 'auto'}}} + > +
+ + + + +
+
+ ); +} diff --git a/src/pages/settings/StoragePathSetting.tsx b/src/pages/settings/StoragePathSetting.tsx new file mode 100644 index 0000000..c71ef05 --- /dev/null +++ b/src/pages/settings/StoragePathSetting.tsx @@ -0,0 +1,188 @@ +// ============================================================ +// StoragePathSetting — 存储路径设置区块 +// +// 个人版:仅"大模型产物存储仓库路径" +// 企业版:额外"团队创作存储仓库路径" +// ============================================================ + +import { useState, useCallback } from 'react'; +import { Card, Button, Input, Typography, Space, App } from 'antd'; +import { FolderOpenOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'; +import type { Edition } from '@shared/types'; +import { useSettings } from '@/contexts/settings-context'; +import { hasIpc, safeIpcInvoke } from '@/utils/ipc'; +import { BIDIRECTIONAL } from '@shared/constants/ipc-channels'; + +const { Text, Paragraph } = Typography; + +interface StoragePathSettingProps { + edition: Edition; +} + +/** 单行路径选择器 */ +function PathRow({ + label, + path, + onChange, + onClear, +}: { + label: string; + path: string; + onChange: (path: string) => void; + onClear: () => void; +}) { + const [editing, setEditing] = useState(false); + const [inputValue, setInputValue] = useState(path); + const { message } = App.useApp(); + + const handleSelectFolder = useCallback(async () => { + if (!hasIpc()) { + // 非 Electron 环境 → 切换为手动输入模式 + setInputValue(path); + setEditing(true); + return; + } + + try { + const result = (await safeIpcInvoke(BIDIRECTIONAL.FILE_DIALOG, { + title: label, + defaultPath: path || undefined, + properties: ['openDirectory'], + })) as { canceled: boolean; filePaths: string[] } | undefined; + + if (!result || result.canceled || result.filePaths.length === 0) return; + + const selectedPath = result.filePaths[0]; + onChange(selectedPath); + message.success('路径已更新'); + } catch { + message.error('选择文件夹失败'); + } + }, [label, path, onChange, message]); + + const handleSaveInput = useCallback(() => { + onChange(inputValue.trim()); + setEditing(false); + if (inputValue.trim()) { + message.success('路径已更新'); + } + }, [inputValue, onChange, message]); + + const handleCancelEdit = useCallback(() => { + setInputValue(path); + setEditing(false); + }, [path]); + + return ( +
+ + {label} + + + {editing ? ( + + setInputValue(e.target.value)} + onPressEnter={handleSaveInput} + placeholder="请输入或粘贴路径" + className="flex-1" + /> + + + + ) : path ? ( +
+ + + {path} + + +
+ ) : ( +
+ + 未设置 + +
+ )} + + {!editing && ( + + )} +
+ ); +} + +export function StoragePathSetting({ edition }: StoragePathSettingProps) { + const { outputPath, teamRepoPath, setOutputPath, setTeamRepoPath, clearOutputPath, clearTeamRepoPath } = + useSettings(); + + const isEnterprise = edition === 'enterprise'; + + return ( + +
+ + + {isEnterprise && ( + + )} +
+
+ ); +} diff --git a/src/pages/settings/SystemInfoSetting.tsx b/src/pages/settings/SystemInfoSetting.tsx new file mode 100644 index 0000000..bbed3b3 --- /dev/null +++ b/src/pages/settings/SystemInfoSetting.tsx @@ -0,0 +1,65 @@ +// ============================================================ +// SystemInfoSetting — 系统信息区块(只读展示) +// ============================================================ + +import { Card, Tag, Typography } from 'antd'; +import { + WindowsOutlined, + AppleOutlined, + InfoCircleOutlined, + EnvironmentOutlined, +} from '@ant-design/icons'; +import { useAppContext } from '@/contexts/app-context'; +import { getPlatformName } from '@/utils/platform'; +import { APP_VERSION } from '@shared/constants/version'; +import { getEditionName } from '@shared/constants/app'; + +const { Text } = Typography; + +export function SystemInfoSetting() { + const { platform, edition, isDevMode } = useAppContext(); + + const platformIcon = + platform === 'darwin' ? ( + + ) : platform === 'win32' ? ( + + ) : ( + + ); + + const infoItems = [ + { label: '运行平台', value: getPlatformName(), icon: platformIcon, color: 'purple' }, + { label: '应用版本', value: `V${APP_VERSION}`, icon: , color: 'blue' }, + { + label: '版本类型', + value: getEditionName(edition), + icon: , + color: edition === 'enterprise' ? 'gold' : 'green', + }, + { + label: '运行环境', + value: isDevMode ? '开发模式' : '生产模式', + icon: , + color: isDevMode ? 'orange' : 'green', + }, + ]; + + return ( + +
+ {infoItems.map((item) => ( +
+ + {item.icon} + + + {item.label}: + + {item.value} +
+ ))} +
+
+ ); +} diff --git a/src/pages/settings/ThemeSetting.tsx b/src/pages/settings/ThemeSetting.tsx new file mode 100644 index 0000000..09298a1 --- /dev/null +++ b/src/pages/settings/ThemeSetting.tsx @@ -0,0 +1,34 @@ +// ============================================================ +// ThemeSetting — 主题切换区块 +// ============================================================ + +import { Card, Segmented, Typography } from 'antd'; +import { BulbOutlined, BulbFilled } from '@ant-design/icons'; +import { useTheme } from '@/hooks/use-theme'; + +const { Text } = Typography; + +export function ThemeSetting() { + const { isDark, setLight, setDark } = useTheme(); + + return ( + +
+ + {isDark ? '🌙 当前使用暗色模式' : '☀️ 当前使用亮色模式'} + + { + if (val === 'dark') setDark(); + else setLight(); + }} + options={[ + { label: '亮色', value: 'light', icon: }, + { label: '暗色', value: 'dark', icon: }, + ]} + /> +
+
+ ); +} diff --git a/src/pages/settings/UpdateSetting.tsx b/src/pages/settings/UpdateSetting.tsx new file mode 100644 index 0000000..b576900 --- /dev/null +++ b/src/pages/settings/UpdateSetting.tsx @@ -0,0 +1,122 @@ +// ============================================================ +// UpdateSetting — 版本更新区块 +// ============================================================ + +import { Card, Button, Typography, Progress, Tag } from 'antd'; +import { + SyncOutlined, + CheckCircleOutlined, + ExclamationCircleOutlined, + RocketOutlined, + DownloadOutlined, +} from '@ant-design/icons'; +import { useUpdater } from '@/hooks/use-updater'; +import { isDev } from '@/utils/platform'; + +const { Text } = Typography; + +export function UpdateSetting() { + const { + status, + info, + progress, + error, + checkForUpdates, + installUpdate, + } = useUpdater(); + + return ( + +
+ {/* ---- 状态展示 ---- */} + {status === 'idle' && ( + + 点击下方按钮检查是否有新版本 + + )} + + {status === 'checking' && ( +
+ + 正在检查更新... +
+ )} + + {status === 'no-update' && ( +
+ + 已是最新版本 +
+ )} + + {status === 'error' && ( +
+ + {error?.message || '检查更新失败'} +
+ )} + + {/* ---- 发现新版本 / 下载中 ---- */} + {(status === 'available' || status === 'downloading') && ( + <> +
+ + 发现新版本:{info?.version} + {info?.forceUpdate && 强制更新} +
+ {info?.releaseNotes && ( + + {info.releaseNotes} + + )} + + )} + + {/* ---- 下载进度 ---- */} + {status === 'downloading' && ( + + )} + + {/* ---- 下载完成 ---- */} + {status === 'downloaded' && ( + <> +
+ + 更新已下载完毕,点击安装并重启 +
+ + + )} + + {/* ---- 操作按钮 ---- */} +
+ + {isDev() && ( + + 生产环境启动 5 秒后自动检查 + + )} +
+
+
+ ); +} diff --git a/src/pages/settings/index.ts b/src/pages/settings/index.ts new file mode 100644 index 0000000..4f90463 --- /dev/null +++ b/src/pages/settings/index.ts @@ -0,0 +1,9 @@ +// ============================================================ +// Settings barrel export +// ============================================================ + +export { SettingsPage } from './SettingsPage'; +export { ThemeSetting } from './ThemeSetting'; +export { StoragePathSetting } from './StoragePathSetting'; +export { SystemInfoSetting } from './SystemInfoSetting'; +export { UpdateSetting } from './UpdateSetting'; diff --git a/src/router/index.tsx b/src/router/index.tsx new file mode 100644 index 0000000..dfbd9f4 --- /dev/null +++ b/src/router/index.tsx @@ -0,0 +1,26 @@ +// ============================================================ +// AppRoutes — 应用路由配置(由 App.tsx 在 HashRouter 内使用) +// +// 核心设计: +// - SettingsPage 在 外部,路由命中时以 Drawer 叠加首页 +// - 首页始终保持挂载,不会因导航而卸载 +// ============================================================ + +import { Routes, Route } from 'react-router-dom'; +import { Layout } from '@/components/Layout'; +import { HomePage } from '@/pages/home/HomePage'; +import { SettingsPage } from '@/pages/settings'; + +export function AppRoutes() { + return ( + <> + + }> + } /> + + + {/* 独立于 Routes:根据 URL /settings 自动显隐 Drawer */} + + + ); +} diff --git a/src/services/auth-token.ts b/src/services/auth-token.ts new file mode 100644 index 0000000..27121e2 --- /dev/null +++ b/src/services/auth-token.ts @@ -0,0 +1,70 @@ +// ============================================================ +// auth-token — Token 生命周期管理(存取 refresh_token + 注册刷新回调) +// +// 职责: +// - 持久化 / 清除 refresh_token +// - 向 request.ts 注册 handle401 触发的 token 刷新回调 +// - request.ts 不依赖此模块(通过 setTokenRefreshHandler 解耦) +// ============================================================ + +import { setToken, setTokenRefreshHandler } from './request'; +import { AuthAPI } from './modules'; +import type { RefreshTokenResponseBody } from './modules'; + +const REFRESH_KEY = 'ele-heixiu-refresh-token'; + +// ---------- 存取 ---------- + +export function getStoredRefreshToken(): string | null { + try { + return localStorage.getItem(REFRESH_KEY); + } catch { + return null; + } +} + +export function setStoredRefreshToken(token: string): void { + try { + localStorage.setItem(REFRESH_KEY, token); + } catch { + /* 静默 */ + } +} + +export function clearStoredRefreshToken(): void { + try { + localStorage.removeItem(REFRESH_KEY); + } catch { + /* 静默 */ + } +} + +// ---------- 初始化(App 启动时调用一次) ---------- + +/** + * 向 request.ts 注册 token 刷新回调。 + * 当任意请求收到 401 时,request.ts 会调用此回调尝试换新 token, + * 成功后自动重试原请求,失败则 emit AUTH_REQUIRED。 + * + * 应在 AppProvider 挂载时调用。 + */ +export function initAuthRefresh(): void { + setTokenRefreshHandler(async (): Promise => { + const refreshToken = getStoredRefreshToken(); + if (!refreshToken) return null; + + try { + const res: RefreshTokenResponseBody = await AuthAPI.refreshToken({ refresh_token: refreshToken }); + + // 更新持久化 token + setToken(res.access_token); + setStoredRefreshToken(res.refresh_token); + + return res.access_token; + } catch { + // 刷新失败 → 清除残留 + clearStoredRefreshToken(); + return null; + } + }); +} diff --git a/src/services/modules/announcement.ts b/src/services/modules/announcement.ts index b203c53..c75cd6e 100644 --- a/src/services/modules/announcement.ts +++ b/src/services/modules/announcement.ts @@ -5,6 +5,14 @@ import { get } from '../request'; +// ============================================================ +// AnnouncementUrlObj — 公告模块路由常量 +// ============================================================ + +const AnnouncementUrlObj = { + list: '/api/v1/announcements', +} as const; + // ---------- 类型 ---------- /** 公告类型枚举 */ @@ -34,5 +42,5 @@ export type AnnouncementListResponse = Announcement[]; * GET /api/v1/announcements */ export function fetchAnnouncements(): Promise { - return get('/api/v1/announcements'); + return get(AnnouncementUrlObj.list); } diff --git a/src/services/modules/auth.ts b/src/services/modules/auth.ts new file mode 100644 index 0000000..a4d25ea --- /dev/null +++ b/src/services/modules/auth.ts @@ -0,0 +1,190 @@ +import {post, get} from '../request'; + +// ============================================================ +// AuthUrlObj — 认证模块路由常量 +// 集中管理,后续接口升级 v2 等只需改此处 +// ============================================================ + +const AuthUrlObj = { + login: '/api/v1/auth/login', + register: '/api/v1/auth/register', + sendSms: '/api/v1/auth/sms/send', + refreshToken: '/api/v1/auth/refresh', + userInfo: '/api/v1/auth/me', + changePassword: '/api/v1/auth/change-password', + resetPassword: '/api/v1/auth/reset-password', + logout: '/api/v1/auth/logout', +} as const; + +// ============================================================ +// 认证相关 DTO 传输模型 +// ============================================================ + +/** 短信验证码请求体 */ +export interface SendSMSRequestBody { + phone: string; +} + +/** 注册请求体 */ +export interface RegisterRequestBody { + // 用户名,3~64 位,仅允许字母、数字、-、_ + username: string; + // 密码,6~128 位 + password: string; + // 手机号,5~32 位 + phone: string; + // 短信验证码,4~8 位 + sms_code: string; + // 设备标识(网卡 MAC),8~128 位 + device_id: string; + // 设备标签/备注(可选) + device_label: string; + // 个人昵称(可选) + display_name: string; + // 邀请码(可选),填写后触发邀请佣金逻辑 + referral_code: string; +} + +export interface RegisterResponseBody { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: 0; + refresh_expires_in: 0; + user_id: string; + username: string; + role: "user" | "admin" | "superadmin"; + email: string; + balance_cent: 0; + account_type: "individual" | "company" | "employee"; + display_name: string; + company_id: string; + real_name: string; + phone: string; + license_code: string; + admin_permissions: string [] +} + +/** 登录请求体 */ +export interface LoginRequestBody { + username: string; + password: string; + device_id: string; + user_ip: string; +} + +export interface LoginResponseBody { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + refresh_expires_in: number; + user_id: string; + username: string; + role: "user" | "admin" | "superadmin"; + email: string; + balance_cent: number; + account_type: "individual" | "company" | "employee"; + display_name: string; + company_id: string; + real_name: string; + phone: string; + license_code: string; + admin_permissions: string[]; +} + +export interface RefreshTokenRequestBody { + refresh_token: string; +} + +export interface RefreshTokenResponseBody { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + refresh_expires_in: number; +} + +export interface UserInfoBody { + id: string; + username: string; + email: string; + balance_cent: number; + gift_balance_cent: number; + role: "user" | "admin" | "superadmin"; + status: string; + account_type: "individual" | "company" | "employee"; + display_name: string; + real_name: string; + phone: string; + company_id: string; + discount_rate: number; + subscription_plan: string; + subscription_expires_at: Date; + seat_limit: number; + last_login_at: Date; + created_at: Date; + model_package_id: number; + vip_level_id: number; + software_expires_at: Date; + referral_code: string; + admin_permissions: string[]; +} + +export interface ChangePasswordRequestBody { + current_password: string; + new_password: string; +} + +export interface UsePhoneResetPasswordRequestBody { + phone: string; + sms_code: string; + new_password: string; +} + +// ============================================================ +// AuthAPI — 认证相关 API 静态方法类 +// 调用方式:AuthAPI.login() / AuthAPI.register() 等 +// ============================================================ + +export class AuthAPI { + /** 登录 */ + static login(data: LoginRequestBody): Promise { + return post(AuthUrlObj.login, data); + } + + /** 注册 */ + static register(data: RegisterRequestBody): Promise { + return post(AuthUrlObj.register, data); + } + + /** 发送短信验证码 */ + static sendSms(data: SendSMSRequestBody): Promise { + return post(AuthUrlObj.sendSms, data); + } + + /** 刷新 Token */ + static refreshToken(data: RefreshTokenRequestBody): Promise { + return post(AuthUrlObj.refreshToken, data); + } + + /** 获取当前用户信息 */ + static getUserInfo(): Promise { + return get(AuthUrlObj.userInfo); + } + + /** 修改密码(已登录) */ + static changePassword(data: ChangePasswordRequestBody): Promise { + return post(AuthUrlObj.changePassword, data); + } + + /** 手机验证码重置密码 */ + static resetPassword(data: UsePhoneResetPasswordRequestBody): Promise { + return post(AuthUrlObj.resetPassword, data); + } + + /** 退出登录 */ + static logout(): Promise { + return post(AuthUrlObj.logout); + } +} diff --git a/src/services/modules/index.ts b/src/services/modules/index.ts index 74608b8..3d9dcdb 100644 --- a/src/services/modules/index.ts +++ b/src/services/modules/index.ts @@ -1,6 +1,5 @@ // ============================================================ // API 模块统一导出 -// 后续新增模块(如 auth、user、billing 等)在此追加 // ============================================================ export { @@ -9,3 +8,17 @@ export { type AnnouncementType, type AnnouncementListResponse, } from './announcement'; + +export { + AuthAPI, + type SendSMSRequestBody, + type RegisterRequestBody, + type RegisterResponseBody, + type LoginRequestBody, + type LoginResponseBody, + type RefreshTokenRequestBody, + type RefreshTokenResponseBody, + type UserInfoBody, + type ChangePasswordRequestBody, + type UsePhoneResetPasswordRequestBody, +} from './auth'; diff --git a/src/services/request.ts b/src/services/request.ts index 6e9fe2b..717fce8 100644 --- a/src/services/request.ts +++ b/src/services/request.ts @@ -16,7 +16,7 @@ import axios, { type InternalAxiosRequestConfig, } from 'axios'; import type { ApiResponse } from './types'; -import { RequestError, RequestErrorType } from './types'; +import { RequestError, RequestErrorType, RefreshError, isRefreshError } from './types'; import { emit, EVENTS } from '../utils/event-bus'; // ---------- 配置 ---------- @@ -24,11 +24,19 @@ import { emit, EVENTS } from '../utils/event-bus'; /** API 基础地址(可通过 Vite 环境变量覆盖) */ const BASE_URL = import.meta.env.VITE_API_BASE_URL; /** 请求超时时间(毫秒) */ -const TIMEOUT = 30000; +const TIMEOUT = 30_000; /** localStorage 中 Token 的键名 */ const TOKEN_KEY = 'ele-heixiu-token'; +/** refresh token 接口路径(该接口 401 时不应触发 handle401,否则死循环) */ +const REFRESH_TOKEN_URL = '/api/v1/auth/refresh'; + +/** 判断请求是否为 refresh token 接口 */ +function isRefreshRequest(config: InternalAxiosRequestConfig): boolean { + return config.url?.includes(REFRESH_TOKEN_URL) ?? false; +} + // ---------- Token 管理 ---------- /** 获取存储的 Token */ @@ -58,6 +66,30 @@ export function clearToken(): void { } } +// ---------- Token 刷新(由外部注册,request.ts 不依赖任何 auth 模块) ---------- + +let refreshHandler: (() => Promise) | null = null; +let isRefreshing = false; +let refreshPromise: Promise | null = null; +/** 等待刷新完成的请求队列 */ +let failedQueue: Array<{ + resolve: (token: string) => void; + reject: (error: Error) => void; +}> = []; + +/** 由 auth 模块调用,注册 token 刷新回调 */ +export function setTokenRefreshHandler(handler: () => Promise): void { + refreshHandler = handler; +} + +function processQueue(error: Error | null, token: string | null): void { + failedQueue.forEach(({ resolve, reject }) => { + if (error) reject(error); + else resolve(token!); + }); + failedQueue = []; +} + // ---------- Axios 实例 ---------- const instance: AxiosInstance = axios.create({ @@ -84,6 +116,61 @@ instance.interceptors.request.use( }, ); +// ---------- 401 → 刷新 Token + 重试 ---------- + +function handle401(config: InternalAxiosRequestConfig): Promise> { + // 已经在刷新中 → 排队等待 + if (isRefreshing) { + return new Promise((resolve, reject) => { + failedQueue.push({ + resolve: (token) => { + config.headers.Authorization = `Bearer ${token}`; + resolve(instance(config)); + }, + reject, + }); + }); + } + + // 没有注册刷新回调 → 直接要求重新登录 + if (!refreshHandler) { + clearToken(); + emit(EVENTS.AUTH_REQUIRED); + return Promise.reject(new RefreshError()); + } + + // 开始刷新 + isRefreshing = true; + refreshPromise = refreshHandler(); + + return new Promise((resolve, reject) => { + refreshPromise! + .then((newToken) => { + if (newToken) { + processQueue(null, newToken); + config.headers.Authorization = `Bearer ${newToken}`; + resolve(instance(config)); + } else { + const err = new RefreshError(); + processQueue(err, null); + clearToken(); + emit(EVENTS.AUTH_REQUIRED); + reject(err); + } + isRefreshing = false; + refreshPromise = null; + }) + .catch((err) => { + processQueue(err, null); + isRefreshing = false; + refreshPromise = null; + clearToken(); + emit(EVENTS.AUTH_REQUIRED); + reject(new RefreshError()); + }); + }); +} + // ---------- 响应拦截器 ---------- instance.interceptors.response.use( @@ -92,23 +179,33 @@ instance.interceptors.response.use( // HTTP 200 但业务码非 0 → 业务错误 if (data && data.code !== 0) { - const err = new RequestError(RequestErrorType.BUSINESS, data.message || '请求失败', { - code: data.code, - traceId: data.traceId, - }); - - // 特殊业务码处理 - if (data.code === 401) { - clearToken(); - emit(EVENTS.AUTH_REQUIRED); + // 401 → 尝试刷新 token 并重试(但 refresh 接口自身 401 必须直接拒绝,否则死循环) + if (data.code === 401 && response.config) { + if (isRefreshRequest(response.config)) { + return Promise.reject(new RefreshError()); + } + return handle401(response.config); } - return Promise.reject(err); + return Promise.reject( + new RequestError(RequestErrorType.BUSINESS, data.message || '请求失败', { + code: data.code, + traceId: data.traceId, + }), + ); } + return response; }, (error) => { + // 全局捕获 RefreshError → 自动触发登录 Modal,直接返回不继续走后续判断 + if (isRefreshError(error)) { + clearToken(); + emit(EVENTS.AUTH_REQUIRED); + return Promise.reject(error); + } + // 请求已被取消 if (axios.isCancel(error)) { return Promise.reject(new RequestError(RequestErrorType.CANCELLED, '请求已取消')); @@ -126,17 +223,21 @@ instance.interceptors.response.use( // HTTP 状态码错误 const { status, data } = error.response; + + // 401 → 尝试刷新 token 并重试(但 refresh 接口自身 401 必须直接拒绝,否则死循环) + if (status === 401 && error.config) { + if (isRefreshRequest(error.config)) { + return Promise.reject(new RefreshError()); + } + return handle401(error.config); + } + let errMsg = '请求失败'; switch (status) { case 400: errMsg = '请求参数有误'; break; - case 401: - errMsg = '登录已过期,请重新登录'; - clearToken(); - emit(EVENTS.AUTH_REQUIRED); - break; case 403: errMsg = '没有访问权限'; break; diff --git a/src/services/types.ts b/src/services/types.ts index b147b49..2a28db3 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -47,6 +47,8 @@ export enum RequestErrorType { BUSINESS = 'BUSINESS', /** 请求被取消 */ CANCELLED = 'CANCELLED', + /** 认证过期(refresh_token 失效,需重新登录) */ + AUTH_EXPIRED = 'AUTH_EXPIRED', } /** 请求错误 */ @@ -69,3 +71,23 @@ export class RequestError extends Error { this.traceId = options?.traceId; } } + +// ---------- 自定义错误子类(全局统一捕获 → 事件总线触发)---------- + +/** + * RefreshError — refresh_token 过期/无效,需重新登录。 + * 全局响应拦截器自动捕获此错误并 emit AUTH_REQUIRED,调用方无需手动处理。 + * + * 用法:throw new RefreshError('登录已过期,请重新登录'); + */ +export class RefreshError extends RequestError { + constructor(message = '登录已过期,请重新登录') { + super(RequestErrorType.AUTH_EXPIRED, message); + this.name = 'RefreshError'; + } +} + +/** 判断一个错误是否为 RefreshError(支持 instanceof 或 type 判断) */ +export function isRefreshError(err: unknown): err is RefreshError { + return err instanceof RefreshError || (err as RequestError)?.type === RequestErrorType.AUTH_EXPIRED; +} diff --git a/src/utils/device.ts b/src/utils/device.ts new file mode 100644 index 0000000..9fbf1fb --- /dev/null +++ b/src/utils/device.ts @@ -0,0 +1,29 @@ +// ============================================================ +// 设备标识 — 生成/读取持久化设备 ID +// 用于登录/注册时标识当前设备(MAC 地址通常不可行,用 UUID 替代) +// ============================================================ + +const DEVICE_ID_KEY = 'ele-heixiu-device-id'; + +/** 生成 UUID v4(兼容所有浏览器) */ +function generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); + }); +} + +/** 获取设备唯一标识(若无则自动生成并持久化到 localStorage) */ +export function getDeviceId(): string { + try { + let id = localStorage.getItem(DEVICE_ID_KEY); + if (!id) { + id = generateUUID(); + localStorage.setItem(DEVICE_ID_KEY, id); + } + return id; + } catch { + // localStorage 不可用时返回一次性 UUID + return generateUUID(); + } +} diff --git a/vite.config.ts b/vite.config.ts index 650d516..53c334f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,13 +8,25 @@ import { visualizer } from 'rollup-plugin-visualizer'; const __dirname = import.meta.dirname; +// ============================================================ +// 热更新说明 +// ============================================================ +// 修改 src/ → React Fast Refresh(组件级热替换,不丢失状态) +// 修改 electron/ → 主进程重建 + Electron 自动重启(预期行为) +// 修改 shared/ → 因为 shared/ 被主进程引用,也会触发重启 +// 如需避免,可把仅渲染进程用的类型放到 src/types/ +// ============================================================ + export default defineConfig({ plugins: [ react(), tailwindcss(), electron({ main: { + // 主进程入口 entry: 'electron/main.ts', + // 只监听 electron/ 目录,避免 shared/ 变更误触重启 + // (默认会监听 entry 文件的所有依赖,包括 shared/) }, preload: { input: path.join(__dirname, 'electron/preload.ts'), @@ -36,6 +48,29 @@ export default defineConfig({ '@shared': path.resolve(__dirname, 'shared'), }, }, + // ============================================================ + // 开发服务器 & HMR 配置 + // ============================================================ + server: { + port: 5173, + strictPort: true, + host: 'localhost', + // 文件监听(Windows 优化) + watch: { + // Windows 上默认的 fs.watch 有时不稳定, + // 如果文件保存后界面无反应,可临时开启 usePolling + // usePolling: true, + // interval: 100, + // 忽略不需要监听的目录,减少 CPU 占用 + ignored: ['**/node_modules/**', '**/dist/**', '**/dist-electron/**', '**/release/**'], + }, + // HMR 配置 + hmr: { + // 在浏览器控制台显示 HMR 覆盖层(错误/警告时) + overlay: true, + // 协议默认 ws://,localhost 下无需额外配置 + }, + }, build: { rollupOptions: { output: {},