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

13 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

常用命令

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() 工厂函数返回标准接口:

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 绑定对:

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/tasksX-Session-Id header
  6. 20s 间隔轮询直至完成/失败

模型参数配置

Painting 模型参数 schema 在 src/platforms/painting/models/*.js 中,参数通过 ui 字段映射到 UI 控件:

ui 控件 说明
textarea Sender 内置 textarea prompt 输入框
proportion PaintingProportion / VideoProportion 比例选择 Popoveroptionscustom 时可自定义宽高)
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.jsdisplayNameMap 负责将 API 返回的 display_name 映射到 config key。同一模型在不同 tag 下可能共用一个 display_name(如 GPT-Image-2GPT-image-2config key 采用内部中文名区分。

已知 bugdisplayNameMap 存在重复 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.jsfetchPlatformModels 使用 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 删除历史记录

任务响应格式

// 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 按需加载