feat: 输入框宽度62%、发送按钮改为圆形灰度切换、绘画平台默认选文生图首个模型
- 输入框从75%缩至62% - 发送按钮去掉文字改为44px正圆形,无提示词时浅灰底+深色箭头,有提示词时稍深灰底+白色箭头 - 绘画平台 getDefaultModel() 返回空字符串,modelSelector 加载后优先选 text tag 模型 - CLAUDE.md 新增 Display Store/Canvas/视图层架构章节,移除已删除的 model-configs 目录,修正环境变量表
This commit is contained in:
parent
5022404792
commit
1d07cdc907
62
CLAUDE.md
62
CLAUDE.md
@ -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 # 用户认证、信息(含 sessionId),pinia 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_` 开头的变量会被暴露给客户端代码。
|
||||
|
||||
### 平台编码映射
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -171,7 +171,7 @@ export function definePaintingPlatform() {
|
||||
},
|
||||
|
||||
getDefaultModel() {
|
||||
return 'Flux 2'
|
||||
return ''
|
||||
},
|
||||
|
||||
validateBeforeSubmit() {
|
||||
|
||||
@ -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 })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user