AI_Painting_V2.0/CLAUDE.md
WangLeo 1d07cdc907 feat: 输入框宽度62%、发送按钮改为圆形灰度切换、绘画平台默认选文生图首个模型
- 输入框从75%缩至62%
- 发送按钮去掉文字改为44px正圆形,无提示词时浅灰底+深色箭头,有提示词时稍深灰底+白色箭头
- 绘画平台 getDefaultModel() 返回空字符串,modelSelector 加载后优先选 text tag 模型
- CLAUDE.md 新增 Display Store/Canvas/视图层架构章节,移除已删除的 model-configs 目录,修正环境变量表
2026-06-17 18:22:30 +08:00

31 KiB
Raw Blame History

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-configVue 支持,无 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 是通用编排壳,不包含任何平台特定逻辑。

路由与认证

  • 路由表/ → 重定向 /home/login → 登录页(懒加载);/home/generate → 同一主页组件 views/home/index.vue
  • Token 来源:两种方式 — localStorageauth.ts 存取)或 URL query ?token=xxx(外部认证回调跳转)
  • 路由守卫router/index.js:32beforeEach 中先检查 URL query 是否有 token有则写入 localStorage无则检查 localStorage都没有 → 重定向 /login
  • 白名单/login 直接放行,不触发 token 校验
  • Token 校验:非白名单路径调 userStore.checkTokenValid()checkUsertokenApi() 验证,失败则跳转 /login
  • 用户信息token 有效且 userInfo.id 为空时自动调用 getInfo() 获取用户信息(含 sessionId)。userStore 使用 pinia persist 将 token/roles/permissions 等持久化到 localStorage

关键目录

├── 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          # 用户认证、信息(含 sessionIdpinia persist 持久化 token
│   ├── display.js       # 中央调度器:历史列表、画布状态、重新编辑/再次生成、输入框收缩
│   └── param.js         # 参数 store当前为空
├── apis/                # HTTP API 层:纯请求封装,不含业务逻辑
│   ├── auth/            # 认证相关登录、token 校验、用户信息、验证码)
│   └── display/         # 任务创建/轮询/历史、平台模型、收藏/删除
├── components/
│   ├── dialogBox/       # 通用编排壳(核心交互入口)
│   │   └── index.vue    # <component :is> 动态渲染平台控件,不含平台分支
│   ├── Popover/         # 自定义弹出层Teleport to bodyposition: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 实例 + 拦截器:统一 AuthBearer 格式)+ 按前缀路由 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() 工厂函数返回标准接口。markRaw() 标记组件使其不被 Vue 变为响应式代理 —— 控件组件只通过 props() 函数接收响应式绑定,组件本身不需要响应式追踪。

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) { ... },  // 从历史结果回填参数
  getGenerateDataExtras() { ... },     // (可选)返回平台专属字段,在 handleStart 中合并到 generateData用于回填和任务列表展示
}

控件通过 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 获取模型参数配置:

  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.bodygetModelId(type, modelName) 查找 UUID → POST /suanli/v1/tasks(请求体含 sessionId
  5. 20s 间隔轮询直至完成/失败

Display Store — 中央调度器

src/stores/display.jsuseDisplayStore)被 6 个文件引用,是展示层跨组件通信的中央枢纽:

  • 历史列表生命周期initHistoryList(首次加载)→ appendHistoryList(滚动加载更多)→ deleteHistoryItem
  • 生成状态追踪addGeneratingItem(生成中占位)→ updateItemToSuccess(完成时替换为结果 URLisSubGerenate 控制发送按钮 loading 态
  • 重新编辑/再次生成setResultData() 存储当前操作的历史数据 → fillParamsForEdit() 仅回填参数到 dialogBox → triggerGenerateWithResult() 回填后立即发起生成。二者通过 dialogBoxRef 跨组件调用 dialogBoxfillParamsFromResult() + handleStart()
  • Canvas 状态openCanvas(data) 统一入口,接收来自 Set 卡片或 ImageUploader 的数据,设置 canvasVisible/canvasImage/canvasReferenceImages/canvasSource 四个响应式状态
  • 滚动 → 输入框收缩scrollToBottom() 调用虚拟滚动 APISender_variant ref 在 display/index.vuehandleScroll 中被修改,在 dialogBoxautoSizeConfig 中被消费

Canvas 画布编辑架构

src/components/canvas/index.vue 是一个完整的图片编辑器(~600 行),用于局部重绘场景:

  • 选区绘制:圆形/矩形选区5 色(红/橙/绿/蓝/紫)自动轮换,按住拖拽绘制到 canvas 上
  • undo/redohistory 数组 + historyIndex 指针,每次操作(添加选区/删除/修改描述)保存快照并推进指针
  • 参考图选择:从 props 的 referenceImages 中多选参考图选区描述自动拼接为「将图1{颜色}{框/圈}内的【XXX】替换为【图{X}中的{描述}】」
  • 提示词组合:选区描述 + 笔刷 textarea 内容组合为完整 prompt通过 generate() 提交任务(modelType: 'edit'
  • 编辑模式限制:画笔生成的任务标记 modelType === 'edit',在 Set 卡片中禁止"重新编辑"和"再次生成"
  • 触发入口Set 卡片的画笔按钮、ImageUploader 的已上传图片点击

视图层

src/views/home/index.vue 是 thin shell同时挂载 dialogBox(输入编排)和 display(结果展示),通过 useDisplay.setDialogBoxRef() 建立两者之间的通信桥梁。

src/views/home/display/index.vue 负责结果展示区:

  • 首次加载onMountedfetchHistory() 拉取第 1 页 → 转换后 initHistoryList → 多次重试 scrollToBottom
  • 无限滚动handleScroll 监听 isAtPageTopfetchHistory(true)加载更早的历史3 秒防抖锁 isLoadingMoreLocked 防重复请求
  • 数据转换conversion() 将 API 响应适配为 UI 格式 — status 映射(queued/processinggeneratefailed/cancellederror)、outputs 扁平化为 files URL 数组
  • 筛选控件:时间段选择(全部/一周/一月/三月)+ 收藏筛选,通过 requestTaskHistoryuser_id/platform_code/status 参数过滤
  • 退出按钮:如果在 iframe 内通过 postMessage 通知父页面导航,否则 router.go(-1)

模型标识与查找

API 返回的模型对象包含三个标识字段:

字段 示例 用途
id e7e1e743-7621-403d-b8fb-2b7f4fa1b4fc UUID 主键,唯一且稳定,用于 API 调用和内部追踪
name vidu-text-to-video-q3-turbo 内部标识名,通常也唯一
display_name Vidu q3-turbo 用户可见的显示名,可能重复(不同 pattern 下的同名模型)

各平台 model.value 使用的标识类型不同:

平台 model.value 类型 原因
Painting display_name 历史兼容,getDefaultModel() 返回 '' 后由 modelSelector watcher 自动选第一个 text tag 模型
Video idUUID 避免 display_name 在不同 pattern 下重复导致查找歧义
Music idUUID 与 Video 一致,避免冲突

Video 平台使用 idUUID作为 model.value,避免 display_name 冲突导致的模型查找错误。modelSelector.vuemodelGroupsvalue 设为 m.idlabel 仍用 display_name 显示。

getModelId(type, modelName) 查找优先级:m.id === modelNamem.name === modelNamem.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 比例选择 Popoveroptionscustom 时可自定义宽高)
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/dimensionpromptPlaceholder 同步。resolution 同时匹配 ui:"resolution"ui:"select"
syncParamValues(config, state) buildTaskBody 前将专用 refproportion/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 中包含无专用控件的类型(如 selectswitch),由 ParamGroup 统一处理:

  • 容器定位:作为平台 controls 数组的最后一项,show() 检查是否存在未被 handledUis 覆盖的参数
  • handledUis['textarea', 'proportion', 'resolution', 'dimension', 'dimensionWidth', 'dimensionHeight', 'quantity', 'imageUpload', 'hidden', 'number'] — 这些类型的参数由专用控件处理ParamGroup 跳过
  • excludeNames prop:额外按参数名排除(如 resolutionduration),因为 VideoProportion/Time 已处理这些参数(即使其 ui 类型不在 handledUis 中)
  • 双层过滤:平台 show() 做第一层(判断是否渲染 ParamGroupParamGroup 内部 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']  // 第二层过滤
  })
}

Music 平台特有行为

Music 平台与 Painting/Video 的关键差异:

  • 模式驱动控件显隐mode ref常用模式/专业模式/Remix模式/编辑模式)决定大部分控件的 show()。切换模式时 visibleControls 自动更新。
  • 纯音乐/歌词互斥:常用模式下 pureMusicGroup 控件内含纯音乐开关 + 歌词输入弹窗。开关开启时清空歌词;关闭时弹出歌词输入框。buildTaskBodypureMusic 为 true 时 lyrics 为空字符串。
  • 参考音频imageUploader 用于上传音频文件(非图片),仅在专业模式下显示(showImageUploader()mode === '专业模式'),且为必填(isImageRequired() → true
  • 数量限制:专业模式下 quantity 强制为 1props() 中做 Math.min(mode === '专业模式' ? 1 : maxQty))。
  • Music 独有控件
控件 name beforeModel 作用
ModeSelector modeSelector true(在模型选择器前) 常用/专业/Remix/编辑 四选一
PureMusicGroup pureMusicGroup false 纯音乐开关 + 歌词输入(仅常用模式显示)
LyricsInput lyricsInput false 歌词输入(仅专业模式显示)
TimeControl timeControl false 时长滑块 min~max仅常用模式显示
  • handledUisMusic 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 等),图标除外。自定义组件使用项目自研的 SelectPopover 或纯 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 中,通过请求体 sessionId 字段传递给创建任务接口。taskPolling.js 必须使用该值,禁止随机生成。
  • 模型列表缓存modelApi.jsfetchPlatformModels 使用 localStorage 30 秒 TTL + pendingRequests Map 并发去重。
  • 模型配置缓存modelApi.jsgetModelConfig 使用 localStorage 60 秒 TTL + pendingConfigRequests Map 并发去重。loadModels() 会在获取模型列表后调用 preloadModelConfigs 批量预加载。
  • 平台包预加载dialogBox 顶层 import Painting 和 Video 平台包,触发自注册,确保首次使用时 registry 已就绪。
  • VirtualScroller 实现说明:虽然 package.json 安装了 vue-virtual-scrollernpm 包)并在 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 时读取 autosizeprop 更新时不会重新计算高度。因此 dialogBoxSender 必须绑定 :key="useDisplay.Sender_variant",通过强制重挂载来使新的 autosizeminRows/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]firstImageUrlreferenceImages[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 事件(含 isAtPageBottomdistanceToPageBottom 等)
  2. display/index.vuehandleScroll 根据滚动位置切换 useDisplay.Sender_variant
    • isAtPageBottom(底部)→ 'updown'展开5-9 行)
    • distanceToPageBottom >= 350(已滚离底部 350px'default'收缩1 行)
    • 中间状态 → 'updown'(展开)
  3. display.js store 中 Sender_variant 是响应式 ref
  4. dialogBox/index.vueautoSizeConfig 计算属性读取 Sender_variant,返回 { minRows, maxRows }
  5. Sender 组件通过 :key="useDisplay.Sender_variant" 强制重挂载(因为 Element Plus ElInput 不支持 autosize 动态更新),新实例以正确的行数初始化

滚动阈值 350px 对应 VirtualScrollerbottomPlaceholderHeight

接口速查

函数 端点 用途
requestCreateTask POST /suanli/v1/tasks 创建任务(请求体含 sessionId
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: Bearer <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(测试环境)和 .env.production(生产环境)中实际配置的变量:

# 地址前缀
VITE_BASE = '/'                            # 应用基础路径

# 主服务
VITE_API_PREFIX = '/api'                   # 主服务前缀
VITE_API_BASE_URL = 'http://...'           # 主服务(默认目标)

# 任务服务
VITE_API_TASK_PREFIX = '/suanli'           # 任务服务前缀
VITE_API_TASK_TARGET = 'http://...'        # 任务服务目标

# 图片上传
VITE_API_WORKFLOW_UPLOAD = 'http://...'    # 图片上传地址imageUploader 组件 action

# 其他
VITE_OPEN_DEVTOOLS = false                 # 是否开启开发者工具(仅 .env.development
FILE_OPEN_PREVIEW = true                   # 是否开启 KKFileView 预览

request.js 还引用了 VITE_API_PAY_PREFIX/VITE_API_PAY_TARGETVITE_API_AIGC_PREFIX/VITE_API_AIGC_TARGET,作为可选前缀路由扩展点——未配置时走默认 target当前两个 .env 文件中均未设置。

vite.config.jsenvPrefix: ['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 按需加载