feat: 输入框宽度62%、发送按钮改为圆形灰度切换、绘画平台默认选文生图首个模型

- 输入框从75%缩至62%
- 发送按钮去掉文字改为44px正圆形,无提示词时浅灰底+深色箭头,有提示词时稍深灰底+白色箭头
- 绘画平台 getDefaultModel() 返回空字符串,modelSelector 加载后优先选 text tag 模型
- CLAUDE.md 新增 Display Store/Canvas/视图层架构章节,移除已删除的 model-configs 目录,修正环境变量表
This commit is contained in:
王佑琳 2026-06-17 18:22:30 +08:00
parent 5022404792
commit 1d07cdc907
4 changed files with 73 additions and 30 deletions

View File

@ -34,7 +34,6 @@ AI 绘画/视频/音乐生成前端操作平台,通过 HTTP 接口对接算力
### 关键目录
```
├── 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
@ -74,7 +73,7 @@ src/
│ └── timeControl.vue # 时长滑块(常用模式)
├── stores/ # Pinia 状态管理
│ ├── user.js # 用户认证、信息(含 sessionIdpinia persist 持久化 token
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
│ ├── display.js # 中央调度器:历史列表、画布状态、重新编辑/再次生成、输入框收缩
│ └── param.js # 参数 store当前为空
├── apis/ # HTTP API 层:纯请求封装,不含业务逻辑
│ ├── auth/ # 认证相关登录、token 校验、用户信息、验证码)
@ -171,6 +170,39 @@ props: (config) => ({
4. `taskPolling.js` 直接读取 `data.body``getModelId(type, modelName)` 查找 UUID → POST `/suanli/v1/tasks`(请求体含 `sessionId`
5. 20s 间隔轮询直至完成/失败
### Display Store — 中央调度器
`src/stores/display.js``useDisplayStore`)被 6 个文件引用,是展示层跨组件通信的中央枢纽:
- **历史列表生命周期**`initHistoryList`(首次加载)→ `appendHistoryList`(滚动加载更多)→ `deleteHistoryItem`
- **生成状态追踪**`addGeneratingItem`(生成中占位)→ `updateItemToSuccess`(完成时替换为结果 URL`isSubGerenate` 控制发送按钮 loading 态
- **重新编辑/再次生成**`setResultData()` 存储当前操作的历史数据 → `fillParamsForEdit()` 仅回填参数到 dialogBox → `triggerGenerateWithResult()` 回填后立即发起生成。二者通过 `dialogBoxRef` 跨组件调用 `dialogBox``fillParamsFromResult()` + `handleStart()`
- **Canvas 状态**`openCanvas(data)` 统一入口,接收来自 Set 卡片或 ImageUploader 的数据,设置 `canvasVisible/canvasImage/canvasReferenceImages/canvasSource` 四个响应式状态
- **滚动 → 输入框收缩**`scrollToBottom()` 调用虚拟滚动 API`Sender_variant` ref 在 `display/index.vue``handleScroll` 中被修改,在 `dialogBox``autoSizeConfig` 中被消费
### Canvas 画布编辑架构
`src/components/canvas/index.vue` 是一个完整的图片编辑器(~600 行),用于局部重绘场景:
- **选区绘制**:圆形/矩形选区5 色(红/橙/绿/蓝/紫)自动轮换,按住拖拽绘制到 canvas 上
- **undo/redo**`history` 数组 + `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` 负责结果展示区:
- **首次加载**`onMounted` → `fetchHistory()` 拉取第 1 页 → 转换后 `initHistoryList` → 多次重试 `scrollToBottom`
- **无限滚动**`handleScroll` 监听 `isAtPageTop``fetchHistory(true)`加载更早的历史3 秒防抖锁 `isLoadingMoreLocked` 防重复请求
- **数据转换**`conversion()` 将 API 响应适配为 UI 格式 — status 映射(`queued/processing`→`generate``failed/cancelled`→`error`)、`outputs` 扁平化为 `files` URL 数组
- **筛选控件**:时间段选择(全部/一周/一月/三月)+ 收藏筛选,通过 `requestTaskHistory``user_id`/`platform_code`/`status` 参数过滤
- **退出按钮**:如果在 iframe 内通过 `postMessage` 通知父页面导航,否则 `router.go(-1)`
### 模型标识与查找
API 返回的模型对象包含三个标识字段:
@ -181,6 +213,14 @@ 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 | `id`UUID | 避免 `display_name` 在不同 pattern 下重复导致查找歧义 |
| Music | `id`UUID | 与 Video 一致,避免冲突 |
**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 调用。
@ -384,20 +424,30 @@ Music 平台与 Painting/Video 的关键差异:
### 环境变量速查
`.env.development`(测试环境)和 `.env.production`(生产环境)中实际配置的变量:
```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 # 是否开启开发者工具
# 其他
VITE_OPEN_DEVTOOLS = false # 是否开启开发者工具(仅 .env.development
FILE_OPEN_PREVIEW = true # 是否开启 KKFileView 预览
```
`request.js` 还引用了 `VITE_API_PAY_PREFIX`/`VITE_API_PAY_TARGET` 和 `VITE_API_AIGC_PREFIX`/`VITE_API_AIGC_TARGET`,作为**可选**前缀路由扩展点——未配置时走默认 target当前两个 .env 文件中均未设置。
`vite.config.js``envPrefix: ['VITE', 'FILE']`,因此只有以 `VITE_``FILE_` 开头的变量会被暴露给客户端代码。
### 平台编码映射

View File

@ -60,10 +60,9 @@
<el-button v-if="isgerenate" round color="#626aef">
<i-ep-loading style="animation: spin 1s linear infinite;" />
</el-button>
<div v-else class="gerenate" :class="{ isprompt: prompt }" @click="handleStart">
<img v-if="!prompt" src="@/assets/dialog/darkArrow.svg" alt="">
<div v-else class="gerenate" :class="{ isprompt: !prompt }" @click="handleStart">
<img v-if="prompt" src="@/assets/dialog/darkArrow.svg" alt="">
<img v-else src="@/assets/dialog/writerArrow.svg" alt="">
<div v-show="useDisplay.Sender_variant !== 'default'">发送</div>
</div>
</div>
</template>
@ -213,7 +212,7 @@ watch(() => props.type, (newType) => {
<style lang="less" scoped>
.input-container {
width: 75%;
width: 62%;
min-width: 830px;
max-width: 100%;
position: absolute;
@ -381,25 +380,18 @@ watch(() => props.type, (newType) => {
.gerenate {
display: inline-flex;
height: 40px;
padding: 0 20px;
width: 44px;
height: 44px;
padding: 0;
justify-content: center;
align-items: center;
gap: 5px;
border-radius: 10px;
background: rgba(0, 15, 51, 0.10);
border-radius: 50%;
background: rgba(0, 15, 51, 0.08);
cursor: pointer;
color: #000F33;
text-align: center;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: normal;
transition: background 0.2s ease;
}
.isprompt {
color: #ffffff;
background-color: #000F33;
background: rgba(0, 15, 51, 0.20);
}
</style>

View File

@ -171,7 +171,7 @@ export function definePaintingPlatform() {
},
getDefaultModel() {
return 'Flux 2'
return ''
},
validateBeforeSubmit() {

View File

@ -102,15 +102,16 @@ const modelGroups = computed(() => {
}))
})
//
// text tag
watch(platformModels, (models) => {
if (models.length === 0) return
const currentModel = models.find((m) => (m.display_name || m.name) === props.modelValue || m.id === props.modelValue)
if (!currentModel || currentModel.disabled) {
const firstEnabled = models.find((m) => !m.disabled)
if (firstEnabled) {
emit('update:modelValue', firstEnabled.display_name || firstEnabled.name)
emit('update:typeValue', tagToInputType(findTagForModel(firstEnabled.display_name || firstEnabled.name)))
const firstText = models.find((m) => !m.disabled && m.tags?.includes('text'))
const fallback = firstText || models.find((m) => !m.disabled)
if (fallback) {
emit('update:modelValue', fallback.display_name || fallback.name)
emit('update:typeValue', tagToInputType(findTagForModel(fallback.display_name || fallback.name)))
}
}
}, { immediate: true })