diff --git a/CLAUDE.md b/CLAUDE.md index e5f0ff5..d15356d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,8 +20,8 @@ AI 绘画/视频生成前端操作平台,通过 HTTP 接口对接算力调度 **Painting 和 Video 走两套不同的任务构造路径:** -- **Painting(新架构)**:本地模型参数 schema → 动态表单 → 扁平 API body 提交 -- **Video(旧架构)**:远程 workflow JSON → RunningHub Playload 适配器 → `{ workflowId, nodeInfoList }` body 提交 +- **Painting(新架构)**:本地模型参数 schema → 专用控件 + 动态表单 → `X-Session-Id` header + 扁平 API body +- **Video(旧架构)**:远程 workflow JSON → RunningHub Playload 适配器 → `{ workflowId, nodeInfoList }` body ### 关键目录 @@ -30,27 +30,30 @@ src/ ├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/VueVirtualScroller ├── router/index.js # 路由定义 + token 验证守卫 ├── stores/ # Pinia 状态管理 -│ ├── user.js # 用户认证、信息 -│ └── display.js # 生成历史列表、UI 状态(滚动、画布等) +│ ├── user.js # 用户认证、信息(含 sessionId) +│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等) +│ └── param.js # 参数 store(当前为空) ├── apis/ # HTTP API 层:纯请求封装,不含业务逻辑 │ ├── auth/ # 认证相关(登录、token 校验、用户信息) │ └── display/ # 任务创建/轮询/历史、平台模型、收藏/删除 ├── components/ │ ├── dialogBox/ # 生成参数输入面板(核心交互入口) -│ │ ├── model/ # 模型选择器(painting 按 tag 分组,video 按 pattern 分组) -│ │ ├── params/ # 动态参数控件(Painting 新架构):ProportionSelect、ResolutionSelect、SelectInput 等 -│ │ ├── proportion/ # 比例选择器(仅 video.vue,Video 旧架构) +│ │ ├── model/ # 模型选择器(按 API 返回的 tags 分组,value 编码为 tag::display_name) +│ │ ├── proportion/ # 比例/分辨率选择器(painting.vue 用于 Painting,video.vue 用于 Video) │ │ ├── imageUploader/ # 图片上传(Painting) -│ │ └── videoImageUploader/ # 视频图片上传(Video) +│ │ ├── videoImageUploader/ # 视频图片上传(Video) +│ │ ├── quantity/ # 生成数量选择器(支持 1-6) +│ │ ├── Time/ # 视频时长选择器 +│ │ └── pattern/ # 视频模式选择器 │ ├── virtual-scroller/# 虚拟滚动列表组件(自定义实现,reverse 模式) │ └── canvas/ # 图片画布编辑(圆/矩形选区,局部重绘) ├── views/ # 页面(home、login) ├── utils/ │ ├── request.js # Axios 实例 + 拦截器:统一 Auth(不带 Bearer)+ 按前缀路由 baseURL -│ ├── websocket.js # 任务生成入口:组装参数 → 调用 API → 20s 轮询直至完成/失败 -│ ├── modelApi.js # 模型业务层:localStorage 每日缓存 + 模型名称→UUID 查找 + 平台编码映射 +│ ├── websocket.js # 任务生成入口:组装参数 → POST 创建任务 → 20s 轮询直至完成/失败 +│ ├── modelApi.js # 模型业务层:localStorage 30s 缓存 + pendingRequests 并发去重 + 平台编码映射 │ ├── createTask.js # 任务 body 构造:Painting 返回 modelParams,Video 走 Playload 适配器 -│ ├── modelConfig.js # Video 旧架构:从远程 JSON 加载 workflow 配置(Video 专用) +│ ├── modelConfig.js # Video 旧架构:从远程 JSON 加载 workflow 配置 │ └── auth.ts # token 存取工具(localStorage) ├── config/ │ ├── index.js # 平台配置入口(目前仅导出 runninghub 供 Video 使用) @@ -60,67 +63,56 @@ src/ ### 模型参数配置(Painting 新架构) -`src/config/models/` 下每个模型一个 JS 文件,定义该模型的 API 参数 schema: +`src/config/models/` 下每个模型一个 JS 文件,参数通过不同 UI 组件承载: -```js -export default { - name: 'Flux 2', - tag: '文生图', // 与 API 返回的 tag 对应,用于模型选择器分组 - inputType: 'text', // 'text' | 'image' | 'both' — 控制是否显示图片上传 - maxImages: 4, // 最大上传图片数(inputType 为 image/both 时有效) - params: [ - { - name: 'prompt', // API 字段名 - label: '提示词', - type: 'string', // 'string' | 'number' | 'boolean' | 'select' | 'image' - required: true, - ui: 'textarea', // 渲染控件:'textarea' | 'proportion' | 'resolution' | 'select' | 'number' | 'switch' | 'imageUpload' - default: '', - options: [...], // select 类型的枚举值 - showWhen: { aspectRatio: 'custom' }, // 条件显示(可选) - }, - ], -} -``` +- **`ui: 'textarea'`** → Sender 组件主输入框(prompt) +- **`ui: 'proportion'`** + **`ui: 'resolution'`** → `paintingProportion` 组件(共用 Popover,含自定义 W/H) +- **`ui: 'quantity'`** → `Quantity` 组件(动态 1-6 张) +- **`ui: 'imageUpload'`** → `ImageUploader` 组件 +- **`ui: 'hidden'`** → 无 UI,仅写入默认值(如 outputFormat: 'png') -- `src/config/models/index.js` 提供 `getModelConfig(modelName)` 查找函数 -- `ui: 'textarea'` 的参数由 Sender 组件承载(prompt),`ui: 'imageUpload'` 由独立上传组件处理,其余渲染为 params/ 下的动态控件 -- 模型选择器从 API(`fetchPlatformModels`)获取模型列表,按 `tag` 字段分组 +模型选择器从 API(`fetchPlatformModels`)获取模型列表,按 API 返回的 `tags` 数组字段分组(`text`→生成模型,`edit`→编辑模型,`vision`→视觉理解模型)。 + +### `displayNameMap` 机制 + +`src/config/models/index.js` 中 `displayNameMap` 负责将 API 返回的 `display_name` 映射到 config key。因为同一模型在不同 tag 下可能共用一个 `display_name`(如 `GPT-Image-2` 和 `GPT-image-2` 分别对应编辑/生成),config key 采用内部中文名区分。 ### API 层设计原则 -- `src/apis/` 中的函数只做**纯 HTTP 调用**(`service.get/post/delete` 等),不包含缓存、localStorage、业务判断等逻辑 -- 缓存、数据转换等业务逻辑放在 `src/utils/` 中,调用 apis 层的原始函数 - -示例:`utils/modelApi.js` 导入 `apis/display` 的原始 `fetchPlatformModels`,在其上叠加 localStorage 每日缓存和 `getModelId` 查找逻辑。 +- `src/apis/` 只做纯 HTTP 调用(`service.get/post/delete`),不含缓存、localStorage、业务逻辑 +- 缓存、数据转换等业务逻辑放在 `src/utils/` 中 ### 核心数据流 **Painting(新架构):** -1. 用户在 `dialogBox` 中设置参数——模型选择器从 API 获取模型列表按 tag 分组,参数控件根据模型 config 动态渲染 -2. 点击生成 → `dialogBox:handleStart()` 收集 `paramValues`(含 prompt)→ 组装 data(含 `modelParams` 扁平对象) -3. 调用 `websocket.js:generate(data, generateData)` -4. `generate()` 内部通过 `createTask(data)` → 因 `type === 'Painting'` 直接返回 `data.modelParams` 作为 body -5. 调用 `modelApi.getModelId(type, modelName)` 查找模型 UUID -6. 调用 `requestCreateTask(body, sessionId)` → POST `/suanli/v1/tasks`(`{ model_id, body: modelParams, request }`) -7. 返回 task_id → 轮询直至完成 +1. 用户设置参数 → 模型选择器按 `tags` 分组,控件根据 model config 的 `ui` 字段渲染 +2. `handleStart()` 收集 `paramValues`(UI refs 通过 watcher 双向同步)→ 组装 `{ modelParams, request }` +3. `websocket.js:generate()` → `createTask(data)` → Painting 直接返回 `data.modelParams` +4. `getModelId(type, modelName)` 查找 UUID(内部调用 `fetchPlatformModels` 走缓存) +5. `requestCreateTask(body, sessionId)` → POST `/suanli/v1/tasks`,携带 `X-Session-Id` header +6. 返回 task_id → 20s 间隔轮询直至完成/失败 **Video(旧架构,保留):** -1. 用户在 `dialogBox` 中设置参数(Pattern、videoModel、比例、时长) -2. 点击生成 → `dialogBox:handleStart()` 组装 data(含旧 params 数组) -3. `createTask(data)` → `runninghub.Playload(data)` → `fetchModelConfig()` 获取 workflow JSON → 返回 `{ workflowId, nodeInfoList }` -4. 后续同 Painting 的步骤 5-7 +1. 用户设置 Pattern、videoModel、比例、时长 +2. `createTask(data)` → `runninghub.Playload(data)` → `fetchModelConfig()` 获取 workflow JSON → 返回 `{ workflowId, nodeInfoList }` +3. 后续同 Painting 步骤 4-6 + +### 关键注意事项 + +- **`sessionId`** 来自登录接口返回的 `userInfo.sessionId`,存储在 `useUserStore().userInfo` 中。`websocket.js` 必须使用该值,禁止随机生成。 +- **`X-Session-Id`** 自定义 header 需要 nginx 在 `/suanli/` location 的 `Access-Control-Allow-Headers` 中加入,否则 POST 请求会触发 CORS 预检失败。 +- **模型列表缓存**:`modelApi.js` 中 `fetchPlatformModels` 使用 localStorage 30 秒 TTL + `pendingRequests` Map 并发去重,避免重复请求。 ### 接口速查 | 函数 | 端点 | 用途 | |------|------|------| -| `requestCreateTask` | POST `/suanli/v1/tasks` | 创建生成任务 | +| `requestCreateTask` | POST `/suanli/v1/tasks` | 创建任务(带 `X-Session-Id` header) | | `requestTaskStatus` | GET `/suanli/v1/tasks/:id` | 查询单个任务状态 | | `requestTaskHistory` | GET `/suanli/v1/tasks/history` | 历史任务列表(支持 `user_id`/`platform_code`/`page`/`pageSize`) | -| `fetchPlatformModels` | GET `/suanli/v1/platforms/:code/models` | 获取平台模型列表(返回 `{id, name, tag, disabled?}`) | +| `fetchPlatformModels` | GET `/suanli/v1/platforms/:code/models` | 获取平台模型列表(返回 `{id, display_name, tags, disabled?}`) | | `cancelOrCollect` | POST `/collect/toggle` | 收藏/取消收藏 | | `deleteGenerateHistory` | DELETE `/taskRecordHistory/delete` | 删除历史记录 | @@ -128,15 +120,12 @@ export default { 拦截器统一设置 `Authorization: `(不带 Bearer 前缀),根据 URL 前缀切换后端: -| URL 前缀 | 环境变量 | 示例 target | -|----------|----------|-------------| -| `/suanli` | `VITE_API_TASK_TARGET` | `http://test.xueai.art` | -| `/pay` | `VITE_API_PAY_TARGET` | `http://test.xueai.art` | -| 其他 | `VITE_API_BASE_URL`(默认) | `http://test.xueai.art/newapi/api` | - -### 响应格式兼容 - -响应拦截器同时识别 `code === 0` 和 `status === 0` 两种成功状态。用户信息接口兼容新旧两种格式:`data.userInfo` 嵌套(新)和 `data` 扁平(旧),在 store 的 `getInfo()` 中做了 `const u = res.data.userInfo || res.data` 的兼容处理。 +| URL 前缀 | 环境变量 | +|----------|----------| +| `/suanli` | `VITE_API_TASK_TARGET` | +| `/pay` | `VITE_API_PAY_TARGET` | +| `/aigc` | `VITE_API_AIGC_TARGET` | +| 其他 | `VITE_API_BASE_URL`(默认) | ### 平台编码映射 @@ -149,10 +138,6 @@ export default { ### 自动导入 -- `unplugin-auto-import`:自动导入 Vue/Router/Pinia API,`.vue` 中无需手动 import +- `unplugin-auto-import`:自动导入 Vue/Router/Pinia API - `unplugin-vue-components`:自动注册 `src/components/` 下的组件和 Element Plus 组件 - Element Plus 图标通过 `unplugin-icons` 按需加载 - -### 路由守卫 - -`src/router/index.js` 的 `beforeEach` 守卫检查 token 存在性和有效性(调用 `POST /login/validateToken`),无效则跳转 `/login`。支持通过 URL query `?token=xxx` 传入 token。 diff --git a/components.d.ts b/components.d.ts index 283a166..f0d77a2 100644 --- a/components.d.ts +++ b/components.d.ts @@ -18,6 +18,7 @@ declare module 'vue' { DialogBox: typeof import('./src/components/dialogBox/index.vue')['default'] ElButton: typeof import('element-plus/es')['ElButton'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] + ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElOption: typeof import('element-plus/es')['ElOption'] ElSelect: typeof import('element-plus/es')['ElSelect'] @@ -32,18 +33,14 @@ declare module 'vue' { IEpStar: typeof import('~icons/ep/star')['default'] ImageUploader: typeof import('./src/components/dialogBox/imageUploader/index.vue')['default'] Img: typeof import('./src/components/Img/index.vue')['default'] - NumberInput: typeof import('./src/components/dialogBox/params/NumberInput.vue')['default'] Painting: typeof import('./src/components/dialogBox/model/painting.vue')['default'] + ParamControl: typeof import('./src/components/dialogBox/ParamControl.vue')['default'] Pattern: typeof import('./src/components/dialogBox/pattern/index.vue')['default'] Popover: typeof import('./src/components/Popover/index.vue')['default'] - ProportionSelect: typeof import('./src/components/dialogBox/params/ProportionSelect.vue')['default'] Quantity: typeof import('./src/components/dialogBox/quantity/index.vue')['default'] - ResolutionSelect: typeof import('./src/components/dialogBox/params/ResolutionSelect.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] Select: typeof import('./src/components/Select/index.vue')['default'] - SelectInput: typeof import('./src/components/dialogBox/params/SelectInput.vue')['default'] - SwitchInput: typeof import('./src/components/dialogBox/params/SwitchInput.vue')['default'] Time: typeof import('./src/components/dialogBox/Time/index.vue')['default'] Video: typeof import('./src/components/dialogBox/model/video.vue')['default'] VideoImageUploader: typeof import('./src/components/dialogBox/videoImageUploader/index.vue')['default'] diff --git a/src/components/Select/index.vue b/src/components/Select/index.vue index 8039048..7d57928 100644 --- a/src/components/Select/index.vue +++ b/src/components/Select/index.vue @@ -261,6 +261,8 @@ onBeforeUnmount(() => { border: 1px solid #e8e8e8; animation: fadeIn 0.2s ease; gap: 10px; + max-height: 360px; + overflow-y: auto; } @keyframes fadeIn { diff --git a/src/components/dialogBox/index.vue b/src/components/dialogBox/index.vue index ed096da..20a3552 100644 --- a/src/components/dialogBox/index.vue +++ b/src/components/dialogBox/index.vue @@ -31,13 +31,17 @@