AI_Painting_V2.0/CLAUDE.md
WangLeo 481afadd2b fix: VirtualScroller 滚动锚定防抖 + platform 方法引用修复 + CLAUDE.md 更新
- VirtualScroller: measureItem 高度变化时,对可视区上方项的累积 delta 通过微任务延迟补偿 scrollTop,避免同步调整导致的画面抖动
- VirtualScroller: 新增独立测试页 test.html + test-data.js,用于验证虚拟滚动行为
- platform: 修复 painting/video 中 imageUploadLimit() 调用方式为 this.imageUploadLimit()
- display: 修复 Sender_variant 在非 pageTop/pageBottom 中间状态时未设置的问题,补充 isInitializing 异常状态重置
- CLAUDE.md: 补充 VirtualScroller 180deg 旋转机制说明、模型切换完整链路、反旋转注意事项
2026-06-09 15:52:31 +08:00

243 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 常用命令
```bash
pnpm dev # 启动 Vite 开发服务器
pnpm build # 生产构建
pnpm preview # 预览生产构建
npx eslint . # 代码检查(@antfu/eslint-configVue 支持,无 TypeScript
```
## 技术栈
Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + `vue-element-plus-x`(多媒体编辑) + Less + pnpm
## 架构概览
AI 绘画/视频生成前端操作平台,通过 HTTP 接口对接算力调度后端suanli提交生成任务并轮询结果。
**核心架构Platform Descriptor 模式。** Painting 和 Video 是两个独立的平台包通过统一的注册表动态加载。dialogBox 是通用编排壳,不包含任何平台特定逻辑。
### 关键目录
```
src/
├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/VueVirtualScroller
├── router/index.js # 路由定义 + token 验证守卫
├── platforms/ # 平台包(独立、可插拔)
│ ├── registry.js # 注册表registerPlatform(id, factory) + createPlatform(type)
│ ├── painting/ # Painting 平台
│ │ ├── index.js # definePaintingPlatform()controls、state、loadConfig、buildTaskBody 等
│ │ ├── modelSelector.vue # 模型选择器(按 API tags 分组)
│ │ ├── imageUploader.vue # 图片上传组件
│ │ ├── models/ # 模型参数 schema本地 JS待后端化
│ │ │ └── index.js # getModelConfig(modelName) → 查找 config
│ │ └── controls/ # 平台专用控件
│ │ ├── proportion.vue
│ │ ├── dimension.vue
│ │ ├── quality.vue
│ │ └── quantity.vue
│ └── video/ # Video 平台
│ ├── index.js # defineVideoPlatform()
│ ├── modelSelector.vue
│ ├── imageUploader.vue
│ └── controls/
│ ├── pattern.vue
│ ├── proportion.vue
│ └── time.vue
├── stores/ # Pinia 状态管理
│ ├── user.js # 用户认证、信息(含 sessionIdpinia persist 持久化 token
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
│ └── param.js # 参数 store当前为空
├── apis/ # HTTP API 层:纯请求封装,不含业务逻辑
│ ├── auth/ # 认证相关登录、token 校验、用户信息、验证码)
│ └── display/ # 任务创建/轮询/历史、平台模型、收藏/删除
├── components/
│ ├── dialogBox/ # 通用编排壳(核心交互入口)
│ │ └── index.vue # <component :is> 动态渲染平台控件,不含平台分支
│ ├── Popover/ # 自定义弹出层Teleport to bodyposition:fixed + fit-content 宽度)
│ ├── Select/ # 自定义下拉选择器(分组选项,与 Popover 协调互斥开关)
│ ├── Img/ # 图片包装组件点击全屏查看Teleport 实现)
│ ├── virtual-scroller/ # 虚拟滚动列表自定义实现。reverse 模式用 180deg 旋转实现底部锚定slot 内容须反旋转
│ └── canvas/ # 图片画布编辑(圆/矩形选区局部重绘undo/redo
├── views/ # 页面home、login
└── utils/
├── request.js # Axios 实例 + 拦截器:统一 Auth不带 Bearer+ 按前缀路由 baseURL
├── taskPolling.js # 任务生成入口:组装参数 → POST 创建任务 → 20s HTTP 轮询直至完成/失败
├── modelApi.js # 模型业务层localStorage 30s 缓存 + pendingRequests 并发去重 + 平台编码映射
├── createTask.js # 透传层return data.body各平台 buildTaskBody() 已返回扁平 modelParams
├── modelConfig.js # Video 专用:从远程 JSON 加载 workflow 配置(含 localStorage 每日缓存)
├── downloadImage.js # 图片/视频下载fetch → Blob → 自动文件名下载
├── uploadImage.js # 图片上传工具
├── tokenError.js # 认证失败处理:提示后 5 秒刷新页面
├── encrypt.ts # 加密工具Base64/MD5/RSA/AES依赖 crypto-js、jsencrypt
└── auth.ts # token 存取工具localStorage
```
### Platform Descriptor 模式
每个平台通过 `defineXxxPlatform()` 工厂函数返回标准接口:
```js
const platform = {
id: 'painting', // 平台唯一标识
label: 'AI绘画2026', // 显示名称
ModelSelector: markRaw(Component), // 模型选择器组件
modelSelectorProps: null, // 模型选择器的额外 props可为函数
controls: [{ // 控件描述符数组
name: 'proportion',
component: markRaw(Component),
show: (config) => boolean, // 根据 model config 决定是否显示
props: (config) => ({ ... }) // 根据 model config 生成 v-model props
}],
ImageUploader: markRaw(Component), // 图片上传组件(可为 null
state: { ... }, // 平台所有响应式状态
model, modelType, // 当前模型/类型ref
modelConfig, // 当前模型参数配置ref
promptPlaceholder, // 提示词占位文本ref
async loadModels() { ... }, // 获取模型列表
async loadConfig(modelName, modelType) { ... }, // 加载模型参数配置
getDefaultModel() { ... }, // 返回默认模型名
validateBeforeSubmit() { ... }, // 提交前校验,返回 null 表示通过
getUploaderBindings() { ... }, // 图片上传组件的绑定参数
showImageUploader() { ... }, // 是否显示图片上传区域
isImageRequired() { ... }, // 图片是否必填
buildTaskBody({ prompt, referenceImages }) { ... }, // 构造扁平 modelParams
fillFromResult(resultData) { ... }, // 从历史结果回填参数
}
```
控件通过 `ctrl.props(config)` 接收 v-model 绑定对:
```js
props: (config) => ({
modelValue: proportion.value,
'onUpdate:modelValue': (v) => { proportion.value = v },
// ...
})
```
**自注册:** 每个平台文件底部调用 `registerPlatform('Painting', definePaintingPlatform)`,在 import 时自动注册。dialogBox 通过 `createPlatform(props.type)` 获取实例。
### dialogBox 通用编排壳
`src/components/dialogBox/index.vue` 是纯编排组件,不含任何平台分支:
- **平台切换**`const platform = computed(() => createPlatform(props.type))`,切换时重置默认模型并加载模型列表
- **控件渲染**`visibleControls = platform.controls.filter(c => c.show(getCurrentConfig()))`,用 `<component :is>` + `v-bind="ctrl.props(...)"` 渲染
- **配置获取**`getCurrentConfig()` 返回 `platform.modelConfig?.value ?? platform.modelDisplayConfig?.value`,兼容两种配置来源
- **模型切换**`watch([model, modelType])` → `platform.loadConfig()``syncDefaults()` 将 schema 写入响应式 state → controls 的 `show()`/`props()` 读取 state → `visibleControls` 自动更新 → UI 重渲染
- **任务发起**`handleStart()` 调用 `platform.validateBeforeSubmit()``platform.buildTaskBody()``generate()`
- **参数回填**`fillParamsFromResult()` 委托给 `platform.fillFromResult()`
### 统一数据流Painting + Video
两个平台现已统一,`createTask.js` 是纯透传:
1. 用户选择模型 → `platform.loadConfig(modelName, modelType)` 加载参数 schema
2. 参数 schema 驱动 `controls` 渲染 UI用户填写参数
3. 用户点击发送 → `handleStart()``platform.buildTaskBody({ prompt, referenceImages })` 返回扁平 `modelParams`
4. `createTask(data)` 透传 `data.body`(不再做任何转换)
5. `getModelId(type, modelName)` 查找 UUID → POST `/suanli/v1/tasks``X-Session-Id` header
6. 20s 间隔轮询直至完成/失败
### 模型参数配置
Painting 模型参数 schema 在 `src/platforms/painting/models/*.js` 中,参数通过 `ui` 字段映射到 UI 控件:
| `ui` 值 | 控件 | 说明 |
|---------|------|------|
| `textarea` | Sender 内置 textarea | prompt 输入框 |
| `proportion` | `PaintingProportion` / `VideoProportion` | 比例选择 Popover`options` 含 `custom` 时可自定义宽高) |
| `resolution` | proportion 控件内部 | 分辨率子选项,与 proportion 共用 Popover |
| `dimension` | `DimensionInput` | **组合模式**:单字段 `"W*H"` 格式,通过 `dimension.parse/format` 序列化 |
| `dimensionWidth` + `dimensionHeight` | `DimensionInput` | **拆分模式**:两个独立字段,共享同一 Popover 和比例锁 |
| `select` | `Select` | 通用下拉(如 quality |
| `quantity` | `Quantity` | 生成数量,上限由 `options` 最大值派生 |
| `imageUpload` | `ImageUploader` | 参考图上传,`maxCount` 控制上限 |
| `hidden` | 无 | 静默写入默认值 |
**dimension 模式区分**:通过 `getDimConfig()` 自动检测 `ui: 'dimension'`(组合)或 `ui: 'dimensionWidth'`(拆分),两种模式共用 `DimensionInput` 组件。
**条件显示**`showWhen: { aspectRatio: 'custom' }` 使参数仅在 proportion 选 `custom` 时显示。
### `displayNameMap` 机制
`src/platforms/painting/models/index.js``displayNameMap` 负责将 API 返回的 `display_name` 映射到 config key。同一模型在不同 tag 下可能共用一个 `display_name`(如 `GPT-Image-2``GPT-image-2`config key 采用内部中文名区分。
**已知 bug**`displayNameMap` 存在重复 key `'GPT-Image-2'`,第二条会覆盖第一条,导致文字生图版 GPT-Image-2 查找走 `displayNameMap` 时映射到 I2I 版。当前因 `model.value` 已是中文 config key 直达 `configs[]`,暂不触发。若后续改为按 `display_name` 查找,需修复此重复 key。
### `$attrs` 穿透注意
向子组件传递的 prop 如果子组件未声明,会通过 `$attrs` 穿透到根元素。**所有通过 `v-model` 传递的值,子组件必须声明对应的 prop。**
### API 层设计原则
- `src/apis/` 只做纯 HTTP 调用(`service.get/post/delete`不含缓存、localStorage、业务逻辑
- 缓存、数据转换等业务逻辑放在 `src/utils/`
### 关键注意事项
- **`sessionId`** 来自登录接口返回的 `userInfo.sessionId`,存储在 `useUserStore().userInfo` 中。`taskPolling.js` 必须使用该值,禁止随机生成。
- **`X-Session-Id`** 自定义 header 需要 nginx 在 `/suanli/` location 的 `Access-Control-Allow-Headers` 中加入,否则 POST 请求会触发 CORS 预检失败。
- **模型列表缓存**`modelApi.js` 中 `fetchPlatformModels` 使用 localStorage 30 秒 TTL + `pendingRequests` Map 并发去重。
- **平台包预加载**dialogBox 顶层 `import` Painting 和 Video 平台包,触发自注册,确保首次使用时 registry 已就绪。
- **VirtualScroller 反旋转**:组件用外层 `transform: rotate(180deg)` 实现 reverse 底部锚定。所有作为 slot 插入的内容必须在根元素加 `transform: rotate(180deg)` 反旋转,否则文字/图片会颠倒显示。参考 `src/views/home/display/components/set.vue:2`
### 接口速查
| 函数 | 端点 | 用途 |
|------|------|------|
| `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, display_name, tags, disabled?}` |
| `cancelOrCollect` | POST `/collect/toggle` | 收藏/取消收藏 |
| `deleteGenerateHistory` | DELETE `/taskRecordHistory/delete` | 删除历史记录 |
### 任务响应格式
```json
// GET /suanli/v1/tasks/:id 返回结构
{
"code": 0,
"data": {
"task_id": "uuid",
"status": "completed", // queued | processing | completed | failed
"outputs": [ // ⚠️ 扁平数组,不是 { images: [...] }
{ "url": "https://...", "type": "png" }
],
"vendor_error": "..." // 仅 failed 时有值
}
}
```
### 请求拦截器路由
拦截器统一设置 `Authorization: <token>`(不带 Bearer 前缀),根据 URL 前缀切换后端:
| URL 前缀 | 环境变量 |
|----------|----------|
| `/suanli` | `VITE_API_TASK_TARGET` |
| `/pay` | `VITE_API_PAY_TARGET` |
| `/aigc` | `VITE_API_AIGC_TARGET` |
| 其他 | `VITE_API_BASE_URL`(默认) |
### 平台编码映射
| 类型 | 平台编码 |
|------|----------|
| Painting | `ai_painting_talk` |
| Video | `ai_video_talk` |
映射函数 `getPlatformCode()` 位于 `utils/modelApi.js`
### 自动导入
- `unplugin-auto-import`:自动导入 Vue/Router/Pinia API
- `unplugin-vue-components`:自动注册 `src/components/` 下的组件和 Element Plus 组件
- Element Plus 图标通过 `unplugin-icons` 按需加载