- .env 文件移除未使用的 VITE_API_PAY_* 变量,更新生产环境 URL - 删除 5 个已废弃的 model-configs JSON 文件 - CLAUDE.md 新增 Music 平台架构说明与计费类型映射 - getPlatformCode / getChargeType 增加输入大小写归一化
409 lines
26 KiB
Markdown
409 lines
26 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## 常用命令
|
||
|
||
```bash
|
||
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 和 Music 是三个独立的平台包,通过统一的注册表动态加载。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
|
||
│ └── music/ # Music 平台
|
||
│ ├── index.js # defineMusicPlatform()
|
||
│ ├── modelSelector.vue
|
||
│ ├── imageUploader.vue
|
||
│ └── controls/
|
||
│ ├── modeSelector.vue # 模式选择(常用/专业/Remix/编辑)
|
||
│ ├── pureMusicGroup.vue # 纯音乐开关 + 歌词输入弹窗
|
||
│ ├── lyricsInput.vue # 专业模式歌词输入
|
||
│ └── timeControl.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()` 工厂函数返回标准接口:
|
||
|
||
```js
|
||
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 绑定对:
|
||
```js
|
||
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 获取模型参数配置:
|
||
|
||
1. 用户选择模型 → `platform.loadConfig(modelName, modelType)` → 调用 `GET /suanli/v1/models/:id/config`(优先 60s 缓存)加载参数 schema
|
||
2. 参数 schema 驱动 `controls` 渲染 UI,用户填写参数
|
||
3. 用户点击发送 → `handleStart()` → `platform.buildTaskBody({ prompt, referenceImages })` 返回扁平 `modelParams`
|
||
4. `taskPolling.js` 直接读取 `data.body` → `getModelId(type, modelName)` 查找 UUID → POST `/suanli/v1/tasks`(`X-Session-Id` header)
|
||
5. 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 跳过
|
||
- **`excludeNames` prop**:额外按参数名排除(如 `resolution`、`duration`),因为 VideoProportion/Time 已处理这些参数(即使其 `ui` 类型不在 handledUis 中)
|
||
- **双层过滤**:平台 `show()` 做第一层(判断是否渲染 ParamGroup),ParamGroup 内部 `dynamicParams` 做第二层(判断具体渲染哪些参数)。**两层必须保持一致**,否则会出现重复控件
|
||
|
||
```js
|
||
// 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'] // 第二层过滤
|
||
})
|
||
}
|
||
```
|
||
|
||
### Music 平台特有行为
|
||
|
||
Music 平台与 Painting/Video 的关键差异:
|
||
|
||
- **模式驱动控件显隐**:`mode` ref(常用模式/专业模式/Remix模式/编辑模式)决定大部分控件的 `show()`。切换模式时 `visibleControls` 自动更新。
|
||
- **纯音乐/歌词互斥**:常用模式下 `pureMusicGroup` 控件内含纯音乐开关 + 歌词输入弹窗。开关开启时清空歌词;关闭时弹出歌词输入框。`buildTaskBody` 中 `pureMusic` 为 true 时 `lyrics` 为空字符串。
|
||
- **参考音频**:`imageUploader` 用于上传音频文件(非图片),仅在专业模式下显示(`showImageUploader()` → `mode === '专业模式'`),且为必填(`isImageRequired()` → true)。
|
||
- **数量限制**:专业模式下 `quantity` 强制为 1(`props()` 中做 `Math.min(mode === '专业模式' ? 1 : maxQty)`)。
|
||
- **Music 独有控件**:
|
||
|
||
| 控件 | `name` | `beforeModel` | 作用 |
|
||
|------|--------|---------------|------|
|
||
| `ModeSelector` | `modeSelector` | `true`(在模型选择器前) | 常用/专业/Remix/编辑 四选一 |
|
||
| `PureMusicGroup` | `pureMusicGroup` | `false` | 纯音乐开关 + 歌词输入(仅常用模式显示) |
|
||
| `LyricsInput` | `lyricsInput` | `false` | 歌词输入(仅专业模式显示) |
|
||
| `TimeControl` | `timeControl` | `false` | 时长滑块 min~max(仅常用模式显示) |
|
||
|
||
- **`handledUis`**(Music ParamGroup 过滤用):`['textarea', 'proportion', 'imageUpload', 'hidden', 'quantity']`,额外按名称排除 `['mode', 'pureMusic', 'lyrics', 'duration', 'quantity']`。
|
||
|
||
### 计费类型映射(`getChargeType`)
|
||
|
||
`taskPolling.js` 中的 `getChargeType(chargeType)` 将平台类型映射为计费数字:
|
||
|
||
| 平台 | 计费码 |
|
||
|------|--------|
|
||
| Painting | 1 |
|
||
| Video | 4 |
|
||
| Music | 5 |
|
||
| 其他 | 2 |
|
||
|
||
### 组件规范
|
||
|
||
**禁止在项目组件中使用外部 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 + `pendingRequests` Map 并发去重。
|
||
- **模型配置缓存**:`modelApi.js` 中 `getModelConfig` 使用 localStorage 60 秒 TTL + `pendingConfigRequests` Map 并发去重。`loadModels()` 会在获取模型列表后调用 `preloadModelConfigs` 批量预加载。
|
||
- **平台包预加载**:dialogBox 顶层 `import` Painting 和 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-scroller` npm 包当前为冗余依赖。
|
||
- **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 个文件的响应式链路:
|
||
|
||
1. `VirtualScroller` 发出 `scroll` 事件(含 `isAtPageBottom`、`distanceToPageBottom` 等)
|
||
2. `display/index.vue` 的 `handleScroll` 根据滚动位置切换 `useDisplay.Sender_variant`:
|
||
- `isAtPageBottom`(底部)→ `'updown'`(展开,5-9 行)
|
||
- `distanceToPageBottom >= 350`(已滚离底部 350px)→ `'default'`(收缩,1 行)
|
||
- 中间状态 → `'updown'`(展开)
|
||
3. `display.js` store 中 `Sender_variant` 是响应式 ref
|
||
4. `dialogBox/index.vue` 的 `autoSizeConfig` 计算属性读取 `Sender_variant`,返回 `{ minRows, maxRows }`
|
||
5. `Sender` 组件通过 `:key="useDisplay.Sender_variant"` **强制重挂载**(因为 Element Plus `ElInput` 不支持 `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` | 删除历史记录 |
|
||
|
||
### 任务响应格式
|
||
|
||
```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 前缀(由环境变量 `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 映射表。
|
||
|
||
### 环境变量速查
|
||
|
||
```bash
|
||
# .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` |
|
||
| Music | `ai_music_talk` |
|
||
|
||
映射函数 `getPlatformCode()` 位于 `utils/modelApi.js`。
|
||
|
||
### 自动导入
|
||
|
||
- `unplugin-auto-import`:自动导入 Vue/Router/Pinia API
|
||
- `unplugin-vue-components`:自动注册 `src/components/` 下的组件和 Element Plus 组件,**生成 `components.d.ts`(勿手动编辑)**
|
||
- Element Plus 图标通过 `unplugin-icons` 按需加载
|