diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 58733c3..02a0864 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # 船长 · HeiXiu — 前端架构文档 -> 最后更新:2026-06-08 +> 最后更新:2026-06-09 ## 目录结构 @@ -165,18 +165,20 @@ NavBar / 设置页 → AppProvider.logout() - 纯发布-订阅,不依赖任何框架 - 用于跨模块解耦通信(如 axios 拦截器无法直接用 React Context) -- 预定义事件:`AUTH_REQUIRED` / `SHOW_LOGIN` / `LOGIN_SUCCESS` / `LOGOUT` / `TASK_SUBMITTED` / `OPEN_SETTINGS` +- 预定义事件:`AUTH_REQUIRED` / `SHOW_LOGIN` / `LOGIN_SUCCESS` / `LOGOUT` / `TASK_SUBMITTED` / `OPEN_SETTINGS` / `OPEN_PAGE` ### 页面叠加层模式(QT 多窗口模型映射) 本项目是 Python QT 桌面应用的 Web 重构版。QT 应用以"新开窗口"为常态,原窗口不因新窗口打开而销毁。Web 端对应原则: -| QT 原版 | Web 对应 | 实现方式 | -|---------|---------|---------| -| 新开窗口叠加在主窗口之上 | Modal / Drawer | 事件总线驱动,主页面不卸载 | -| Tab 切换 / 视图替换 | React Router | 跨页状态放 Provider 或 localStorage | +| QT 原版 | Web 对应 | 实现方式 | +|---------------|----------------|-------------------------------| +| 新开窗口叠加在主窗口之上 | Modal / Drawer | 事件总线驱动,主页面不卸载 | +| Tab 切换 / 视图替换 | React Router | 跨页状态放 Provider 或 localStorage | **当前叠加层**: + +- `PageDispatcher` — 统一页面调度器,监听 `OPEN_PAGE` 事件,根据 `modalType` 渲染 Modal/Drawer/FloatingPanel,全局互斥(同一时刻最多一个面板) - LoginPage — Modal,事件总线 `SHOW_LOGIN` / `AUTH_REQUIRED` 触发 - SettingsPage — Modal,事件总线 `OPEN_SETTINGS` 触发 - AnnouncementDrawer — Drawer,NavBar 内部 state 控制 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1417d71..d226185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ > 每次发版前修改此文件,`npm run build:win` 打包后会自动读取生成 update-info.json。 --- +## 0.0.16(2026-06-09) + +**模型输入栏重构 + 统一页面调度 + 依赖联动** + +- 模型输入栏三层架构:useUI 组件体系(10 种 widget → antd 映射)+ useModelUI Hook(param_schema → UIFieldConfig[]) +- constraints_config.requires 动态联动:enable_switch 开关控制依赖字段启用/禁用 +- 修复模型默认值不加载(antd 外部 form 实例忽略 initialValues)+ DependentFormItem prop 透传断裂 +- PageDispatcher 统一页面调度:Modal/Drawer/FloatingPanel 三容器,全局互斥,动态视口尺寸 +- 账户信息页:AuthAPI.getUserInfo() 驱动,积分/套餐/统计三卡片布局 +- 代码去重:runFieldChecks 校验运行器 + createImageBeforeUpload 上传校验工厂 +- 主题切换性能优化 — View Transitions API + 0.15s 统一过渡 + 设置页增强(0.0.15 延续) +- 目录调整:wechat-work/ → headers/ + +--- + ## 0.0.15(2026-06-09) **主题切换性能优化 — 桌面端流畅度提升** diff --git a/TODO.md b/TODO.md index b9b9537..10aad2d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,6 @@ # TODO — 船长·HeiXiu 待办与优化清单 -> 最后更新:2026-06-08 +> 最后更新:2026-06-09 --- @@ -14,10 +14,10 @@ ### 路由与页面 +- [x] 账户信息页面(`/account`) — `AccountInfo.tsx`,AuthAPI.getUserInfo() 驱动 - [ ] 合规素材库页面(`/compliance-assets`) - [ ] 项目管理页面(`/projects`,企业版) -- [ ] 账户/会员/充值/消费记录/订单明细页面 -- [ ] 各页面暗色主题适配 +- [ ] 会员/充值/消费记录/订单明细页面 ### 导航栏 diff --git a/src/pages/home/HomeContent.tsx b/src/pages/home/HomeContent.tsx index 1270a12..19e11dc 100644 --- a/src/pages/home/HomeContent.tsx +++ b/src/pages/home/HomeContent.tsx @@ -3,7 +3,7 @@ // 可拖拽调整三栏宽度,所有数据请求在登录后发送 // ============================================================ -import { createContext, useContext, useState, useCallback, useRef, useEffect, useMemo } from 'react'; +import React, { createContext, useContext, useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { theme as antTheme, Empty } from 'antd'; import { useAppContext } from '@/components/AppProvider'; diff --git a/src/pages/home/components/ModelInputForm.tsx b/src/pages/home/components/ModelInputForm.tsx index 4fd46e2..cb4b950 100644 --- a/src/pages/home/components/ModelInputForm.tsx +++ b/src/pages/home/components/ModelInputForm.tsx @@ -75,12 +75,17 @@ function DependentFormItem({ onChange?: (value: unknown) => void; disabled?: boolean; }) { - const depValues = (field.dependsOn || []).map((depName) => - Form.useWatch(depName, form), - ); + // Form.useWatch 必须在组件顶层无条件调用(不能放 .map() 或条件分支中)。 + // dependsOn 长度由 param_schema 约束(当前 API 最多 2 个), + // 预留 3 个固定槽位覆盖未来扩展;无依赖时 watch 空串 → 返回 undefined。 + const deps = field.dependsOn || []; + const d0 = Form.useWatch(deps[0] || '', form); + const d1 = Form.useWatch(deps[1] || '', form); + const d2 = Form.useWatch(deps[2] || '', form); + const depValues = [d0, d1, d2].slice(0, deps.length); const isDepDisabled = - field.dependsOn?.some((_, i) => isValueEmpty(depValues[i])) ?? false; + deps.some((_, i) => isValueEmpty(depValues[i])); const effectiveDisabled = formItemDisabled || isDepDisabled; const effectiveConfig = effectiveDisabled @@ -115,7 +120,7 @@ export function ModelInputForm() { // 提交突变 Hook const { execute: submitTask, loading: submitting, errorMessage } = useSubmitTask({ onSuccess: useCallback((_data: TaskInfoItemResponseBody) => { - message.success('任务已提交'); + void message.success('任务已提交'); emit(EVENTS.TASK_SUBMITTED); }, []), }); @@ -147,7 +152,7 @@ export function ModelInputForm() { form.setFieldsValue(initialValues); setTaskTag(''); } - }, [selectedModel?.id, initialValues]); + }, [selectedModel?.id, initialValues, selectedModel, form]); // 重置参数(回到默认值 + 清空标签) const handleReset = useCallback(() => { @@ -384,7 +389,7 @@ export function ModelInputForm() { } - onClick={handleSubmit} + onClick={() => void handleSubmit()} loading={submitting} disabled={!isLoggedIn || !selectedModel} size="large" diff --git a/src/pages/home/components/useUI/ImageInput.tsx b/src/pages/home/components/useUI/ImageInput.tsx index a5f0c28..38a73f2 100644 --- a/src/pages/home/components/useUI/ImageInput.tsx +++ b/src/pages/home/components/useUI/ImageInput.tsx @@ -14,10 +14,11 @@ // ============================================================ import { useState, useCallback } from 'react'; -import { Upload, Switch, Space, Typography, message } from 'antd'; +import { Upload, Switch, Space, Typography } from 'antd'; import { UploadOutlined } from '@ant-design/icons'; import type { UploadFile } from 'antd/es/upload'; import type { WidgetProps } from './types'; +import { createImageBeforeUpload } from './uploadValidation'; const { Text } = Typography; @@ -81,18 +82,7 @@ export function ImageInput({ value, onChange, disabled, config }: WidgetProps) { } }} disabled={isUploadDisabled} - beforeUpload={(file) => { - const isImage = file.type.startsWith('image/'); - if (!isImage) { - message.error('只能上传图片文件'); - return Upload.LIST_IGNORE; - } - if (file.size / 1024 / 1024 >= maxSizeMb) { - message.error(`图片大小不能超过 ${maxSizeMb}MB`); - return Upload.LIST_IGNORE; - } - return false; - }} + beforeUpload={createImageBeforeUpload(maxSizeMb)} > {fileList.length < 1 && (
diff --git a/src/pages/home/components/useUI/uploadValidation.ts b/src/pages/home/components/useUI/uploadValidation.ts
new file mode 100644
index 0000000..fa411f3
--- /dev/null
+++ b/src/pages/home/components/useUI/uploadValidation.ts
@@ -0,0 +1,26 @@
+// ============================================================
+// uploadValidation — 文件上传前校验工具
+// ImageInput / ImageListInput 共用
+// ============================================================
+
+import { Upload, message } from 'antd';
+import type { RcFile } from 'antd/es/upload';
+
+/**
+ * 创建图片上传前的类型 & 大小校验函数
+ * @param maxSizeMb 最大文件大小(MB),默认 10
+ */
+export function createImageBeforeUpload(maxSizeMb: number = 10) {
+ return (file: RcFile) => {
+ const isImage = file.type.startsWith('image/');
+ if (!isImage) {
+ message.error('只能上传图片文件');
+ return Upload.LIST_IGNORE;
+ }
+ if (file.size / 1024 / 1024 >= maxSizeMb) {
+ message.error(`图片大小不能超过 ${maxSizeMb}MB`);
+ return Upload.LIST_IGNORE;
+ }
+ return false;
+ };
+}
diff --git a/src/pages/login/components/ChangePasswordModal.tsx b/src/pages/login/components/ChangePasswordModal.tsx
index 72b0c8f..b737993 100644
--- a/src/pages/login/components/ChangePasswordModal.tsx
+++ b/src/pages/login/components/ChangePasswordModal.tsx
@@ -14,7 +14,7 @@ import { LockOutlined, KeyOutlined, SafetyOutlined } from '@ant-design/icons';
import { useTheme } from '@/components/ThemeProvider';
import { AuthAPI } from '@/services/modules/auth';
-import { validatePassword, validateConfirmPassword } from '../config/validators';
+import { validatePassword, validateConfirmPassword, runFieldChecks } from '../config/validators';
const { Text, Title } = Typography;
@@ -50,22 +50,12 @@ export function ChangePasswordModal({ open, onClose }: ChangePasswordModalProps)
// ---- 提交 ----
const handleSubmit = useCallback(async () => {
- const newErrors: Record