diff --git a/.env.development b/.env.development index 11b8f6c..ba18680 100644 --- a/.env.development +++ b/.env.development @@ -9,9 +9,5 @@ VITE_API_BASE_URL=https://www.heixiu.net # 主进程 — 更新检查 API(指向本地测试服务器) UPDATE_API_URL=http://127.0.0.1:8000 -# 版本类型 -VITE_EDITION=personal -EDITION=personal - # 应用标题后缀 VITE_APP_TITLE=船长·HeiXiu diff --git a/.env.production b/.env.production index a61dc74..5b9a7b0 100644 --- a/.env.production +++ b/.env.production @@ -10,9 +10,5 @@ VITE_API_BASE_URL=https://www.heixiu.net # 主进程 — 更新检查 API(生产服务器) UPDATE_API_URL=https://www.heixiu.net -# 版本类型 -VITE_EDITION=personal -EDITION=personal - # 应用标题后缀 VITE_APP_TITLE=船长·HeiXiu diff --git a/CHANGELOG.md b/CHANGELOG.md index e0dc5c8..b05b1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,57 @@ # 更新日志 > 每次发版前修改此文件,`npm run build:win` 打包后会自动读取生成 update-info.json。 +--- +## 0.0.24(2026-06-22) + +**去除构建时版本变量 + 窗口标题动态版控 + 退出登录保留缓存** + +### 版控体系重构 + +- **去除构建时 `EDITION` 环境变量**:`--personal`/`--enterprise` 参数从 `build.mjs` 移除,`vite.config.ts` 不再注入 `process.env.EDITION`,`.env.*` 清理相应行 +- **统一的安装包文件名**:`船长-HeiXiu-{os}-{版本号}-Setup.{ext}`(移除 `${env.EDITION}`) +- **`package.json` 简化**:移除 3 条 `:enterprise` 构建脚本 +- **`scripts/*.mjs` 统一**:`upload-oss`/`generate-update-info`/`publish-release` 不再接受 `edition` 参数 + +### 窗口标题动态版控 + +- **新格式**:`船长-HeiXiu-{os}-{版本号}`(未登录)/ `船长-HeiXiu-{os}-{版本号}·{版控}`(登录后) +- **运行时链路**:`AppProvider` 根据 `user.account_type` 派生 `edition` → `useEffect` → `window.appRuntime.setWindowEdition()` → IPC `set-window-title` → 主进程 `buildWindowTitle` → `mainWindow.setTitle` +- **Edtion 类型守卫**:`src/vite-env.d.ts` + `electron/electron-env.d.ts` 双端类型声明 +- **更新检查 API** 移除 `edition` 查询参数 + +### 本地存储生命周期修正 + +- **退出登录不再自动清缓存**:`AppProvider.logout()` 移除 `clearUserStorage()` 调用。数据已按 `user_id` 隔离,同一用户重新登录缓存直接命中 +- **`clearUserStorage` 竞态修复**:添加 `isOpen()` 前置守卫,防止数据库未初始化时执行 SQL;移除尾部 `closeDatabase()`,确保退出→重新登录周期内数据库持续可用 +- **手动清理保留**:`DataManageSetting` 的「清理缓存」按钮仍可主动触发 + +--- +## 0.0.23(2026-06-22) + +**三栏等比缩放修复 + 代码重组** + +### 三栏布局修复 + +- **约束逻辑修正**:双遍最小值约束改为 clamp + 超限兜底,修复窗口缩小至 1036px 以下时中列被挤出视口 +- **双触发器**:`ResizeObserver` + `window.resize` 事件双重驱动,防止 React 批量更新吞掉重绘 +- **三栏等比缩放**:旧算法只保 left:right 比例,中列被动吸收剩余;新算法通过 `prevWidthRef` 反推中列上次宽度,三栏按各自占比同步缩放——窗口从半屏拉到全屏保持 ≈1:1:1 + +### 代码重组(第二轮) + +- **src/utils/**:11 文件精简为 7 文件 —— `env.ts`(platform+device)、`bus.ts`(event-bus+ipc+logger)、`display.ts`(display+logo+type)、`media-upload.ts` +- **controls/**:14 文件精简为 6 文件 —— `simple-widgets.tsx`(6合1)、`upload-widgets.tsx`(ImageInput+VideoInput 去重)、`list-upload-widgets.tsx`(ImageListInput+AudioListInput) +- **FormField 提升**:从 `auth/components/` 提升到 `src/components/ui/`,`FieldConfig` 类型同步移到 `ui/types.ts` +- **data-sync 归位**:从 `settings/sections/` 移到 `infrastructure/storage/` +- **electron/main/ 分组**:3 个 IPC handler(log-ipc/safe-storage-ipc/storage-ipc)收拢到 `ipc/` 子目录 +- **shell/ → frame/**:`src/components/shell/` 重命名为 `frame/`,更直观表达"应用框架" +- **storage/ 分层**:15 文件平铺 → `core/`(4)+ `stores/`(5)+ `services/`(4)+ 入口 + +### 文档 + +- 37 个文件夹全部生成 README.md(不进 Git) +- PROJECT.md 目录树同步更新 + --- ## 0.0.22(2026-06-18) diff --git a/CLAUDE.md b/CLAUDE.md index e7e8dd6..e279827 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,9 @@ - 进行任务时都需要考虑安全、性能、主题切换、业务自定义错误、事件总线驱动方式以及日志记录; - 进行系统级别的操作时,需要考虑系统差异,并处理兼容性。如WINDOWS、MAC OS; -- 处理业务时,需要同时考虑兜底机制和业务自定义错误捕获; -- 所有设计思路、代码实现均需要考虑以上三个条件。 +- 处理业务时,需要同时考虑兜底机制和业务自定义错误捕获,比如后端在修改了某些数据的枚举值但没有通知时,需要能够正常转化对用户友好的提示信息; +- 新增文件时,需要注意各个文件夹当前平铺的文件数量,如果过多需要考虑合并文件或者合并为子文件夹; +- 所有设计思路、代码实现均需要考虑以上四个条件。 # 相关文档说明 diff --git a/PROJECT.md b/PROJECT.md index ce25bca..3d06d04 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -66,12 +66,13 @@ │ └── main/ # 主进程核心模块 │ │ ├── load-env.ts # .env.{mode} + .env.local 加载器 │ │ ├── logger.ts # 日志核心:JSON Lines / 每日轮转 / 批量冲刷 -│ │ ├── log-ipc.ts # 渲染进程日志 → 主进程日志桥接 │ │ ├── net-request.ts # Electron net.request 的 axios 风格封装 -│ │ ├── safe-storage-ipc.ts # safeStorage 加密/解密 IPC handler -│ │ ├── storage-ipc.ts # 沙箱文件存储 IPC handler(heixiu-data/) │ │ ├── updater.ts # 自动更新:自定义 Provider + 平台 Updater │ │ ├── menu/index.ts # 系统菜单(macOS Edit / Win 无菜单) +│ │ ├── ipc/ # IPC handler 集合 +│ │ │ ├── log-ipc.ts # 渲染进程日志 → 主进程日志桥接 +│ │ │ ├── safe-storage-ipc.ts # safeStorage 加密/解密 IPC handler +│ │ │ └── storage-ipc.ts # 沙箱文件存储 IPC handler(heixiu-data/) │ │ └── utils/ # 平台判断 / 图标路径 │ └── preload/ # 预加载脚本(contextBridge 桥接层) │ ├── device.ts # 硬件指纹采集(wmic/powershell | ioreg/sysctl) @@ -85,32 +86,39 @@ │ │ │ ├── AppProvider.tsx # 认证状态(login/logout/Token 刷新) │ │ │ ├── ThemeProvider.tsx # 亮/暗主题切换 + antd ConfigProvider │ │ │ └── SettingsProvider.tsx # 本地持久化设置(存储路径等) -│ │ ├── shell/ # 应用外壳 +│ │ ├── frame/ # 应用框架(外层结构 + 页面调度) │ │ │ ├── Layout.tsx # 全高 flex 列布局壳 │ │ │ ├── PageDispatcher.tsx # 通用页面调度器(Modal/Drawer/FloatingPanel) │ │ │ └── navbar/ # 导航栏(nav-config → NavBar → NavItem) │ │ └── ui/ # 通用 UI 原子组件 │ │ ├── FloatingPanel.tsx # 可拖拽非模态浮动面板 -│ │ └── LegalTextModal.tsx # 法律/协议文本 Modal +│ │ ├── LegalTextModal.tsx # 法律/协议文本 Modal +│ │ ├── FormField.tsx # 通用表单字段渲染器(数据驱动,从 auth/components 提升) +│ │ └── types.ts # FieldConfig / FieldRule / InputType 通用类型 │ ├── hooks/ # 自定义 Hooks │ │ ├── use-async.ts # 通用异步基础设施(竞态/取消/门控) │ │ ├── use-api.ts # 业务 API Hooks(Banner/模型/任务/账户/财务) │ │ └── use-updater.ts # 更新状态机(单例 IPC 监听) │ ├── infrastructure/ # 基础设施层 -│ │ └── storage/ # 本地加密数据库(sql.js + safeStorage + IPC) -│ │ ├── db-core.ts # sql.js WASM 初始化 + 通用 CRUD -│ │ ├── db-encrypt.ts # 序列化 → safeStorage 加密 → IPC 写磁盘 -│ │ ├── schema.ts # 6 张业务表 DDL(含索引) -│ │ ├── task-store.ts # 任务记录本地 CRUD -│ │ ├── order-store.ts # 订单记录本地 CRUD(充值 + VIP) -│ │ ├── param-store.ts # 参数历史快照(每模型最多 20 条) -│ │ ├── media-store.ts # 媒体缓存元数据管理 -│ │ ├── media-loader.ts # 媒体下载与三级缓存策略 -│ │ ├── settings-store.ts # 应用设置 key-value 存储 -│ │ ├── sync-manager.ts # 分页同步游标管理 -│ │ ├── user-scope.ts # 用户作用域隔离(JWT user_id) +│ │ └── storage/ # 本地加密数据库(core/ + stores/ + services/) +│ │ ├── index.ts # 统一入口:init/clear/stats + 全部 re-export │ │ ├── ipc-file.ts # 渲染进程端文件操作桥接 -│ │ └── index.ts # 统一入口:init/clear/stats +│ │ ├── core/ # 数据库核心(4 文件) +│ │ │ ├── db-core.ts # sql.js WASM 初始化 + 通用 CRUD +│ │ │ ├── db-encrypt.ts # 序列化 → safeStorage 加密 → IPC 写磁盘 +│ │ │ ├── schema.ts # 6 张业务表 DDL(含索引) +│ │ │ └── types.ts # 实体类型定义 +│ │ ├── stores/ # 实体 CRUD Store(5 文件) +│ │ │ ├── task-store.ts # 任务记录 +│ │ │ ├── order-store.ts # 订单记录(充值 + VIP) +│ │ │ ├── param-store.ts # 参数历史快照 +│ │ │ ├── media-store.ts # 媒体缓存元数据 +│ │ │ └── settings-store.ts # 应用设置 key-value +│ │ └── services/ # 高级服务(4 文件) +│ │ ├── media-loader.ts # 媒体下载与三级缓存 +│ │ ├── sync-manager.ts # 分页同步游标 +│ │ ├── data-sync.ts # 数据同步辅助(从 settings/sections 归位) +│ │ └── user-scope.ts # 用户作用域隔离(JWT user_id) │ ├── pages/ # 业务页面(按功能模块组织) │ │ ├── auth/ # 认证模块 │ │ │ ├── index.tsx # LoginPage Modal(登录/注册 Tabs) @@ -123,7 +131,13 @@ │ │ │ ├── HomeContext.tsx # 共享状态(选中模型/任务/分页) │ │ │ ├── panels/ # 三列面板容器(Left/Center/Right) │ │ │ ├── features/ # 功能模块(Banner/ModelSelector/ModelInputForm/TaskHistory/OutputPreview) -│ │ │ ├── controls/ # Widget 控件体系(12 种 Qt→React 映射 + WIDGET_MAP) +│ │ │ ├── controls/ # Widget 控件体系(6 种 Qt→React 映射 + WIDGET_MAP) +│ │ │ │ ├── types.ts # WidgetProps 统一接口 +│ │ │ │ ├── index.ts # WIDGET_MAP 注册表 +│ │ │ │ ├── simple-widgets.tsx # 6 个简单控件(Checkbox/LineInput/TextInput/ComboBox/SpinBox/DoubleSpinBox) +│ │ │ │ ├── upload-widgets.tsx # 单文件上传(ImageInput/VideoInput,共享 enable_switch) +│ │ │ │ ├── list-upload-widgets.tsx # 多文件拖拽上传(ImageListInput/AudioListInput) +│ │ │ │ └── SeedanceContent.tsx # 复合内容编排 │ │ │ └── hooks/ # useModelUI(param_schema → UIFieldConfig) │ │ ├── panels/ # NavBar 触发的功能面板页 │ │ │ ├── AccountInfo.tsx # 账户信息(用户/积分/套餐/统计) @@ -158,18 +172,14 @@ │ │ ├── transitions.css # 主题切换过渡(View Transitions API + CSS fallback) │ │ ├── base.css # 全局重置(盒模型/滚动条/聚焦环/拖拽区域) │ │ └── globals.css # 入口文件(按序导入以上 CSS) -│ └── utils/ # 渲染进程工具函数 -│ ├── event-bus.ts # 事件总线(on/off/emit + 日志审计钩子) -│ ├── ipc.ts # IPC 安全守卫(hasIpc → safeIpcOn/send/invoke) -│ ├── logger.ts # 渲染进程日志(IPC 转发 | DEV 控制台) -│ ├── platform.ts # 平台判定(isMacOS/isWindows/isLinux) -│ ├── device.ts # 设备 ID 生成与持久化(UUID v4) -│ ├── safe-storage.ts # 加密 localStorage 封装(safeStorage 桥接) -│ ├── logo.ts # Logo 资产 URL 解析 -│ ├── pricing.ts # 成本计算(Python 六种定价模式移植) +│ └── utils/ # 渲染进程工具函数(7 文件,已按功能合并) +│ ├── env.ts # 运行时环境:平台检测 + 设备 ID(← platform + device) +│ ├── bus.ts # 通信层:事件总线 + IPC 守卫 + 日志(← event-bus + ipc + logger) +│ ├── display.ts # 展示/格式化:金额、日期、Logo、Nullable(← display + logo + type) │ ├── enum-resolver.ts # 枚举外观模式(7 模块标签/颜色统一解析) -│ ├── display.ts # 金额/日期格式化 -│ └── type.ts # Nullable 类型别名 +│ ├── pricing.ts # 成本计算(Python 六种定价模式移植) +│ ├── safe-storage.ts # 加密 localStorage 封装(safeStorage 桥接) +│ └── media-upload.ts # 媒体上传共享工具(← controls/media-upload-utils) ├── shared/ # 主进程 & 渲染进程共享 │ ├── constants/ │ │ ├── app.ts # 应用名称/版本类型/窗口标题 diff --git a/TODO.md b/TODO.md index c49560d..b868141 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,14 @@ # TODO — 船长·HeiXiu 待办与优化清单 -> 最后更新:2026-06-18 +> 最后更新:2026-06-22 + +--- + +## ✅ 近期完成 + +- **构建时 EDITION 变量已移除**:版控全部收敛到后端 `account_type` 运行时判断,不再区分个人版/企业版构建产物 +- **窗口标题动态版控**:登录后通过 IPC 追加 `·个人版`/`·企业版` 后缀 +- **退出登录不再自动清空本地缓存**:`user_id` 隔离 + 手动清理按钮 --- diff --git a/build.mjs b/build.mjs index a131ec3..f35f8a4 100644 --- a/build.mjs +++ b/build.mjs @@ -8,10 +8,6 @@ // --mac macOS // --linux Linux // -// 版本(可选,默认 personal): -// --personal 个人版 -// --enterprise 企业版 -// // 操作(可选组合,默认只执行 --build): // --build 仅构建(默认) // --clean 构建前清理 release/ dist/ dist-electron/ @@ -25,10 +21,10 @@ // --cert-password

证书密码 // // 示例: -// node build.mjs --win --personal # 仅打包 -// node build.mjs --win --personal --upload --publish # 打包 + 上传 + 发布 -// node build.mjs --win --enterprise --all # 全流程 -// node build.mjs --win --personal --sign --cert ./cert.pfx --cert-password xxx +// node build.mjs --win # 仅打包 +// node build.mjs --win --upload --publish # 打包 + 上传 + 发布 +// node build.mjs --win --all # 全流程 +// node build.mjs --win --sign --cert ./cert.pfx --cert-password xxx // ============================================================ import { spawn } from 'node:child_process'; @@ -55,8 +51,6 @@ const opts = { win: false, mac: false, linux: false, - personal: false, - enterprise: false, clean: false, build: false, sign: false, @@ -73,8 +67,6 @@ while (i < args.length) { case '--win': opts.win = true; break; case '--mac': opts.mac = true; break; case '--linux': opts.linux = true; break; - case '--personal': opts.personal = true; break; - case '--enterprise': opts.enterprise = true; break; case '--clean': opts.clean = true; break; case '--build': opts.build = true; break; case '--sign': opts.sign = true; break; @@ -109,21 +101,12 @@ if (platformCount !== 1) { process.exit(1); } -const editionCount = [opts.personal, opts.enterprise].filter(Boolean).length; -if (editionCount === 0) { - opts.personal = true; // 默认个人版 -} else if (editionCount > 1) { - console.error('错误: --personal 和 --enterprise 不能同时使用'); - process.exit(1); -} - // 默认执行 build if (!opts.clean && !opts.build && !opts.sign && !opts.upload && !opts.publish) { opts.build = true; } const platform = opts.win ? 'win32' : opts.mac ? 'darwin' : 'linux'; -const edition = opts.personal ? 'personal' : 'enterprise'; const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); const version = pkg.version; @@ -162,10 +145,9 @@ function step(title) { async function main() { const electronBuilderPlatform = opts.win ? '--win' : opts.mac ? '--mac' : '--linux'; - const editionEnv = { EDITION: edition }; console.log('┌──────────────────────────────────────┐'); - console.log(`│ HeiXiu 构建 v${version} ${platform}/${edition}`.padEnd(45) + '│'); + console.log(`│ HeiXiu 构建 v${version} ${platform}`.padEnd(45) + '│'); console.log('└──────────────────────────────────────┘'); console.log(`操作: ${[ opts.clean && '清理', @@ -195,33 +177,33 @@ async function main() { // 2a. TypeScript 编译 console.log('[tsc] 类型检查...'); - await run('npx', ['tsc'], { env: editionEnv }); + await run('npx', ['tsc']); // 2b. Vite 构建 console.log('[vite] 打包前端 + Electron...'); - await run('npx', ['vite', 'build'], { env: editionEnv }); + await run('npx', ['vite', 'build']); // 2c. electron-builder 打包 console.log(`[electron-builder] ${electronBuilderPlatform} 打包...`); await run('npx', ['electron-builder', electronBuilderPlatform], { - env: { ...editionEnv, ...signEnv }, + env: { ...signEnv }, }); // 2d. 生成 update-info.json console.log('[info] 生成 update-info.json...'); - await runScript('generate-update-info', platform, edition, version); + await runScript('generate-update-info', platform, version); } // ── Step 3: 上传 OSS ── if (opts.upload) { step('3/ 上传 OSS'); - await runScript('upload-oss', platform, edition, version); + await runScript('upload-oss', platform, version); } // ── Step 4: 发布到 API ── if (opts.publish) { step('4/ 发布版本到 API'); - await runScript('publish-release', platform, edition, version); + await runScript('publish-release', platform, version); } console.log('\n✅ 全部完成'); @@ -256,11 +238,6 @@ function printHelp() { row('--mac', 'macOS'); row('--linux', 'Linux'); - // 版本 - hdr('版本(可选,默认 --personal)'); - row('--personal', '个人版'); - row('--enterprise', '企业版'); - // 操作 hdr('操作(可选组合,不指定则默认 --build)'); row('--build', '构建应用(tsc + vite + electron-builder + info)'); @@ -278,15 +255,15 @@ function printHelp() { // 示例 box('示例'); - console.log(` ${dim('# 日常开发')}`); - console.log(` ${bold('node build.mjs --win --personal')}`); + console.log(` ${dim('# 日常构建')}`); + console.log(` ${bold('node build.mjs --win')}`); console.log(` ${bold('npm run build:win')} ${dim('— 等效')}`); console.log(''); console.log(` ${dim('# 打包 + 上传 OSS + 发布到 API')}`); - console.log(` ${bold('node build.mjs --win --personal --upload --publish')}`); + console.log(` ${bold('node build.mjs --win --upload --publish')}`); console.log(''); console.log(` ${dim('# 全流程(清理 → 构建 → 签名 → 上传 → 发布)')}`); - console.log(` ${bold('node build.mjs --win --enterprise --all --cert ./cert.pfx --cert-password xxx')}`); + console.log(` ${bold('node build.mjs --win --all --cert ./cert.pfx --cert-password xxx')}`); console.log(''); console.log(` ${dim('# 仅清理')}`); console.log(` ${bold('npm run build:clean')}`); @@ -305,10 +282,9 @@ function printHelp() { // 快捷命令 box('npm 快捷命令'); - console.log(` ${bold('npm run build:win')} ${dim('→ node build.mjs --win --personal --build')}`); - console.log(` ${bold('npm run build:win:enterprise')} ${dim('→ node build.mjs --win --enterprise --build')}`); - console.log(` ${bold('npm run build:mac')} ${dim('→ node build.mjs --mac --personal --build')}`); - console.log(` ${bold('npm run build:linux')} ${dim('→ node build.mjs --linux --personal --build')}`); + console.log(` ${bold('npm run build:win')} ${dim('→ node build.mjs --win --build')}`); + console.log(` ${bold('npm run build:mac')} ${dim('→ node build.mjs --mac --build')}`); + console.log(` ${bold('npm run build:linux')} ${dim('→ node build.mjs --linux --build')}`); console.log(` ${bold('npm run build:clean')} ${dim('→ node scripts/clean.mjs')}`); console.log(` ${bold('npm run upload:oss')} ${dim('→ node scripts/upload-oss.mjs')}`); console.log(` ${bold('npm run publish:release')} ${dim('→ node scripts/publish-release.mjs')}`); diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 43c8516..e6101c4 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -29,4 +29,9 @@ declare interface Window { encrypt(plaintext: string): Promise; decrypt(encryptedBase64: string): Promise; }; + /** 应用运行时 API */ + appRuntime: { + /** 登录/登出后更新窗口标题中的版控后缀 */ + setWindowEdition(edition: string | null): void; + }; } diff --git a/electron/main.ts b/electron/main.ts index 9143230..22e14ae 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -5,15 +5,15 @@ import path from 'node:path'; import {getPlatform, isMacOS} from './main/utils/platform'; import {getWindowIconPath} from './main/utils/logo'; -import {buildWindowTitle, parseEdition} from '../shared/constants/app'; +import {buildWindowTitle} from '../shared/constants/app'; import {loadEnvFile} from './main/load-env'; import {initUpdater, registerUpdateIpcHandlers} from './main/updater'; import {setupAppMenu} from './main/menu'; import {initLogger, flushLogger, logger} from './main/logger'; -import {registerLogIpcHandlers} from './main/log-ipc'; -import {registerSafeStorageIpcHandlers} from './main/safe-storage-ipc'; -import {registerStorageIpcHandlers} from './main/storage-ipc'; -import {BIDIRECTIONAL} from '../shared/constants/ipc-channels'; +import {registerLogIpcHandlers} from './main/ipc/log-ipc'; +import {registerSafeStorageIpcHandlers} from './main/ipc/safe-storage-ipc'; +import {registerStorageIpcHandlers} from './main/ipc/storage-ipc'; +import {BIDIRECTIONAL, RENDERER_TO_MAIN} from '../shared/constants/ipc-channels'; createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -34,8 +34,8 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL // ---------- 平台 & 版本信息 ---------- const currentPlatform = getPlatform(); -const currentEdition = parseEdition(process.env.EDITION); -const WINDOW_TITLE = buildWindowTitle(currentPlatform, currentEdition); +/** 基础标题(无版控后缀),登录后通过 IPC 追加 */ +const WINDOW_TITLE = buildWindowTitle(currentPlatform); // ============================================================ // 全局未捕获异常(主进程兜底记录) @@ -174,6 +174,15 @@ app.whenReady().then(() => { }); }); + // 窗口标题版控后缀(渲染进程登录/登出后调用) + ipcMain.on(RENDERER_TO_MAIN.SET_WINDOW_TITLE, (_event, edition: string | null) => { + if (!mainWindow) return; + const title = edition + ? buildWindowTitle(currentPlatform, undefined, edition as 'personal' | 'enterprise') + : buildWindowTitle(currentPlatform); + mainWindow.setTitle(title); + }); + // 始终打开主窗口 // 登录/注册由渲染进程内的 Modal 弹层处理,不再新开窗口 const win = createMainWindow(); diff --git a/electron/main/log-ipc.ts b/electron/main/ipc/log-ipc.ts similarity index 78% rename from electron/main/log-ipc.ts rename to electron/main/ipc/log-ipc.ts index ce9d755..302f0a5 100644 --- a/electron/main/log-ipc.ts +++ b/electron/main/ipc/log-ipc.ts @@ -3,9 +3,9 @@ // ============================================================ import { ipcMain } from 'electron'; -import { RENDERER_TO_MAIN } from '../../shared/constants/ipc-channels'; -import { writeRendererLog } from './logger'; -import type { LogEntry } from '../../shared/types/logging'; +import { RENDERER_TO_MAIN } from '../../../shared/constants/ipc-channels'; +import { writeRendererLog } from '../logger'; +import type { LogEntry } from '../../../shared/types/logging'; /** * 注册日志 IPC 监听器 diff --git a/electron/main/safe-storage-ipc.ts b/electron/main/ipc/safe-storage-ipc.ts similarity index 96% rename from electron/main/safe-storage-ipc.ts rename to electron/main/ipc/safe-storage-ipc.ts index b160065..ec246c0 100644 --- a/electron/main/safe-storage-ipc.ts +++ b/electron/main/ipc/safe-storage-ipc.ts @@ -16,8 +16,8 @@ // ============================================================ import { ipcMain, safeStorage } from 'electron'; -import { BIDIRECTIONAL } from '../../shared/constants/ipc-channels'; -import { logger } from './logger'; +import { BIDIRECTIONAL } from '../../../shared/constants/ipc-channels'; +import { logger } from '../logger'; /** 检查 safeStorage 是否可用,不可用时记录警告 */ function checkAvailability(): boolean { diff --git a/electron/main/storage-ipc.ts b/electron/main/ipc/storage-ipc.ts similarity index 98% rename from electron/main/storage-ipc.ts rename to electron/main/ipc/storage-ipc.ts index 97c7806..3ac9371 100644 --- a/electron/main/storage-ipc.ts +++ b/electron/main/ipc/storage-ipc.ts @@ -16,8 +16,8 @@ import { ipcMain, shell, app } from 'electron'; import fs from 'node:fs'; import path from 'node:path'; -import { BIDIRECTIONAL } from '../../shared/constants/ipc-channels'; -import { logger } from './logger'; +import { BIDIRECTIONAL } from '../../../shared/constants/ipc-channels'; +import { logger } from '../logger'; // ---------- 内部工具 ---------- diff --git a/electron/main/updater.ts b/electron/main/updater.ts index 1d6fdf1..0c15123 100644 --- a/electron/main/updater.ts +++ b/electron/main/updater.ts @@ -77,7 +77,6 @@ class HeiXiuProvider extends Provider { params: { platform: process.platform, version: APP_VERSION, - edition: process.env.EDITION || 'personal', }, }, ); diff --git a/electron/preload.ts b/electron/preload.ts index 6db28fd..8dfed5e 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,5 +1,5 @@ import { ipcRenderer, contextBridge } from 'electron'; -import { BIDIRECTIONAL } from '../shared/constants/ipc-channels'; +import { BIDIRECTIONAL, RENDERER_TO_MAIN } from '../shared/constants/ipc-channels'; // --------- Expose some API to the Renderer process --------- contextBridge.exposeInMainWorld('ipcRenderer', { @@ -87,3 +87,14 @@ contextBridge.exposeInMainWorld('fileStorage', { .then((r: { success: boolean; data: boolean }) => r?.success ? r.data : false); }, }); + +// --------- 应用运行时 API --------- +contextBridge.exposeInMainWorld('appRuntime', { + /** + * 登录/登出后更新窗口标题中的版控后缀 + * @param edition - 'personal' | 'enterprise' | null(null 清除后缀) + */ + setWindowEdition(edition: string | null): void { + ipcRenderer.send(RENDERER_TO_MAIN.SET_WINDOW_TITLE, edition); + }, +}); diff --git a/package.json b/package.json index d0f22e5..bdf03fc 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,10 @@ "type": "module", "scripts": { "dev": "chcp 65001 > null && cross-env ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ vite", - "build": "node build.mjs --win --personal --build", - "build:win": "node build.mjs --win --personal --build", - "build:win:enterprise": "node build.mjs --win --enterprise --build", - "build:mac": "node build.mjs --mac --personal --build", - "build:mac:enterprise": "node build.mjs --mac --enterprise --build", - "build:linux": "node build.mjs --linux --personal --build", - "build:linux:enterprise": "node build.mjs --linux --enterprise --build", + "build": "node build.mjs --win --build", + "build:win": "node build.mjs --win --build", + "build:mac": "node build.mjs --mac --build", + "build:linux": "node build.mjs --linux --build", "build:clean": "node scripts/clean.mjs", "upload:oss": "node scripts/upload-oss.mjs", "publish:release": "node scripts/publish-release.mjs", @@ -62,21 +59,21 @@ ] } ], - "artifactName": "${productName}-Windows-${version}-${env.EDITION}-Setup.${ext}" + "artifactName": "${productName}-Windows-${version}-Setup.${ext}" }, "mac": { "icon": "src/assets/logo/heixiu.icns", "target": [ "dmg" ], - "artifactName": "${productName}-Mac-${version}-${env.EDITION}-Installer.${ext}" + "artifactName": "${productName}-Mac-${version}-Installer.${ext}" }, "linux": { "icon": "src/assets/logo/HX.png", "target": [ "AppImage" ], - "artifactName": "${productName}-Linux-${version}-${env.EDITION}.${ext}" + "artifactName": "${productName}-Linux-${version}.${ext}" }, "nsis": { "oneClick": false, diff --git a/scripts/generate-update-info.mjs b/scripts/generate-update-info.mjs index ab78e68..e1b2dfe 100644 --- a/scripts/generate-update-info.mjs +++ b/scripts/generate-update-info.mjs @@ -2,14 +2,14 @@ // 打包后自动生成 update-info.json // // 用法: -// node scripts/generate-update-info.mjs [version] [--url ] +// node scripts/generate-update-info.mjs [version] [--url ] // // 两种用法: // 方式 A(预判 URL):打包后自动拼接 URL → 适用于 OSS 路径可预测时 -// node scripts/generate-update-info.mjs win32 personal +// node scripts/generate-update-info.mjs win32 // // 方式 B(指定 URL):上传后用实际 URL 重新生成 → 适用于上传后才知道 URL 时 -// node scripts/generate-update-info.mjs win32 personal 0.1.0 --url "https://oss.../xxx.exe" +// node scripts/generate-update-info.mjs win32 0.1.0 --url "https://oss.../xxx.exe" // // 环境变量(可选): // UPDATE_DOWNLOAD_BASE — 方式 A 的 URL 基地址,默认 https://heixiu.oss-cn-hangzhou.aliyuncs.com/releases @@ -45,17 +45,16 @@ for (let i = 0; i < args.length; i++) { } const platform = positional[0]; -const edition = positional[1]; -if (!platform || !edition) { - console.error('用法: node scripts/generate-update-info.mjs [version] [--url ]'); - console.error('示例: node scripts/generate-update-info.mjs win32 personal'); - console.error(' node scripts/generate-update-info.mjs win32 personal 0.1.0 --url "https://..."'); +if (!platform) { + console.error('用法: node scripts/generate-update-info.mjs [version] [--url ]'); + console.error('示例: node scripts/generate-update-info.mjs win32'); + console.error(' node scripts/generate-update-info.mjs win32 0.1.0 --url "https://..."'); process.exit(1); } const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); -const version = positional[2] || pkg.version; +const version = positional[1] || pkg.version; const productName = pkg.build?.productName || '船长·HeiXiu'; // ---------- 查找安装包 ---------- @@ -190,7 +189,6 @@ const updateInfo = { // --- 附加:方便后端录入 --- _meta: { platform, - edition, fileName, blockmap: blockMapUrl ? { url: blockMapUrl, size: blockMapSize } : null, generatedAt: new Date().toISOString(), @@ -199,7 +197,7 @@ const updateInfo = { const outputPath = join( releaseDir, - `${productName}-${platform}-${edition}-v${version}-update-info.json`, + `${productName}-${platform}-v${version}-update-info.json`, ); writeFileSync(outputPath, JSON.stringify(updateInfo, null, 2), 'utf-8'); diff --git a/scripts/publish-release.mjs b/scripts/publish-release.mjs index 834a220..e8dada1 100644 --- a/scripts/publish-release.mjs +++ b/scripts/publish-release.mjs @@ -2,7 +2,7 @@ // 发布版本信息到后端 API // // 用法: -// node scripts/publish-release.mjs [version] [--url ] +// node scripts/publish-release.mjs [version] [--url ] // // 环境变量: // UPDATE_API_URL — API 基地址(默认 https://www.heixiu.com) @@ -35,16 +35,15 @@ for (let i = 0; i < args.length; i++) { } const platform = positional[0]; -const edition = positional[1]; -if (!platform || !edition) { - console.error('用法: node scripts/publish-release.mjs [version] [--url ]'); - console.error('示例: node scripts/publish-release.mjs win32 personal 0.1.0'); +if (!platform) { + console.error('用法: node scripts/publish-release.mjs [version] [--url ]'); + console.error('示例: node scripts/publish-release.mjs win32 0.1.0'); process.exit(1); } const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); -const version = positional[2] || pkg.version; +const version = positional[1] || pkg.version; // ---------- 读取 update-info.json ---------- @@ -63,9 +62,9 @@ if (jsonOverride) { try { const all = readdirSync(releaseDir); infoFile = all.find((f) => - f.includes(platform) && f.includes(edition) && f.endsWith('-update-info.json'), + f.includes(platform) && f.endsWith('-update-info.json'), ); - if (!infoFile) console.error(`未找到 ${platform}+${edition} 的 update-info.json`); + if (!infoFile) console.error(`未找到 ${platform} 的 update-info.json`); } catch (err) { console.error(`错误: ${releaseDir} — ${err.message}`); process.exit(1); @@ -81,7 +80,6 @@ const apiKey = process.env.UPDATE_API_KEY || ''; const payload = { platform, - edition, latestVersion: updateInfo.latestVersion || version, downloadUrl: updateInfo.downloadUrl, fileSize: updateInfo.fileSize, @@ -98,7 +96,7 @@ console.log('━━━━━━━━━━━━━━━━━━━━━━ console.log('📡 发布版本到 API'); console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); console.log(`POST ${endpoint}`); -console.log(`版本: ${version} (${platform}/${edition})`); +console.log(`版本: ${version} (${platform})`); console.log(`URL: ${payload.downloadUrl}\n`); try { diff --git a/scripts/upload-oss.mjs b/scripts/upload-oss.mjs index 73221e0..d6ad859 100644 --- a/scripts/upload-oss.mjs +++ b/scripts/upload-oss.mjs @@ -2,7 +2,7 @@ // 上传安装包 + blockmap 文件到阿里云 OSS // // 用法: -// node scripts/upload-oss.mjs [version] +// node scripts/upload-oss.mjs [version] // // 环境变量(使用 Node.js 直传时需要): // OSS_ENDPOINT — OSS endpoint(如 oss-cn-hangzhou.aliyuncs.com) @@ -15,12 +15,12 @@ // // 示例: // # 仅打印上传命令 -// node scripts/upload-oss.mjs win32 personal 0.1.0 +// node scripts/upload-oss.mjs win32 0.1.0 // // # 直接上传 // OSS_BUCKET=heixiu OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com \ // OSS_ACCESS_KEY_ID=xxx OSS_ACCESS_KEY_SECRET=xxx \ -// node scripts/upload-oss.mjs win32 personal 0.1.0 +// node scripts/upload-oss.mjs win32 0.1.0 // ============================================================ import {readFileSync, readdirSync} from 'node:fs'; @@ -34,16 +34,15 @@ const ROOT = join(__dirname, '..'); // ---------- 参数 ---------- const platform = process.argv[2]; -const edition = process.argv[3]; -if (!platform || !edition) { - console.error('用法: node scripts/upload-oss.mjs [version]'); - console.error('示例: node scripts/upload-oss.mjs win32 personal 0.1.0'); +if (!platform) { + console.error('用法: node scripts/upload-oss.mjs [version]'); + console.error('示例: node scripts/upload-oss.mjs win32 0.1.0'); process.exit(1); } const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); -const version = process.argv[4] || pkg.version; +const version = process.argv[3] || pkg.version; // ---------- 查找安装包 ---------- @@ -132,15 +131,15 @@ if (!OSS_ENDPOINT || !OSS_BUCKET || !OSS_ACCESS_KEY_ID) { console.log('上传后执行以下命令,用实际 URL 重新生成 info:'); if (blockmapUrl) { - console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`); + console.log(` node scripts/generate-update-info.mjs ${platform} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`); console.log(''); console.log('或直接发布到后端 API:'); - console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`); + console.log(` node scripts/publish-release.mjs ${platform} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`); } else { - console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`); + console.log(` node scripts/generate-update-info.mjs ${platform} ${version} --url "${downloadUrl}"`); console.log(''); console.log('或直接发布到后端 API:'); - console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`); + console.log(` node scripts/publish-release.mjs ${platform} ${version} --url "${downloadUrl}"`); } console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); process.exit(0); @@ -238,11 +237,11 @@ if (blockmapPath) { // 后续步骤提示 console.log('\n接下来 — 重新生成 update-info.json:'); if (blockmapUrl) { - console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`); + console.log(` node scripts/generate-update-info.mjs ${platform} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`); console.log('\n或直接发布到后端 API:'); - console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`); + console.log(` node scripts/publish-release.mjs ${platform} ${version} --url "${downloadUrl}" --blockmap-url "${blockmapUrl}"`); } else { - console.log(` node scripts/generate-update-info.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`); + console.log(` node scripts/generate-update-info.mjs ${platform} ${version} --url "${downloadUrl}"`); console.log('\n或直接发布到后端 API:'); - console.log(` node scripts/publish-release.mjs ${platform} ${edition} ${version} --url "${downloadUrl}"`); + console.log(` node scripts/publish-release.mjs ${platform} ${version} --url "${downloadUrl}"`); } diff --git a/shared/constants/app.ts b/shared/constants/app.ts index fe4fa2a..c0c16e6 100644 --- a/shared/constants/app.ts +++ b/shared/constants/app.ts @@ -6,25 +6,18 @@ import type { Edition, Platform } from '../types'; import { APP_VERSION } from './version'; -/** 应用显示名称 */ -export const APP_NAME = '船长·HeiXiu'; +/** 应用显示名称(文件/标题通用前缀) */ +export const APP_NAME = '船长-HeiXiu'; // APP_VERSION 从 ./version.ts 导入(读取 package.json),不再硬编码 // ============================================================ // 版本类型(企业版 / 个人版) +// +// 注意:edition 由后端 account_type 运行时决定,不由构建变量控制。 +// 不再需要 parseEdition() —— 构建时 EDITION 环境变量已移除。 // ============================================================ -/** - * 解析版本类型字符串 - * @param raw - 环境变量值,如 "enterprise" / undefined - * @returns 版本类型,默认 personal - */ -export function parseEdition(raw: string | undefined): Edition { - if (raw === 'enterprise') return 'enterprise'; - return 'personal'; -} - /** 版本中文名称 */ export function getEditionName(edition: Edition): string { return edition === 'enterprise' ? '企业版' : '个人版'; @@ -42,18 +35,24 @@ function platformShortName(platform: Platform): string { } /** - * 构建完整窗口标题 - * 格式:船长·HeiXiu V{版本号} {平台}{版本} - * 示例:"船长·HeiXiu V0.0.1 MAC企业版" - * "船长·HeiXiu V0.0.1 Windows个人版" + * 构建窗口标题 + * + * 格式(无版控):船长-HeiXiu-{平台}-{版本号} + * 示例:船长-HeiXiu-Windows-0.0.23 + * + * 格式(含版控):船长-HeiXiu-{平台}-{版本号}·{版控} + * 示例:船长-HeiXiu-Windows-0.0.23·企业版 * * @param platform - 当前操作系统平台 - * @param edition - 版本类型 * @param version - 可选覆盖版本号(默认 APP_VERSION) + * @param edition - 可选版本类型(登录后追加,未登录不显示) */ -export function buildWindowTitle(platform: Platform, edition: Edition, version?: string): string { +export function buildWindowTitle(platform: Platform, version?: string, edition?: Edition): string { const ver = version ?? APP_VERSION; const os = platformShortName(platform); - const edName = getEditionName(edition); - return `${APP_NAME} V${ver} ${os}${edName}`; + const base = `${APP_NAME}-${os}-${ver}`; + if (edition) { + return `${base}·${getEditionName(edition)}`; + } + return base; } diff --git a/src/App.tsx b/src/App.tsx index 8a6bf95..2de4bfd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,10 +3,10 @@ import { useState, useEffect, useCallback } from 'react'; import { useAppContext } from '@/components/providers/AppProvider'; import { LoginPage } from './pages/auth'; import { SettingsPage } from './pages/settings'; -import { on, off, EVENTS } from './utils/event-bus'; -import { Layout } from '@/components/shell/Layout'; +import { on, off, EVENTS } from './utils/bus'; +import { Layout } from '@/components/frame/Layout'; import { HomePage } from '@/pages/home/HomePage'; -import { PageDispatcher } from '@/components/shell/PageDispatcher'; +import { PageDispatcher } from '@/components/frame/PageDispatcher'; /** * 根组件 — 主窗口 diff --git a/src/components/shell/Layout.tsx b/src/components/frame/Layout.tsx similarity index 100% rename from src/components/shell/Layout.tsx rename to src/components/frame/Layout.tsx diff --git a/src/components/shell/PageDispatcher.tsx b/src/components/frame/PageDispatcher.tsx similarity index 99% rename from src/components/shell/PageDispatcher.tsx rename to src/components/frame/PageDispatcher.tsx index 153dea0..3b566d9 100644 --- a/src/components/shell/PageDispatcher.tsx +++ b/src/components/frame/PageDispatcher.tsx @@ -18,7 +18,7 @@ import {type ComponentType, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Drawer, Modal} from 'antd'; -import {EVENTS, off, on} from '@/utils/event-bus'; +import {EVENTS, off, on} from '@/utils/bus'; import {FloatingPanel} from '@/components/ui/FloatingPanel'; import {WechatWorkPage} from '@/pages/panels/WechatWorkPage'; diff --git a/src/components/shell/navbar/AnnouncementDrawer.tsx b/src/components/frame/navbar/AnnouncementDrawer.tsx similarity index 100% rename from src/components/shell/navbar/AnnouncementDrawer.tsx rename to src/components/frame/navbar/AnnouncementDrawer.tsx diff --git a/src/components/shell/navbar/NavBar.tsx b/src/components/frame/navbar/NavBar.tsx similarity index 99% rename from src/components/shell/navbar/NavBar.tsx rename to src/components/frame/navbar/NavBar.tsx index af7c354..62a3849 100644 --- a/src/components/shell/navbar/NavBar.tsx +++ b/src/components/frame/navbar/NavBar.tsx @@ -12,7 +12,7 @@ import {App} from 'antd'; import {useAppContext} from '@/components/providers/AppProvider'; import {useTheme} from '@/components/providers/ThemeProvider'; -import {emit, EVENTS} from '@/utils/event-bus.ts'; +import {emit, EVENTS} from '@/utils/bus'; import {NavItem} from './NavItem'; import {AnnouncementDrawer} from './AnnouncementDrawer'; import {personalLeft, personalRight, enterpriseLeft, enterpriseRight} from './nav-config'; diff --git a/src/components/shell/navbar/NavItem.tsx b/src/components/frame/navbar/NavItem.tsx similarity index 100% rename from src/components/shell/navbar/NavItem.tsx rename to src/components/frame/navbar/NavItem.tsx diff --git a/src/components/shell/navbar/index.ts b/src/components/frame/navbar/index.ts similarity index 100% rename from src/components/shell/navbar/index.ts rename to src/components/frame/navbar/index.ts diff --git a/src/components/shell/navbar/nav-config.ts b/src/components/frame/navbar/nav-config.ts similarity index 100% rename from src/components/shell/navbar/nav-config.ts rename to src/components/frame/navbar/nav-config.ts diff --git a/src/components/shell/navbar/types.ts b/src/components/frame/navbar/types.ts similarity index 100% rename from src/components/shell/navbar/types.ts rename to src/components/frame/navbar/types.ts diff --git a/src/components/providers/AppProvider.tsx b/src/components/providers/AppProvider.tsx index b1bd892..15a0ce1 100644 --- a/src/components/providers/AppProvider.tsx +++ b/src/components/providers/AppProvider.tsx @@ -10,12 +10,12 @@ import { createContext, useContext, useState, useMemo, useCallback, useEffect, type ReactNode } from 'react'; import type { Platform, Edition } from '@shared/types'; -import { getPlatform, isDev } from '@/utils/platform'; -import { safeIpcOn, safeIpcOff } from '@/utils/ipc'; +import { getPlatform, isDev } from '@/utils/env'; +import { safeIpcOn, safeIpcOff } from '@/utils/bus'; import { AuthAPI, type LoginResponseBody } from '@/services/modules/auth'; import { setToken, clearToken, initTokenStore } from '@/services/request'; -import { emit, EVENTS } from '@/utils/event-bus'; -import { logger } from '@/utils/logger'; +import { emit, EVENTS } from '@/utils/bus'; +import { logger } from '@/utils/bus'; import { getAutoLoginFlag, clearAutoLoginFlag } from '@/services/auth-persistence'; import { initAuthRefresh, @@ -27,7 +27,7 @@ import { clearProactiveRefresh, } from '@/services/auth-token'; import {initModelCache} from '@/services/cache/model-cache'; -import {initStorage, clearUserStorage} from '@/infrastructure/storage'; +import {initStorage} from '@/infrastructure/storage'; // ---------- Context 类型 ---------- @@ -87,6 +87,11 @@ export function AppProvider({ children }: AppProviderProps) { return user.account_type === 'individual' ? 'personal' : 'enterprise'; }, [user]); + // 登录/登出后同步窗口标题版控后缀到主进程 + useEffect(() => { + window.appRuntime?.setWindowEdition(isLoggedIn ? edition : null); + }, [edition, isLoggedIn]); + // ---------- 登录 / 退出 ---------- const login = useCallback(async (auth: LoginResponseBody) => { @@ -111,8 +116,9 @@ export function AppProvider({ children }: AppProviderProps) { setUser(null); setIsLoggedIn(false); emit(EVENTS.LOGOUT); - // 异步清理本地存储(不阻塞 UI) - void clearUserStorage(); + // 注意:退出登录不自动清理本地缓存 + // —— 数据已按 user_id 隔离,同一用户重新登录后缓存仍可命中 + // 用户主动清理请使用设置页面的「清理缓存」按钮 }, []); // ---------- 启动时初始化 token 存储 + 自动登录 ---------- diff --git a/src/pages/auth/components/FormField.tsx b/src/components/ui/FormField.tsx similarity index 88% rename from src/pages/auth/components/FormField.tsx rename to src/components/ui/FormField.tsx index 26c6570..2ea348d 100644 --- a/src/pages/auth/components/FormField.tsx +++ b/src/components/ui/FormField.tsx @@ -1,22 +1,22 @@ // ============================================================ -// FormField — 单个表单字段(数据驱动渲染) -// 根据 FieldConfig 自动选择 Input / Input.Password 及附加操作按钮 +// FormField — 通用表单字段渲染器(数据驱动) +// +// 根据 FieldConfig 自动选择 Input / Input.Password / 带短信按钮的复合控件。 +// 原在 src/pages/auth/components/FormField.tsx, +// 提升到 src/components/ui/ 供跨页面复用。 // ============================================================ import { useCallback } from 'react'; import { Input, Button, Form } from 'antd'; -import type { FieldConfig } from '../types'; +import type { FieldConfig } from './types'; interface FormFieldProps { config: FieldConfig; value: string; onChange: (name: string, value: string) => void; errors: string[]; - /** 发送短信验证码回调 */ onSendSms?: () => void; - /** 验证码按钮冷却中 */ smsCooldown?: boolean; - /** 冷却剩余秒数 */ smsSecondsLeft?: number; } diff --git a/src/components/ui/types.ts b/src/components/ui/types.ts new file mode 100644 index 0000000..bcec5dd --- /dev/null +++ b/src/components/ui/types.ts @@ -0,0 +1,26 @@ +// ============================================================ +// ui/types — 通用 UI 组件共享类型 +// FieldConfig / FieldRule / InputType 原在 src/pages/auth/types.ts +// 提升到 ui/ 后供 FormField 及未来消费者共用 +// ============================================================ + +export interface FieldRule { + required?: boolean; + minLen?: number; + maxLen?: number; + pattern?: RegExp; + message?: string; +} + +export type InputType = 'text' | 'password' | 'tel'; + +export interface FieldConfig { + name: string; + label: string; + placeholder: string; + inputType: InputType; + rules: FieldRule[]; + optional?: boolean; + extraAction?: 'send-sms'; + cooldown?: number; +} diff --git a/src/hooks/use-async.ts b/src/hooks/use-async.ts index ac3e494..59693f4 100644 --- a/src/hooks/use-async.ts +++ b/src/hooks/use-async.ts @@ -22,7 +22,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { RequestError, RequestErrorType } from '@/services/types'; -import { logger } from '@/utils/logger'; +import { logger } from '@/utils/bus'; // ---------- 错误信息映射 ---------- diff --git a/src/hooks/use-updater.ts b/src/hooks/use-updater.ts index a3ab811..7a60c1c 100644 --- a/src/hooks/use-updater.ts +++ b/src/hooks/use-updater.ts @@ -14,7 +14,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { safeIpcInvoke, safeIpcOn, safeIpcSend } from '@/utils/ipc'; +import { safeIpcInvoke, safeIpcOn, safeIpcSend } from '@/utils/bus'; // ---------- 类型 ---------- diff --git a/src/infrastructure/storage/db-core.ts b/src/infrastructure/storage/core/db-core.ts similarity index 99% rename from src/infrastructure/storage/db-core.ts rename to src/infrastructure/storage/core/db-core.ts index e0e674b..1875aa2 100644 --- a/src/infrastructure/storage/db-core.ts +++ b/src/infrastructure/storage/core/db-core.ts @@ -14,7 +14,7 @@ import initSqlJs, { type Database, type SqlJsStatic, type SqlValue } from 'sql.js'; import { DDL_STATEMENTS, INIT_VERSION_SQL } from './schema'; -import { logger } from '@/utils/logger'; +import { logger } from '@/utils/bus'; // ---------- 内部状态 ---------- diff --git a/src/infrastructure/storage/db-encrypt.ts b/src/infrastructure/storage/core/db-encrypt.ts similarity index 98% rename from src/infrastructure/storage/db-encrypt.ts rename to src/infrastructure/storage/core/db-encrypt.ts index a9c0eae..66e000a 100644 --- a/src/infrastructure/storage/db-encrypt.ts +++ b/src/infrastructure/storage/core/db-encrypt.ts @@ -11,9 +11,9 @@ // 加载:IPC readFile → base64 → safeStorage.decrypt() → Uint8Array → DB // ============================================================ -import { hasIpc } from '@/utils/ipc'; +import { hasIpc } from '@/utils/bus'; import { exportDatabase } from './db-core'; -import { logger } from '@/utils/logger'; +import { logger } from '@/utils/bus'; /** 数据库文件相对路径(相对于 heixiu-data/) */ const DB_RELATIVE_PATH = 'heixiu.db'; diff --git a/src/infrastructure/storage/schema.ts b/src/infrastructure/storage/core/schema.ts similarity index 100% rename from src/infrastructure/storage/schema.ts rename to src/infrastructure/storage/core/schema.ts diff --git a/src/infrastructure/storage/types.ts b/src/infrastructure/storage/core/types.ts similarity index 100% rename from src/infrastructure/storage/types.ts rename to src/infrastructure/storage/core/types.ts diff --git a/src/infrastructure/storage/index.ts b/src/infrastructure/storage/index.ts index e310c81..81c7cf0 100644 --- a/src/infrastructure/storage/index.ts +++ b/src/infrastructure/storage/index.ts @@ -15,35 +15,35 @@ // import { initStorage, getTasks, syncTasks, saveParamSnapshot } from '@/infrastructure/storage'; // ============================================================ -import { openDatabase, closeDatabase, isOpen } from './db-core'; -import { loadDatabase, saveDatabase } from './db-encrypt'; -import { clearUserIdCache } from './user-scope'; -import { clearUserTasks, getTaskCount as _getTaskCount } from './task-store'; -import { clearUserOrders, getOrderCount as _getOrderCount } from './order-store'; -import { clearUserParams } from './param-store'; -import { clearUserMedia, getMediaCount as _getMediaCount, getMediaTotalSize as _getMediaTotalSize } from './media-store'; -import { logger } from '@/utils/logger'; +import { openDatabase, isOpen } from './core/db-core'; +import { loadDatabase, saveDatabase } from './core/db-encrypt'; +import { clearUserIdCache } from './services/user-scope'; +import { clearUserTasks, getTaskCount as _getTaskCount } from './stores/task-store'; +import { clearUserOrders, getOrderCount as _getOrderCount } from './stores/order-store'; +import { clearUserParams } from './stores/param-store'; +import { clearUserMedia, getMediaCount as _getMediaCount, getMediaTotalSize as _getMediaTotalSize } from './stores/media-store'; +import { logger } from '@/utils/bus'; // ---------- 重新导出 ---------- -export { openDatabase, closeDatabase, isOpen, exportDatabase, execute, queryAll, queryOne } from './db-core'; -export { loadDatabase, saveDatabase } from './db-encrypt'; -export { getCurrentUserId, clearUserIdCache, getTodayKey, hashURL, formatBytes } from './user-scope'; -export { upsertTask, syncTasks, getTasks, getTaskById, deleteTask, getTaskCount, clearUserTasks, rowToTask } from './task-store'; -export { upsertOrder, syncRechargeOrders, syncVipOrders, getOrders, getOrderById, deleteOrder, getOrderCount, clearUserOrders } from './order-store'; -export { saveParamSnapshot, fillOutputData, getParamByTaskId, getParamHistory, parseParams, deleteParamHistory, clearUserParams } from './param-store'; -export { upsertMediaCache, getMediaByUrl, hasMedia, getMediaCount, getMediaTotalSize, deleteMediaByUrl, clearExpiredMedia, clearUserMedia } from './media-store'; -export { setSetting, getSetting, getAllSettings, deleteSetting, getOutputPath, setOutputPath, getTeamRepoPath, setTeamRepoPath } from './settings-store'; -export { getCursor, updateCursor, hasMore, nextPage, syncedCount, totalCount, getPageSize } from './sync-manager'; +export { openDatabase, closeDatabase, isOpen, exportDatabase, execute, queryAll, queryOne } from './core/db-core'; +export { loadDatabase, saveDatabase } from './core/db-encrypt'; +export { getCurrentUserId, clearUserIdCache, getTodayKey, hashURL, formatBytes } from './services/user-scope'; +export { upsertTask, syncTasks, getTasks, getTaskById, deleteTask, getTaskCount, clearUserTasks, rowToTask } from './stores/task-store'; +export { upsertOrder, syncRechargeOrders, syncVipOrders, getOrders, getOrderById, deleteOrder, getOrderCount, clearUserOrders } from './stores/order-store'; +export { saveParamSnapshot, fillOutputData, getParamByTaskId, getParamHistory, parseParams, deleteParamHistory, clearUserParams } from './stores/param-store'; +export { upsertMediaCache, getMediaByUrl, hasMedia, getMediaCount, getMediaTotalSize, deleteMediaByUrl, clearExpiredMedia, clearUserMedia } from './stores/media-store'; +export { setSetting, getSetting, getAllSettings, deleteSetting, getOutputPath, setOutputPath, getTeamRepoPath, setTeamRepoPath } from './stores/settings-store'; +export { getCursor, updateCursor, hasMore, nextPage, syncedCount, totalCount, getPageSize } from './services/sync-manager'; export { writeFile, readFile, deleteFile, makeDir, showItemInFolder, fileExists, saveBlobToFile, readFileAsBlobUrl, getDataDir } from './ipc-file'; -export { loadMedia, preloadMedia, isVideoMime, isImageMime, isAudioMime, revokeMediaUrl, isMediaSuccess, isMediaFailure } from './media-loader'; -export type { MediaLoadResult, MediaLoadSuccess, MediaLoadFailure } from './media-loader'; +export { loadMedia, preloadMedia, isVideoMime, isImageMime, isAudioMime, revokeMediaUrl, isMediaSuccess, isMediaFailure } from './services/media-loader'; +export type { MediaLoadResult, MediaLoadSuccess, MediaLoadFailure } from './services/media-loader'; export type { TaskRecordEntry, TaskQueryFilter, OrderRecordEntry, OrderType, OrderQueryFilter, ParamHistoryEntry, MediaCacheEntry, SyncCursor, SyncDataType, AppSetting, -} from './types'; +} from './core/types'; // ---------- 初始化 ---------- @@ -82,7 +82,7 @@ export async function initStorage(): Promise { // 3. 清理过期媒体缓存 try { - const { clearExpiredMedia } = await import('./media-store'); + const { clearExpiredMedia } = await import('./stores/media-store'); clearExpiredMedia(); } catch { /* ignore */ } @@ -93,8 +93,14 @@ export async function initStorage(): Promise { // ---------- 清理 ---------- -/** 清除当前用户的所有本地数据 + 关闭数据库 */ +/** 清除当前用户的所有本地数据并持久化空库到磁盘 */ export async function clearUserStorage(): Promise { + // 守卫:数据库未初始化或已被清理,无需重复操作 + if (!isOpen()) { + logger.debug('storage', '数据库未打开,跳过 clearUserStorage'); + return; + } + // 1. 清空所有表数据 try { await Promise.allSettled([ @@ -105,11 +111,8 @@ export async function clearUserStorage(): Promise { ]); } catch { /* ignore */ } - // 2. 保存空库到磁盘(覆盖旧数据) + // 2. 保存空库到磁盘(覆盖旧数据,但不关闭——以便重新登录后直接使用) await saveDatabase(); - - // 3. 关闭数据库 - closeDatabase(); clearUserIdCache(); logger.info('storage', '用户存储数据已清理'); } diff --git a/src/infrastructure/storage/ipc-file.ts b/src/infrastructure/storage/ipc-file.ts index 9875929..a2644ca 100644 --- a/src/infrastructure/storage/ipc-file.ts +++ b/src/infrastructure/storage/ipc-file.ts @@ -11,8 +11,8 @@ // media-store / db-encrypt 等均通过本模块操作文件。 // ============================================================ -import { hasIpc } from '@/utils/ipc'; -import { logger } from '@/utils/logger'; +import { hasIpc } from '@/utils/bus'; +import { logger } from '@/utils/bus'; // ---------- 文件 API ---------- diff --git a/src/pages/settings/sections/data-sync.ts b/src/infrastructure/storage/services/data-sync.ts similarity index 98% rename from src/pages/settings/sections/data-sync.ts rename to src/infrastructure/storage/services/data-sync.ts index 124a85c..9d8a15a 100644 --- a/src/pages/settings/sections/data-sync.ts +++ b/src/infrastructure/storage/services/data-sync.ts @@ -16,7 +16,7 @@ import { import { TaskAPI } from '@/services/modules/task'; import { PaymentAPI } from '@/services/modules/payments'; import { VipAPI } from '@/services/modules/vip-api'; -import { logger } from '@/utils/logger'; +import { logger } from '@/utils/bus'; const PAGE_SIZE = 100; diff --git a/src/infrastructure/storage/media-loader.ts b/src/infrastructure/storage/services/media-loader.ts similarity index 98% rename from src/infrastructure/storage/media-loader.ts rename to src/infrastructure/storage/services/media-loader.ts index ab723e6..aef071e 100644 --- a/src/infrastructure/storage/media-loader.ts +++ b/src/infrastructure/storage/services/media-loader.ts @@ -16,11 +16,11 @@ import axios from 'axios'; import { axiosInstance } from '@/services/request'; -import { logger } from '@/utils/logger'; -import { hasIpc } from '@/utils/ipc'; +import { logger } from '@/utils/bus'; +import { hasIpc } from '@/utils/bus'; import { getCurrentUserId, hashURL, getTodayKey } from './user-scope'; -import { getMediaByUrl, upsertMediaCache, deleteMediaByUrl } from './media-store'; -import { saveBlobToFile, readFile, fileExists } from './ipc-file'; +import { getMediaByUrl, upsertMediaCache, deleteMediaByUrl } from '../stores/media-store'; +import { saveBlobToFile, readFile, fileExists } from '../ipc-file'; // ---------- 类型 ---------- diff --git a/src/infrastructure/storage/sync-manager.ts b/src/infrastructure/storage/services/sync-manager.ts similarity index 94% rename from src/infrastructure/storage/sync-manager.ts rename to src/infrastructure/storage/services/sync-manager.ts index 58954e7..bfac754 100644 --- a/src/infrastructure/storage/sync-manager.ts +++ b/src/infrastructure/storage/services/sync-manager.ts @@ -12,8 +12,8 @@ // - 不自动拉取,完全由用户手动触发 // ============================================================ -import { execute, queryOne } from './db-core'; -import type { SyncCursor, SyncDataType } from './types'; +import { execute, queryOne } from '../core/db-core'; +import type { SyncCursor, SyncDataType } from '../core/types'; const PAGE_SIZE = 100; diff --git a/src/infrastructure/storage/user-scope.ts b/src/infrastructure/storage/services/user-scope.ts similarity index 100% rename from src/infrastructure/storage/user-scope.ts rename to src/infrastructure/storage/services/user-scope.ts diff --git a/src/infrastructure/storage/media-store.ts b/src/infrastructure/storage/stores/media-store.ts similarity index 92% rename from src/infrastructure/storage/media-store.ts rename to src/infrastructure/storage/stores/media-store.ts index 3d22822..ecc5b97 100644 --- a/src/infrastructure/storage/media-store.ts +++ b/src/infrastructure/storage/stores/media-store.ts @@ -7,10 +7,10 @@ // - 读取时更新访问时间(LRU 排序依据) // ============================================================ -import { execute, queryAll, queryOne } from './db-core'; -import { getCurrentUserId, getTodayKey, hashURL } from './user-scope'; -import type { MediaCacheEntry } from './types'; -import { logger } from '@/utils/logger'; +import { execute, queryAll, queryOne } from '../core/db-core'; +import { getCurrentUserId, getTodayKey, hashURL } from '../services/user-scope'; +import type { MediaCacheEntry } from '../core/types'; +import { logger } from '@/utils/bus'; // ---------- API ---------- diff --git a/src/infrastructure/storage/order-store.ts b/src/infrastructure/storage/stores/order-store.ts similarity index 94% rename from src/infrastructure/storage/order-store.ts rename to src/infrastructure/storage/stores/order-store.ts index d3ee754..76380b8 100644 --- a/src/infrastructure/storage/order-store.ts +++ b/src/infrastructure/storage/stores/order-store.ts @@ -2,10 +2,10 @@ // infrastructure/storage/order-store.ts — 订单记录本地存储 // ============================================================ -import { execute, queryAll, queryOne } from './db-core'; -import { getCurrentUserId } from './user-scope'; -import type { OrderRecordEntry, OrderType, OrderQueryFilter } from './types'; -import { logger } from '@/utils/logger'; +import { execute, queryAll, queryOne } from '../core/db-core'; +import { getCurrentUserId } from '../services/user-scope'; +import type { OrderRecordEntry, OrderType, OrderQueryFilter } from '../core/types'; +import { logger } from '@/utils/bus'; // ---------- 通用接口 ---------- diff --git a/src/infrastructure/storage/param-store.ts b/src/infrastructure/storage/stores/param-store.ts similarity index 93% rename from src/infrastructure/storage/param-store.ts rename to src/infrastructure/storage/stores/param-store.ts index f7d6eb3..90f3e56 100644 --- a/src/infrastructure/storage/param-store.ts +++ b/src/infrastructure/storage/stores/param-store.ts @@ -7,9 +7,9 @@ // - 按模型查询历史参数列表(回填菜单数据源) // ============================================================ -import { execute, queryAll, queryOne } from './db-core'; -import { getCurrentUserId } from './user-scope'; -import type { ParamHistoryEntry } from './types'; +import { execute, queryAll, queryOne } from '../core/db-core'; +import { getCurrentUserId } from '../services/user-scope'; +import type { ParamHistoryEntry } from '../core/types'; const MAX_PER_MODEL = 20; diff --git a/src/infrastructure/storage/settings-store.ts b/src/infrastructure/storage/stores/settings-store.ts similarity index 90% rename from src/infrastructure/storage/settings-store.ts rename to src/infrastructure/storage/stores/settings-store.ts index 37bea8c..e94b551 100644 --- a/src/infrastructure/storage/settings-store.ts +++ b/src/infrastructure/storage/stores/settings-store.ts @@ -7,9 +7,9 @@ // - 兼容现有的 localStorage 设置逻辑 // ============================================================ -import { execute, queryAll, queryOne } from './db-core'; -import type { AppSetting } from './types'; -import { SETTING_KEYS } from './types'; +import { execute, queryAll, queryOne } from '../core/db-core'; +import type { AppSetting } from '../core/types'; +import { SETTING_KEYS } from '../core/types'; // ---------- API ---------- diff --git a/src/infrastructure/storage/task-store.ts b/src/infrastructure/storage/stores/task-store.ts similarity index 93% rename from src/infrastructure/storage/task-store.ts rename to src/infrastructure/storage/stores/task-store.ts index cdba466..163c581 100644 --- a/src/infrastructure/storage/task-store.ts +++ b/src/infrastructure/storage/stores/task-store.ts @@ -7,11 +7,11 @@ // - 按用户 + 状态/模型筛选 // ============================================================ -import { execute, queryAll, queryOne } from './db-core'; -import { getCurrentUserId } from './user-scope'; -import type { TaskRecordEntry, TaskQueryFilter } from './types'; +import { execute, queryAll, queryOne } from '../core/db-core'; +import { getCurrentUserId } from '../services/user-scope'; +import type { TaskRecordEntry, TaskQueryFilter } from '../core/types'; import type { TaskInfoItemResponseBody } from '@/services/modules/task'; -import { logger } from '@/utils/logger'; +import { logger } from '@/utils/bus'; // ---------- 序列化 ---------- diff --git a/src/main.tsx b/src/main.tsx index bdfc396..23e6aaa 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -28,13 +28,13 @@ ReactDOM.createRoot(document.getElementById('root')!).render( ); // 仅在 Electron 环境中监听主进程消息(浏览器预览时跳过) -import { safeIpcOn } from './utils/ipc'; +import { safeIpcOn } from './utils/bus'; safeIpcOn('main-process-message', (_event: unknown, message: unknown) => { console.log('[Main Process Message]:', message); }); // 渲染进程全局未捕获错误(兜底记录到日志文件) -import {logger} from './utils/logger'; +import {logger} from './utils/bus'; window.addEventListener('error', (event) => { logger.error('ui', 'Uncaught error (renderer)', event.error, { diff --git a/src/pages/auth/index.tsx b/src/pages/auth/index.tsx index acb6f18..84f38aa 100644 --- a/src/pages/auth/index.tsx +++ b/src/pages/auth/index.tsx @@ -17,7 +17,7 @@ import { RegisterForm } from './components/RegisterForm'; import { ForgotPasswordModal } from './components/ForgotPasswordModal'; import { useAuthState } from './hooks/use-auth-state'; import { AuthAPI, type LoginResponseBody } from '@/services/modules/auth'; -import { getDeviceId } from '@/utils/device'; +import { getDeviceId } from '@/utils/env'; import type { LoginFormValues, RegisterFormValues } from './types'; const { Text } = Typography; diff --git a/src/pages/auth/types.ts b/src/pages/auth/types.ts index f441e0a..735abff 100644 --- a/src/pages/auth/types.ts +++ b/src/pages/auth/types.ts @@ -25,28 +25,5 @@ export interface RegisterFormValues { referral_code: string; } -// ---------- 字段配置(数据驱动 UI 渲染) ---------- - -export interface FieldRule { - required?: boolean; - minLen?: number; - maxLen?: number; - pattern?: RegExp; - message?: string; -} - -export type InputType = 'text' | 'password' | 'tel'; - -export interface FieldConfig { - name: string; - label: string; - placeholder: string; - inputType: InputType; - rules: FieldRule[]; - /** 仅注册表单的选填字段 */ - optional?: boolean; - /** 附带操作按钮标识 */ - extraAction?: 'send-sms'; - /** 冷却秒数(发送验证码按钮用) */ - cooldown?: number; -} +// 从 ui/ 重新导出通用字段类型(保持向后兼容) +export type { FieldRule, InputType, FieldConfig } from '@/components/ui/types'; diff --git a/src/pages/home/HomeContent.tsx b/src/pages/home/HomeContent.tsx index d1f21e4..f4e7a97 100644 --- a/src/pages/home/HomeContent.tsx +++ b/src/pages/home/HomeContent.tsx @@ -108,6 +108,8 @@ export function HomeContent() { const leftWidthRef = useRef(leftWidth); const rightWidthRef = useRef(rightWidth); const draggingRef = useRef(dragging); + // 上一次重算时的容器宽度(用于三栏等比缩放时反推中列宽度) + const prevWidthRef = useRef(0); // 标记拖拽期间被跳过的重算,拖拽结束后补算 const needsRecalcRef = useRef(false); @@ -164,94 +166,97 @@ export function HomeContent() { // ======== 视口缩放时按比例自动调整面板宽度 ======== - useEffect(() => { - const el = containerRef.current; - if (!el) return; + /** + * 三栏等比缩放核心(ResizeObserver + window.resize 共用)。 + * + * 旧算法只保持 left:right 比例,中列被动吸收剩余空间——窗口从半屏拉到全屏时 + * 新增宽度全部给了左右栏,中列反而被挤到 CENTER_MIN。 + * + * 新算法通过 prevWidthRef 反推中列上次实际宽度,三栏按各自占比分配空间变化。 + */ + const recalcLayout = useCallback(() => { + if (draggingRef.current) { + needsRecalcRef.current = true; + return; + } - const observer = new ResizeObserver(() => { - // 拖拽中由 mousemove handler 处理约束,避免冲突; - // 若跳过则标记 needsRecalcRef,拖拽结束后补算 + requestAnimationFrame(() => { if (draggingRef.current) { needsRecalcRef.current = true; return; } - // 每次回调从 ref 读取当前 DOM 元素(避免闭包捕获 mount 时的 el) const el = containerRef.current; if (!el) return; const containerWidth = el.clientWidth; + const available = containerWidth - LAYOUT_OVERHEAD; // 三栏可用总宽 const currentLeft = leftWidthRef.current; const currentRight = rightWidthRef.current; - // 左右面板可用的总空间(中列至少保留 CENTER_MIN) - const sideAvailable = containerWidth - LAYOUT_OVERHEAD - CENTER_MIN; - const sideTotal = currentLeft + currentRight; + // ---- 反推中列上次实际宽度(基于上一次记录的容器宽度) ---- + const prevWidth = prevWidthRef.current || containerWidth; // 首次调用用当前值 + const currentCenter = Math.max( + CENTER_MIN, + prevWidth - LAYOUT_OVERHEAD - currentLeft - currentRight, + ); + const total = currentLeft + currentCenter + currentRight; + if (total <= 0) return; - if (sideAvailable <= 0 || sideTotal <= 0) return; + // ---- 三栏按比例分配可用空间 ---- + let newLeft = Math.round(available * (currentLeft / total)); + let newCenter = Math.round(available * (currentCenter / total)); + let newRight = available - newLeft - newCenter; - // ---- 按当前比例分配空间 ---- - const ratio = currentLeft / sideTotal; - let newLeft = Math.round(sideAvailable * ratio); - let newRight = sideAvailable - newLeft; - - // ---- 最小值约束(保持比例的前提下不跌破下限) ---- + // ---- 最小值约束(左/右优先保底,中列弹性收缩) ---- if (newLeft < LEFT_MIN) { newLeft = LEFT_MIN; - newRight = sideAvailable - newLeft; + // 差额从右侧扣(先扣中列,再扣右列) + const deficit = available - newLeft - newCenter - newRight; + newCenter = Math.max(CENTER_MIN, newCenter + deficit); } if (newRight < RIGHT_MIN) { newRight = RIGHT_MIN; - newLeft = sideAvailable - newRight; + const deficit = available - newLeft - newCenter - newRight; + newCenter = Math.max(CENTER_MIN, newCenter + deficit); } - // 两侧同时触底 → 保持最小值,中列由 CSS min-width 兜底 - if (newLeft < LEFT_MIN) newLeft = LEFT_MIN; - if (newRight < RIGHT_MIN) newRight = RIGHT_MIN; - // 即时更新 ref,避免 ResizeObserver 连续触发时读到过期值 + // 更新 ref(在 setState 之前,确保并发回调读到最新值) leftWidthRef.current = newLeft; rightWidthRef.current = newRight; + prevWidthRef.current = containerWidth; if (newLeft !== currentLeft) setLeftWidth(newLeft); if (newRight !== currentRight) setRightWidth(newRight); }); + }, []); + // 主触发器:ResizeObserver(响应容器尺寸变化) + useEffect(() => { + const el = containerRef.current; + if (!el) return; + + const observer = new ResizeObserver(() => recalcLayout()); observer.observe(el); return () => observer.disconnect(); - }, []); + }, [recalcLayout]); + + // 兜底触发器:window.resize(捕获 ResizeObserver 可能遗漏的快照/最大化/分屏事件) + useEffect(() => { + const handleResize = () => recalcLayout(); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [recalcLayout]); // 拖拽结束后,若 ResizeObserver 曾因拖拽跳过重算,立即补算 useEffect(() => { - if (dragging !== null) return; // 仍在拖拽中 + if (dragging !== null) return; if (!needsRecalcRef.current) return; needsRecalcRef.current = false; - - const el = containerRef.current; - if (!el) return; - const containerWidth = el.clientWidth; - const currentLeft = leftWidthRef.current; - const currentRight = rightWidthRef.current; - - const sideAvailable = containerWidth - LAYOUT_OVERHEAD - CENTER_MIN; - const sideTotal = currentLeft + currentRight; - if (sideAvailable <= 0 || sideTotal <= 0) return; - - const ratio = currentLeft / sideTotal; - let newLeft = Math.round(sideAvailable * ratio); - let newRight = sideAvailable - newLeft; - - if (newLeft < LEFT_MIN) { newLeft = LEFT_MIN; newRight = sideAvailable - newLeft; } - if (newRight < RIGHT_MIN) { newRight = RIGHT_MIN; newLeft = sideAvailable - newRight; } - if (newLeft < LEFT_MIN) newLeft = LEFT_MIN; - if (newRight < RIGHT_MIN) newRight = RIGHT_MIN; - - leftWidthRef.current = newLeft; - rightWidthRef.current = newRight; - - if (newLeft !== currentLeft) setLeftWidth(newLeft); - if (newRight !== currentRight) setRightWidth(newRight); - }, [dragging]); + // 使用 requestAnimationFrame 确保 React 已完成 commit 再读取 DOM + requestAnimationFrame(() => recalcLayout()); + }, [dragging, recalcLayout]); // ======== 分隔条样式工厂 ======== diff --git a/src/pages/home/controls/AudioListInput.tsx b/src/pages/home/controls/AudioListInput.tsx deleted file mode 100644 index 5d5fb04..0000000 --- a/src/pages/home/controls/AudioListInput.tsx +++ /dev/null @@ -1,47 +0,0 @@ -// ============================================================ -// AudioListInput — audioList → Dragger(音频文件拖拽上传) -// 用于声纹克隆的音频样本上传场景 -// ============================================================ - -import { Upload } from 'antd'; -import { InboxOutlined } from '@ant-design/icons'; -import type { UploadFile } from 'antd/es/upload'; -import type { WidgetProps } from './types'; -import { useFileUpload } from '@/hooks/use-api'; - -const { Dragger } = Upload; - -export function AudioListInput({ value, onChange, disabled, config }: WidgetProps) { - const fileList = (value as UploadFile[]) || []; - const maxSizeMb = (config.maxFileSizeMb as number) || 10; - const maxCount = (config.maxFiles as number) || 10; - const filters = config.fileFilter as string[] | undefined; - - const { customRequest } = useFileUpload(); - - return ( - `.${ext}`).join(',') - : '.wav,.mp3,.m4a,.flac' - } - fileList={fileList} - customRequest={customRequest} - onChange={({ fileList: newList }) => onChange?.(newList)} - disabled={disabled} - beforeUpload={() => true} - maxCount={maxCount} - > -

- -

-

点击或拖拽音频文件到此区域上传

-

- 支持 {filters?.join(' / ').toUpperCase() || 'WAV / MP3 / M4A / FLAC'} - ,单文件不超过 {maxSizeMb}MB -

- - ); -} diff --git a/src/pages/home/controls/Checkbox.tsx b/src/pages/home/controls/Checkbox.tsx deleted file mode 100644 index 9112b0d..0000000 --- a/src/pages/home/controls/Checkbox.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// ============================================================ -// Checkbox — checkbox → Switch -// 用于布尔开关:原始模式、平铺、Base64 输出等 -// ============================================================ - -import { Switch } from 'antd'; -import type { WidgetProps } from './types'; - -export function Checkbox({ value, onChange, disabled }: WidgetProps) { - return ( - onChange?.(v)} - disabled={disabled} - /> - ); -} diff --git a/src/pages/home/controls/ComboBox.tsx b/src/pages/home/controls/ComboBox.tsx deleted file mode 100644 index 35cd23b..0000000 --- a/src/pages/home/controls/ComboBox.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// ============================================================ -// ComboBox — comboBox → Select -// 用于枚举选择:宽高比、音色、分辨率、模型等 -// ============================================================ - -import { Select } from 'antd'; -import type { WidgetProps } from './types'; - -export function ComboBox({ value, onChange, disabled, config }: WidgetProps) { - const enumValues = config.enumValues as string[] | undefined; - const options = enumValues?.map((v) => ({ label: v, value: v })) || []; - - return ( - onChange?.(e.target.value)} - disabled={disabled} - placeholder={(config.placeholder as string) || '请输入'} - maxLength={config.maxLength as number | undefined} - allowClear - /> - ); -} diff --git a/src/pages/home/controls/SpinBox.tsx b/src/pages/home/controls/SpinBox.tsx deleted file mode 100644 index 5cbd8df..0000000 --- a/src/pages/home/controls/SpinBox.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// ============================================================ -// SpinBox — spinBox → InputNumber (step=1) -// 用于整数输入:混沌、风格化、怪异、时长、音调等 -// ============================================================ - -import { InputNumber } from 'antd'; -import type { WidgetProps } from './types'; - -export function SpinBox({ value, onChange, disabled, config }: WidgetProps) { - return ( - onChange?.(v != null ? v : undefined)} - disabled={disabled} - min={config.minValue as number | undefined} - max={config.maxValue as number | undefined} - step={1} - style={{ width: '100%', minWidth: 140 }} - placeholder={(config.placeholder as string) || undefined} - controls - /> - ); -} diff --git a/src/pages/home/controls/TextInput.tsx b/src/pages/home/controls/TextInput.tsx deleted file mode 100644 index 8e70683..0000000 --- a/src/pages/home/controls/TextInput.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// ============================================================ -// TextInput — textEdit → Input.TextArea -// 用于提示词、文本合成等长文本输入场景 -// ============================================================ - -import { Input } from 'antd'; -import type { WidgetProps } from './types'; - -const { TextArea } = Input; - -export function TextInput({ value, onChange, disabled, config }: WidgetProps) { - return ( -