基于旧项目 ai_music_v2.0 迁移,与 Painting/Video 统一架构:HTTP 轮询 + suanli 后端、 API 驱动配置、mode 独立 ref 驱动控件显隐。新增 AudioPlayer/CustomSlider 通用组件, dialogBox/set.vue/taskPolling/modelApi 完成集成适配。
24 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
常用命令
pnpm install # 安装依赖
pnpm dev # 启动 Vite 开发服务器(默认 http://localhost:5173)
pnpm build # 生产构建
pnpm preview # 预览生产构建
npx eslint . # 代码检查(@antfu/eslint-config,Vue 支持,无 TypeScript)
技术栈
Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + vue-element-plus-x(提供 Sender 输入框组件) + Less + pnpm
架构概览
AI 绘画/视频生成前端操作平台,通过 HTTP 接口对接算力调度后端(suanli),提交生成任务并轮询结果。
核心架构:Platform Descriptor 模式。 Painting 和 Video 是两个独立的平台包,通过统一的注册表动态加载。dialogBox 是通用编排壳,不包含任何平台特定逻辑。
关键目录
├── model-configs/ # 运维参考:模型参数配置 JSON 文件(hailuo, ltx, vidu 等,位于项目根目录)
├── config/
│ └── plugins.js # Vite 插件配置(unplugin-auto-import + unplugin-vue-components resolver)
├── vite.config.js # 构建配置:alias(@→src, ~→根目录)、envPrefix: ['VITE','FILE']、optimizeDeps
src/
├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/vue-virtual-scroller
├── router/index.js # 路由定义 + token 验证守卫
├── assets/ # 静态资源
│ ├── dialog/ # 控件区域 SVG 图标(model, proportion, quantity, time, videoPattern 等)
│ └── display/ # 结果展示区 SVG 图标(download, collection, delete, back, brush 等)
├── platforms/ # 平台包(独立、可插拔)
│ ├── registry.js # 注册表:registerPlatform(id, factory) + createPlatform(type)
│ ├── painting/ # Painting 平台
│ │ ├── index.js # definePaintingPlatform():controls、state、loadConfig、buildTaskBody 等
│ │ ├── modelSelector.vue # 模型选择器(按 API tags 分组)
│ │ ├── imageUploader.vue # 图片上传组件
│ │ └── 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 # 用户认证、信息(含 sessionId),pinia persist 持久化 token
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
│ └── param.js # 参数 store(当前为空)
├── apis/ # HTTP API 层:纯请求封装,不含业务逻辑
│ ├── auth/ # 认证相关(登录、token 校验、用户信息、验证码)
│ └── display/ # 任务创建/轮询/历史、平台模型、收藏/删除
├── components/
│ ├── dialogBox/ # 通用编排壳(核心交互入口)
│ │ └── index.vue # <component :is> 动态渲染平台控件,不含平台分支
│ ├── Popover/ # 自定义弹出层(Teleport to body,position:fixed + fit-content 宽度)
│ ├── Select/ # 自定义下拉选择器(分组选项,与 Popover 协调互斥开关)
│ ├── ParamGroup/ # 动态参数容器:遍历 config.params 中未被专用控件处理的 select/switch 参数
│ ├── SwitchControl/ # 纯 CSS 布尔开关(不用外部组件库)
│ ├── Img/ # 图片包装组件(点击全屏查看,Teleport 实现)
│ ├── virtual-scroller/ # 虚拟滚动列表(自定义实现)。reverse 模式用 180deg 旋转实现底部锚定,slot 内容须反旋转
│ └── canvas/ # 图片画布编辑(圆/矩形选区,局部重绘,undo/redo)
├── views/ # 页面(home、login)
└── utils/
├── request.js # Axios 实例 + 拦截器:统一 Auth(不带 Bearer)+ 按前缀路由 baseURL
├── taskPolling.js # 任务生成入口:组装参数 → POST 创建任务 → 首次 5s + 后续 20s HTTP 轮询直至完成/失败
├── modelApi.js # 模型业务层:localStorage 缓存 + 并发去重。平台模型列表(30s TTL) + 模型参数配置(60s TTL)
├── modelConfigHelper.js # 模型配置共享工具:syncDefaults / syncParamValues / getDimConfig / checkShowWhen
├── downloadImage.js # 图片/视频下载:fetch → Blob → 自动文件名下载
├── uploadImage.js # 图片上传工具
├── tokenError.js # 认证失败处理:提示后 5 秒刷新页面
├── encrypt.ts # 加密工具(⚠️ 死代码:依赖 crypto-js/jsencrypt 未安装,无任何 import 引用)
└── 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),
beforeModel: false, // 设为 true 则渲染在 ModelSelector 之前
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) { ... }, // 加载模型参数配置(modelName 可以是 UUID/name/display_name)
getDefaultModel() { ... }, // 返回默认模型标识(可返回 '' 交由 modelSelector 自动纠错)
imageUploadLimit() { ... }, // 返回图片上传槽位数(应累加所有 imageUpload 参数的 maxCount)
validateBeforeSubmit() { ... }, // 提交前校验,返回 null 表示通过
getUploaderBindings() { ... }, // 图片上传组件的绑定参数(modelType + imagesCount)
showImageUploader() { ... }, // 是否显示图片上传区域
isImageRequired() { ... }, // 图片是否必填
buildTaskBody({ prompt, referenceImages }) { ... }, // 构造扁平 modelParams,需将 referenceImages 映射到 imageUpload 参数
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) 获取实例。注册表内部将 key 统一转为小写,因此 'Painting' 和 'painting' 等效。
dialogBox 通用编排壳
src/components/dialogBox/index.vue 是纯编排组件,不含任何平台分支:
- 平台切换:
const platform = computed(() => createPlatform(props.type)),切换时重置默认模型并加载模型列表 - 控件渲染:
visibleControls = platform.controls.filter(c => c.show(getCurrentConfig())),按beforeModel拆分为两组,beforeModel控件 → ModelSelector → 其余控件,用<component :is>+v-bind="ctrl.props(...)"渲染 - 配置获取:
getCurrentConfig()返回platform.modelConfig?.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)
两个平台现已统一,通过后端 API 获取模型参数配置:
- 用户选择模型 →
platform.loadConfig(modelName, modelType)→ 调用GET /suanli/v1/models/:id/config(优先 60s 缓存)加载参数 schema - 参数 schema 驱动
controls渲染 UI,用户填写参数 - 用户点击发送 →
handleStart()→platform.buildTaskBody({ prompt, referenceImages })返回扁平modelParams taskPolling.js直接读取data.body→getModelId(type, modelName)查找 UUID → POST/suanli/v1/tasks(X-Session-Idheader)- 20s 间隔轮询直至完成/失败
模型标识与查找
API 返回的模型对象包含三个标识字段:
| 字段 | 示例 | 用途 |
|---|---|---|
id |
e7e1e743-7621-403d-b8fb-2b7f4fa1b4fc |
UUID 主键,唯一且稳定,用于 API 调用和内部追踪 |
name |
vidu-text-to-video-q3-turbo |
内部标识名,通常也唯一 |
display_name |
Vidu q3-turbo |
用户可见的显示名,可能重复(不同 pattern 下的同名模型) |
Video 平台使用 id(UUID)作为 model.value,避免 display_name 冲突导致的模型查找错误。modelSelector.vue 中 modelGroups 的 value 设为 m.id,label 仍用 display_name 显示。
getModelId(type, modelName) 查找优先级:m.id === modelName → m.name === modelName → m.display_name === modelName,向下兼容旧的 name/display_name 调用。
Video 的 getDefaultModel() 返回 '',不硬编码 UUID;模型列表加载后由 modelSelector 的 watcher 自动选择第一个可用模型。
模型参数配置
模型参数配置通过后端 API GET /suanli/v1/models/:id/config 获取(60s localStorage 缓存),替代了旧的本地硬编码。参数通过 ui 字段映射到前端控件:
ui 值 |
控件 | 说明 |
|---|---|---|
textarea |
Sender 内置 textarea | prompt 输入框 |
proportion |
PaintingProportion / VideoProportion |
比例选择 Popover(options 含 custom 时可自定义宽高) |
resolution |
proportion 控件内部 | 分辨率子选项,与 proportion 共用 Popover |
dimension |
DimensionInput |
组合模式:单字段 "W*H" 格式,后端传 dimension.separator,前端生成 parse/format |
dimensionWidth + dimensionHeight |
DimensionInput |
拆分模式:两个独立字段(含 min/max),共享比例锁 |
number |
proportion 控件内部 | 自定义宽高(如 Flux 的 customWidth/customHight),配合 showWhen 条件显示 |
select |
Select(通过 ParamGroup 或专用控件) |
通用下拉。专用控件直接使用 Select,其余由 ParamGroup 动态渲染 |
switch |
SwitchControl(通过 ParamGroup) |
布尔开关,纯 CSS 实现,不用外部组件库 |
quantity |
Quantity |
生成数量,options 必须为数字数组,上限由 Math.max() 派生 |
imageUpload |
ImageUploader |
参考图上传,maxCount 控制上限 |
hidden |
无 | 静默写入默认值 |
dimension 模式区分:getDimConfig()(来自 modelConfigHelper.js)自动检测 ui: 'dimension'(组合)或 ui: 'dimensionWidth'(拆分),两种模式共用 DimensionInput 组件。
条件显示:showWhen: { aspectRatio: 'custom' } 使参数仅在 proportion 选 custom 时显示。checkShowWhen() 在 controls 的 show() 回调中调用,因直接读取 reactive 的 paramValues[key],computed 自动追踪依赖。
modelConfigHelper 共享工具
src/utils/modelConfigHelper.js 提供 Painting/Video 双平台共用的 4 个纯函数:
| 函数 | 说明 |
|---|---|
syncDefaults(config, state) |
将 API 返回的 config 同步到响应式 state。处理 paramValues 初始化、专用 ref 同步(proportion/resolution/quantity/quality/duration/dimension)、promptPlaceholder 同步。resolution 同时匹配 ui:"resolution" 和 ui:"select" |
syncParamValues(config, state) |
在 buildTaskBody 前将专用 ref(proportion/resolution/quantity/quality/duration/dimension)回写到 paramValues |
getDimConfig(config) |
检测 combined/split 模式 |
checkShowWhen(param, paramValues) |
检查 showWhen 条件 |
state 参数对象需包含 modelConfig, paramValues, proportion, resolution, quantity, quality, customWidth, customHight, dimWidth, dimHeight, promptPlaceholder。Video 平台额外包含 duration(用于 syncDefaults 同步时长默认值)。各平台通过包装函数适配。
ParamGroup 动态参数渲染
当 API 返回的 params 中包含无专用控件的类型(如 select、switch),由 ParamGroup 统一处理:
- 容器定位:作为平台 controls 数组的最后一项,
show()检查是否存在未被handledUis覆盖的参数 handledUis:['textarea', 'proportion', 'resolution', 'dimension', 'dimensionWidth', 'dimensionHeight', 'quantity', 'imageUpload', 'hidden', 'number']— 这些类型的参数由专用控件处理,ParamGroup 跳过excludeNamesprop:额外按参数名排除(如resolution、duration),因为 VideoProportion/Time 已处理这些参数(即使其ui类型不在 handledUis 中)- 双层过滤:平台
show()做第一层(判断是否渲染 ParamGroup),ParamGroup 内部dynamicParams做第二层(判断具体渲染哪些参数)。两层必须保持一致,否则会出现重复控件
// Video 平台的 ParamGroup 配置
{
name: 'paramGroup',
component: markRaw(ParamGroup),
show: (config) => {
// 第一层:有未被处理的参数才显示
return config.params.some((p) => {
if (handledUis.includes(p.ui)) return false
if (p.name === 'resolution') return false
if (p.name === 'duration') return false
return true
})
},
props: (config) => ({
config,
paramValues,
excludeNames: ['resolution', 'duration'] // 第二层过滤
})
}
组件规范
禁止在项目组件中使用外部 UI 库(Element Plus 等),图标除外。自定义组件使用项目自研的 Select、Popover 或纯 CSS 实现。SwitchControl 即为一例——纯 CSS 滑动开关,不依赖 el-switch。
$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 +pendingRequestsMap 并发去重。 - 模型配置缓存:
modelApi.js中getModelConfig使用 localStorage 60 秒 TTL +pendingConfigRequestsMap 并发去重。loadModels()会在获取模型列表后调用preloadModelConfigs批量预加载。 - 平台包预加载:dialogBox 顶层
importPainting 和 Video 平台包,触发自注册,确保首次使用时 registry 已就绪。 - VirtualScroller 实现说明:虽然
package.json安装了vue-virtual-scroller(npm 包)并在main.js中全局注册,但display/index.vue中<VirtualScroller>实际使用的是src/components/virtual-scroller/VirtualScroller.vue自定义实现(通过unplugin-vue-components自动注册的同名组件遮蔽 npm 包)。vue-virtual-scrollernpm 包当前为冗余依赖。 - VirtualScroller 反旋转:组件用外层
transform: rotate(180deg)实现 reverse 底部锚定。所有作为 slot 插入的内容必须在根元素加transform: rotate(180deg)反旋转,否则文字/图片会颠倒显示。参考src/views/home/display/components/set.vue:2。 - VirtualScroller 存在独立测试页:
src/components/virtual-scroller/test.html+test-data.js,可用于验证虚拟滚动行为。 - VirtualScroller 残留备份文件:
src/components/virtual-scroller/VirtualScroller copy.vue是备份副本,不应被引用或修改。 - Element Plus
<el-input type="textarea">不响应autosize动态变化:ElInput只在 mount 时读取autosize,prop 更新时不会重新计算高度。因此dialogBox中Sender必须绑定:key="useDisplay.Sender_variant",通过强制重挂载来使新的autosize(minRows/maxRows)生效。 - Video modelSelector pattern→modelType 映射:
getModelType()在modelSelector.vue中将 pattern tag 映射为 modelType——"文生视频"→text,"图生视频"→imageToVideo,"首尾帧"→image,"数字人"→digitalHuman,"全能参考"→allReference,"主体参考"→subjectReference。该值用于 imageUploader 的标签文本和上传槽位数量。 - Video
imageUploadLimit()累加逻辑:对于有多个imageUpload参数的模型(如首尾帧模型的firstImageUrl+lastImageUrl),应累加所有imageUpload参数的maxCount,而非只取第一个。否则首尾帧模型只显示一个上传槽位,尾帧上传无法触发。 buildTaskBody参考图映射:Video 平台在buildTaskBody中需将referenceImages按索引顺序写入imageUpload参数(referenceImages[0]→firstImageUrl,referenceImages[1]→lastImageUrl),否则图片数据不会包含在任务请求中。
VirtualScroller 坐标系统
VirtualScroller 使用 180deg 旋转实现 reverse 模式。handleScroll 内部将容器物理坐标映射为"页面逻辑坐标"后通过 emit('scroll', {...}) 发出:
| 字段 | 计算 | 含义 |
|---|---|---|
distanceToPageTop |
scrollHeight - scrollTop - clientHeight |
距离页面顶部(旧内容端)的距离 |
distanceToPageBottom |
scrollTop |
距离页面底部(新内容端)的距离 |
isAtPageTop |
distanceToPageTop <= 0 |
滚动到页面最顶部 |
isAtPageBottom |
distanceToPageBottom <= 0 |
滚动到页面最底部 |
注意:distanceToPageBottom = scrollTop(不是 distanceToPageTop),表示从底部向上滚动的距离。判断"用户向上滚动了多少"应使用此字段。
输入框滚动收缩机制
滚动列表时提示词输入框自动收缩为一行,这是跨 4 个文件的响应式链路:
VirtualScroller发出scroll事件(含isAtPageBottom、distanceToPageBottom等)display/index.vue的handleScroll根据滚动位置切换useDisplay.Sender_variant:isAtPageBottom(底部)→'updown'(展开,5-9 行)distanceToPageBottom >= 350(已滚离底部 350px)→'default'(收缩,1 行)- 中间状态 →
'updown'(展开)
display.jsstore 中Sender_variant是响应式 refdialogBox/index.vue的autoSizeConfig计算属性读取Sender_variant,返回{ minRows, maxRows }Sender组件通过:key="useDisplay.Sender_variant"强制重挂载(因为 Element PlusElInput不支持autosize动态更新),新实例以正确的行数初始化
滚动阈值 350px 对应 VirtualScroller 的 bottomPlaceholderHeight。
接口速查
| 函数 | 端点 | 用途 |
|---|---|---|
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(UUID), name, display_name, tags, disabled?})。id=UUID 主键,name=内部标识,display_name=用户可见名 |
requestModelConfigsBatch |
POST /suanli/v1/models/configs |
批量获取模型配置(body: { modelIds: [...] }) |
requestModelConfig |
GET /suanli/v1/models/:id/config |
单条模型配置(60s 缓存优先) |
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 前缀(由环境变量 VITE_API_TASK_PREFIX / VITE_API_PAY_PREFIX / VITE_API_AIGC_PREFIX 定义)切换后端:
| URL 前缀(由环境变量定义) | 目标环境变量 |
|---|---|
VITE_API_TASK_PREFIX 对应前缀 |
VITE_API_TASK_TARGET |
VITE_API_PAY_PREFIX 对应前缀 |
VITE_API_PAY_TARGET |
VITE_API_AIGC_PREFIX 对应前缀 |
VITE_API_AIGC_TARGET |
| 其他 | VITE_API_BASE_URL(默认) |
注意:前缀字符串本身来自环境变量(如 VITE_API_TASK_PREFIX=/suanli),不是硬编码。request.js 在初始化时读取这些变量,构建 prefix→target 映射表。
环境变量速查
# .env.development
VITE_BASE = '/' # 应用基础路径
VITE_API_PREFIX = '/api' # 主服务前缀
VITE_API_BASE_URL = 'http://...' # 主服务(默认目标)
VITE_API_PAY_PREFIX = '/pay' # 支付服务前缀
VITE_API_PAY_TARGET = 'http://...' # 支付服务目标
VITE_API_TASK_PREFIX = '/suanli' # 任务服务前缀
VITE_API_TASK_TARGET = 'http://...' # 任务服务目标
VITE_API_WORKFLOW_UPLOAD = 'http://...' # 图片上传地址(imageUploader 组件 action)
VITE_OPEN_DEVTOOLS = false # 是否开启开发者工具
FILE_OPEN_PREVIEW = true # 是否开启 KKFileView 预览
vite.config.js 中 envPrefix: ['VITE', 'FILE'],因此只有以 VITE_ 和 FILE_ 开头的变量会被暴露给客户端代码。
平台编码映射
| 类型 | 平台编码 |
|---|---|
| Painting | ai_painting_talk |
| Video | ai_video_talk |
映射函数 getPlatformCode() 位于 utils/modelApi.js。
自动导入
unplugin-auto-import:自动导入 Vue/Router/Pinia APIunplugin-vue-components:自动注册src/components/下的组件和 Element Plus 组件,生成components.d.ts(勿手动编辑)- Element Plus 图标通过
unplugin-icons按需加载