Compare commits
8 Commits
6e67acca66
...
16d1496283
| Author | SHA1 | Date | |
|---|---|---|---|
| 16d1496283 | |||
| f0008aedde | |||
| 4f7357eefc | |||
| 239b32fb95 | |||
| 791c56a46b | |||
| 2b1e7385e0 | |||
| 5da5496492 | |||
| 72267ab2c9 |
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git add *)",
|
||||||
|
"Bash(git commit *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,8 +3,7 @@ VITE_BASE = '/'
|
|||||||
|
|
||||||
# 主服务
|
# 主服务
|
||||||
VITE_API_PREFIX = '/api'
|
VITE_API_PREFIX = '/api'
|
||||||
VITE_API_BASE_URL = 'http://test.xueai.art/api' # http://huanda.xueai.art http://106.54.11.219/api 43.248.131.153:8003
|
VITE_API_BASE_URL = 'http://test.xueai.art/newapi/api' # http://huanda.xueai.art http://106.54.11.219/api 43.248.131.153:8003
|
||||||
VITE_API_WS_URL = 'ws://test.xueai.art/api'
|
|
||||||
|
|
||||||
# 支付服务
|
# 支付服务
|
||||||
VITE_API_PAY_PREFIX = '/pay'
|
VITE_API_PAY_PREFIX = '/pay'
|
||||||
@ -12,7 +11,8 @@ VITE_API_PAY_TARGET = 'http://test.xueai.art' # http://43.248.133.202 test.xue
|
|||||||
|
|
||||||
# 任务处理模块
|
# 任务处理模块
|
||||||
VITE_API_WORKFLOW_UPLOAD = 'http://43.248.97.19:4000/aigc/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
|
VITE_API_WORKFLOW_UPLOAD = 'http://43.248.97.19:4000/aigc/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
|
||||||
VITE_API_WORKFLOW_WS = 'ws://43.248.97.19:4000/testworkflow'
|
VITE_API_TASK_PREFIX = '/suanli'
|
||||||
|
VITE_API_TASK_TARGET = 'http://test.xueai.art'
|
||||||
|
|
||||||
# 是否开启开发者工具
|
# 是否开启开发者工具
|
||||||
VITE_OPEN_DEVTOOLS = false
|
VITE_OPEN_DEVTOOLS = false
|
||||||
|
|||||||
@ -7,7 +7,6 @@ VITE_BUILD_MOCK = false
|
|||||||
# 主服务
|
# 主服务
|
||||||
VITE_API_PREFIX = '/api'
|
VITE_API_PREFIX = '/api'
|
||||||
VITE_API_BASE_URL = 'https://sxwz.xueai.art/api'
|
VITE_API_BASE_URL = 'https://sxwz.xueai.art/api'
|
||||||
VITE_API_WS_URL = 'wss://sxwz.xueai.art/api'
|
|
||||||
|
|
||||||
# 支付服务
|
# 支付服务
|
||||||
VITE_API_PAY_PREFIX = '/pay'
|
VITE_API_PAY_PREFIX = '/pay'
|
||||||
@ -15,7 +14,8 @@ VITE_API_PAY_TARGET = 'https://sxwz.xueai.art' # http://43.248.133.202
|
|||||||
|
|
||||||
# 任务处理模块
|
# 任务处理模块
|
||||||
VITE_API_WORKFLOW_UPLOAD = 'https://designtools.xueai.art/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
|
VITE_API_WORKFLOW_UPLOAD = 'https://designtools.xueai.art/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
|
||||||
VITE_API_WORKFLOW_WS = 'wss://talkingdraw.xueai.art/testworkflow'
|
VITE_API_TASK_PREFIX = '/suanli'
|
||||||
|
VITE_API_TASK_TARGET = 'http://test.xueai.art'
|
||||||
|
|
||||||
# 模型资源
|
# 模型资源
|
||||||
VITE_API_MODEL_RESOURCE = 'https://resources.xueai.art/AIGC'
|
VITE_API_MODEL_RESOURCE = 'https://resources.xueai.art/AIGC'
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,3 +22,4 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
TEST/
|
||||||
|
|||||||
168
CLAUDE.md
168
CLAUDE.md
@ -8,15 +8,21 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
pnpm dev # 启动 Vite 开发服务器
|
pnpm dev # 启动 Vite 开发服务器
|
||||||
pnpm build # 生产构建
|
pnpm build # 生产构建
|
||||||
pnpm preview # 预览生产构建
|
pnpm preview # 预览生产构建
|
||||||
|
npx eslint . # 代码检查(@antfu/eslint-config,Vue 支持,无 TypeScript)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + Less + pnpm
|
Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + `vue-element-plus-x`(多媒体编辑) + Less + pnpm
|
||||||
|
|
||||||
## 架构概览
|
## 架构概览
|
||||||
|
|
||||||
这是一个 AI 绘画/视频生成的前端操作平台,通过 WebSocket 连接后端和第三方 AI 平台(RunningHub)提交生成任务并接收结果。
|
AI 绘画/视频生成前端操作平台,通过 HTTP 接口对接算力调度后端(suanli)和第三方 AI 平台(RunningHub),提交生成任务并轮询结果。
|
||||||
|
|
||||||
|
**Painting 和 Video 走两套不同的任务构造路径:**
|
||||||
|
|
||||||
|
- **Painting(新架构)**:本地模型参数 schema → 专用控件 + 动态表单 → `X-Session-Id` header + 扁平 API body
|
||||||
|
- **Video(旧架构)**:远程 workflow JSON → RunningHub Playload 适配器 → `{ workflowId, nodeInfoList }` body
|
||||||
|
|
||||||
### 关键目录
|
### 关键目录
|
||||||
|
|
||||||
@ -25,54 +31,132 @@ src/
|
|||||||
├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/VueVirtualScroller
|
├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/VueVirtualScroller
|
||||||
├── router/index.js # 路由定义 + token 验证守卫
|
├── router/index.js # 路由定义 + token 验证守卫
|
||||||
├── stores/ # Pinia 状态管理
|
├── stores/ # Pinia 状态管理
|
||||||
│ ├── user.js # 用户认证、信息、免费次数
|
│ ├── user.js # 用户认证、信息(含 sessionId)
|
||||||
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
|
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
|
||||||
│ └── param.js # 占位 store(当前为空)
|
│ └── param.js # 参数 store(当前为空)
|
||||||
├── apis/ # HTTP API 模块
|
├── apis/ # HTTP API 层:纯请求封装,不含业务逻辑
|
||||||
│ ├── auth/ # 登录/登出/用户信息/验证码
|
│ ├── auth/ # 认证相关(登录、token 校验、用户信息)
|
||||||
│ └── display/ # 获取历史列表/收藏/删除
|
│ └── display/ # 任务创建/轮询/历史、平台模型、收藏/删除
|
||||||
├── components/ # 通用组件
|
├── components/
|
||||||
│ ├── dialogBox/ # 生成参数输入面板(核心交互入口),含模型选择、比例、上传等子组件
|
│ ├── dialogBox/ # 生成参数输入面板(核心交互入口)
|
||||||
│ ├── virtual-scroller/# 虚拟滚动列表组件(自定义实现)
|
│ │ ├── model/ # 模型选择器(按 API 返回的 tags 分组,value 编码为 tag::display_name)
|
||||||
│ ├── canvas/ # 图片画布编辑(圆/矩形选区,局部重绘)
|
│ │ ├── proportion/ # 比例/分辨率选择器(painting.vue 用于 Painting,video.vue 用于 Video)
|
||||||
│ └── ...
|
│ │ ├── imageUploader/ # 图片上传(Painting)
|
||||||
├── views/ # 页面
|
│ │ ├── videoImageUploader/ # 视频图片上传(Video)
|
||||||
│ ├── home/index.vue # 主页面容器(dialogBox + display)
|
│ │ ├── quantity/ # 生成数量选择器(支持 1-6)
|
||||||
│ ├── home/display/ # 历史记录展示区
|
│ │ ├── Time/ # 视频时长选择器
|
||||||
│ └── login/ # 登录页(跳转外部登录)
|
│ │ └── pattern/ # 视频模式选择器
|
||||||
|
│ ├── virtual-scroller/# 虚拟滚动列表组件(自定义实现,reverse 模式)
|
||||||
|
│ └── canvas/ # 图片画布编辑(圆/矩形选区,局部重绘)
|
||||||
|
├── views/ # 页面(home、login)
|
||||||
├── utils/
|
├── utils/
|
||||||
│ ├── request.js # Axios 实例,拦截器处理 token 和不同服务的 baseURL 路由
|
│ ├── request.js # Axios 实例 + 拦截器:统一 Auth(不带 Bearer)+ 按前缀路由 baseURL
|
||||||
│ ├── websocket.js # WebSocket 生成任务的核心流程(心跳、提交流程、结果处理)
|
│ ├── websocket.js # 任务生成入口:组装参数 → POST 创建任务 → 20s 轮询直至完成/失败
|
||||||
│ ├── createTask.js # 根据配置构造任务 payload
|
│ ├── modelApi.js # 模型业务层:localStorage 30s 缓存 + pendingRequests 并发去重 + 平台编码映射
|
||||||
│ ├── modelConfig.js # 从远程 JSON 加载模型配置,localStorage 每日缓存
|
│ ├── createTask.js # 任务 body 构造:Painting 返回 modelParams,Video 走 Playload 适配器
|
||||||
│ ├── auth.ts # token 存取工具(localStorage)
|
│ ├── modelConfig.js # Video 旧架构:从远程 JSON 加载 workflow 配置
|
||||||
│ └── encrypt.ts # 加密工具(Base64/MD5/RSA/AES)
|
│ └── auth.ts # token 存取工具(localStorage)
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── index.js # 平台配置入口
|
│ ├── plugins.js # Vite 插件配置(unplugin-auto-import + unplugin-vue-components)
|
||||||
│ └── runninghub/ # RunningHub 平台适配器:Payload 构造和 Result 解析
|
│ ├── index.js # 平台配置入口(目前仅导出 runninghub 供 Video 使用)
|
||||||
└── config/ # 项目根目录下
|
│ ├── runninghub/ # RunningHub 平台适配器:Playload() 构造和 result() 解析(Video 专用)
|
||||||
└── plugins.js # Vite 插件配置(自动导入/组件注册/图标)
|
│ └── models/ # Painting 模型参数配置:每模型一个 JS 文件,定义 params schema
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 模型参数配置(Painting 新架构)
|
||||||
|
|
||||||
|
`src/config/models/` 下每个模型一个 JS 文件,参数通过不同 UI 组件承载:
|
||||||
|
|
||||||
|
- **`ui: 'textarea'`** → Sender 组件主输入框(prompt)
|
||||||
|
- **`ui: 'proportion'`** + **`ui: 'resolution'`** → `paintingProportion` 组件(共用 Popover,含自定义 W/H)
|
||||||
|
- **`ui: 'quantity'`** → `Quantity` 组件(动态 1-6 张)
|
||||||
|
- **`ui: 'imageUpload'`** → `ImageUploader` 组件
|
||||||
|
- **`ui: 'hidden'`** → 无 UI,仅写入默认值(如 outputFormat: 'png')
|
||||||
|
|
||||||
|
模型选择器从 API(`fetchPlatformModels`)获取模型列表,按 API 返回的 `tags` 数组字段分组(`text`→生成模型,`edit`→编辑模型,`vision`→视觉理解模型)。
|
||||||
|
|
||||||
|
### `displayNameMap` 机制
|
||||||
|
|
||||||
|
`src/config/models/index.js` 中 `displayNameMap` 负责将 API 返回的 `display_name` 映射到 config key。因为同一模型在不同 tag 下可能共用一个 `display_name`(如 `GPT-Image-2` 和 `GPT-image-2` 分别对应编辑/生成),config key 采用内部中文名区分。
|
||||||
|
|
||||||
|
### API 层设计原则
|
||||||
|
|
||||||
|
- `src/apis/` 只做纯 HTTP 调用(`service.get/post/delete`),不含缓存、localStorage、业务逻辑
|
||||||
|
- 缓存、数据转换等业务逻辑放在 `src/utils/` 中
|
||||||
|
|
||||||
### 核心数据流
|
### 核心数据流
|
||||||
|
|
||||||
1. 用户在 `dialogBox` 中设置参数(模型、提示词、比例、上传图片等)
|
**Painting(新架构):**
|
||||||
2. 点击生成 → `websocket.js:generate()` 被调用
|
|
||||||
3. 先通过 `createTask.js` 调用 `config/runninghub` 的 `Playload()` 构造任务数据(从远程 JSON 加载 workflow 配置)
|
1. 用户设置参数 → 模型选择器按 `tags` 分组,控件根据 model config 的 `ui` 字段渲染
|
||||||
4. 建立 WebSocket 连接,经过握手协议(`please give me taskId` → `OK! Please continue.`)提交任务
|
2. `handleStart()` 收集 `paramValues`(UI refs 通过 watcher 双向同步)→ 组装 `{ modelParams, request }`
|
||||||
5. 任务排队中 → `displayStore.addGeneratingItem()` 在前端列表中插入 "生成中" 条目
|
3. `websocket.js:generate()` → `createTask(data)` → Painting 直接返回 `data.modelParams`
|
||||||
6. 完成后 WebSocket 关闭(code=1000 reason=success) → `getTask()` 解析结果 URL → `updateItemToSuccess()` 更新列表
|
4. `getModelId(type, modelName)` 查找 UUID(内部调用 `fetchPlatformModels` 走缓存)
|
||||||
|
5. `requestCreateTask(body, sessionId)` → POST `/suanli/v1/tasks`,携带 `X-Session-Id` header
|
||||||
|
6. 返回 task_id → 20s 间隔轮询直至完成/失败
|
||||||
|
|
||||||
|
**Video(旧架构,保留):**
|
||||||
|
|
||||||
|
1. 用户设置 Pattern、videoModel、比例、时长
|
||||||
|
2. `createTask(data)` → `runninghub.Playload(data)` → `fetchModelConfig()` 获取 workflow JSON → 返回 `{ workflowId, nodeInfoList }`
|
||||||
|
3. 后续同 Painting 步骤 4-6
|
||||||
|
|
||||||
|
### 关键注意事项
|
||||||
|
|
||||||
|
- **`sessionId`** 来自登录接口返回的 `userInfo.sessionId`,存储在 `useUserStore().userInfo` 中。`websocket.js` 必须使用该值,禁止随机生成。
|
||||||
|
- **`X-Session-Id`** 自定义 header 需要 nginx 在 `/suanli/` location 的 `Access-Control-Allow-Headers` 中加入,否则 POST 请求会触发 CORS 预检失败。
|
||||||
|
- **模型列表缓存**:`modelApi.js` 中 `fetchPlatformModels` 使用 localStorage 30 秒 TTL + `pendingRequests` Map 并发去重,避免重复请求。
|
||||||
|
|
||||||
|
### 接口速查
|
||||||
|
|
||||||
|
| 函数 | 端点 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `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, display_name, tags, disabled?}`) |
|
||||||
|
| `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 前缀切换后端:
|
||||||
|
|
||||||
|
| URL 前缀 | 环境变量 |
|
||||||
|
|----------|----------|
|
||||||
|
| `/suanli` | `VITE_API_TASK_TARGET` |
|
||||||
|
| `/pay` | `VITE_API_PAY_TARGET` |
|
||||||
|
| `/aigc` | `VITE_API_AIGC_TARGET` |
|
||||||
|
| 其他 | `VITE_API_BASE_URL`(默认) |
|
||||||
|
|
||||||
|
### 平台编码映射
|
||||||
|
|
||||||
|
| 类型 | 平台编码 |
|
||||||
|
|------|----------|
|
||||||
|
| Painting | `ai_painting_talk` |
|
||||||
|
| Video | `ai_video_talk` |
|
||||||
|
|
||||||
|
映射函数 `getPlatformCode()` 位于 `utils/modelApi.js`。
|
||||||
|
|
||||||
### 自动导入
|
### 自动导入
|
||||||
|
|
||||||
- `unplugin-auto-import` 自动导入 Vue/VRouter/Pinia API,无需在 `.vue` 文件中手动 `import { ref, computed, watch } from 'vue'`
|
- `unplugin-auto-import`:自动导入 Vue/Router/Pinia API
|
||||||
- `unplugin-vue-components` 自动注册 `src/components/` 下的组件和 Element Plus 组件
|
- `unplugin-vue-components`:自动注册 `src/components/` 下的组件和 Element Plus 组件
|
||||||
- Element Plus 图标通过 `unplugin-icons` 按需加载
|
- Element Plus 图标通过 `unplugin-icons` 按需加载
|
||||||
|
|
||||||
### 环境变量
|
|
||||||
|
|
||||||
有两套环境文件。`VITE_API_BASE_URL` 定义主 API 地址,请求拦截器根据 URL 前缀自动切换不同的后端服务(主服务/支付服务/AIGC 工作流服务)。`VITE_API_WORKFLOW_WS` 定义 WebSocket 地址。
|
|
||||||
|
|
||||||
### 路由守卫
|
|
||||||
|
|
||||||
`src/router/index.js` 的 `beforeEach` 守卫检查 token 存在性和有效性(调用 `/auth/check/token`),无效则跳转 `/login`。支持通过 URL query `?token=xxx` 传入 token。
|
|
||||||
|
|||||||
9
components.d.ts
vendored
9
components.d.ts
vendored
@ -11,10 +11,18 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
2: typeof import('./src/components/virtual-scroller/VirtualScroller copy 2.vue')['default']
|
||||||
|
3: typeof import('./src/components/virtual-scroller/VirtualScroller copy 3.vue')['default']
|
||||||
Canvas: typeof import('./src/components/canvas/index.vue')['default']
|
Canvas: typeof import('./src/components/canvas/index.vue')['default']
|
||||||
|
copy: typeof import('./src/components/virtual-scroller/VirtualScroller copy.vue')['default']
|
||||||
DialogBox: typeof import('./src/components/dialogBox/index.vue')['default']
|
DialogBox: typeof import('./src/components/dialogBox/index.vue')['default']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
IEpCalendar: typeof import('~icons/ep/calendar')['default']
|
IEpCalendar: typeof import('~icons/ep/calendar')['default']
|
||||||
@ -26,6 +34,7 @@ declare module 'vue' {
|
|||||||
ImageUploader: typeof import('./src/components/dialogBox/imageUploader/index.vue')['default']
|
ImageUploader: typeof import('./src/components/dialogBox/imageUploader/index.vue')['default']
|
||||||
Img: typeof import('./src/components/Img/index.vue')['default']
|
Img: typeof import('./src/components/Img/index.vue')['default']
|
||||||
Painting: typeof import('./src/components/dialogBox/model/painting.vue')['default']
|
Painting: typeof import('./src/components/dialogBox/model/painting.vue')['default']
|
||||||
|
ParamControl: typeof import('./src/components/dialogBox/ParamControl.vue')['default']
|
||||||
Pattern: typeof import('./src/components/dialogBox/pattern/index.vue')['default']
|
Pattern: typeof import('./src/components/dialogBox/pattern/index.vue')['default']
|
||||||
Popover: typeof import('./src/components/Popover/index.vue')['default']
|
Popover: typeof import('./src/components/Popover/index.vue')['default']
|
||||||
Quantity: typeof import('./src/components/dialogBox/quantity/index.vue')['default']
|
Quantity: typeof import('./src/components/dialogBox/quantity/index.vue')['default']
|
||||||
|
|||||||
@ -38,14 +38,10 @@ export function logout() {
|
|||||||
|
|
||||||
/** @desc 获取用户信息 */
|
/** @desc 获取用户信息 */
|
||||||
export const getUserInfo = () => {
|
export const getUserInfo = () => {
|
||||||
return service.get(`${BASE_URL}/user/info`)
|
return service.get(`/sysUser/currentUser
|
||||||
}
|
`)
|
||||||
|
|
||||||
/** @desc 获取路由信息 */
|
|
||||||
export const getUserRoute = () => {
|
|
||||||
return service.get(`${BASE_URL}/route`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkUsertoken = () => {
|
export const checkUsertoken = () => {
|
||||||
return service.get(`${BASE_URL}/check/token`)
|
return service.post(`/login/validateToken`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import service from '@/utils/request'
|
import service from '@/utils/request'
|
||||||
|
|
||||||
// 获取生成历史列表
|
// ==================== 历史记录 API(axios) ====================
|
||||||
export function getGenerateHistoryList(query) {
|
|
||||||
return service.get('/taskRecordHistory', { params: query })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消或收藏
|
// 取消或收藏
|
||||||
export function cancelOrCollect(query) {
|
export function cancelOrCollect(query) {
|
||||||
@ -15,7 +12,28 @@ export function deleteGenerateHistory(query) {
|
|||||||
return service.delete('/taskRecordHistory/delete', { params: query })
|
return service.delete('/taskRecordHistory/delete', { params: query })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取免费次数
|
// ==================== 任务 API(axios,经由 /suanli 前缀路由到算力调度后端) ====================
|
||||||
export function getFreeTimes(id) {
|
|
||||||
return service.get('/plantformBalance/userBalances', { params: { id } })
|
// 创建生成任务(HTTP POST /suanli/v1/tasks)
|
||||||
}
|
export function requestCreateTask(body, sessionId) {
|
||||||
|
return service.post('/suanli/v1/tasks', body, {
|
||||||
|
headers: { 'X-Session-Id': sessionId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询任务状态 / 获取历史任务结果(HTTP GET /suanli/v1/tasks/:id)
|
||||||
|
export function requestTaskStatus(taskId) {
|
||||||
|
return service.get(`/suanli/v1/tasks/${taskId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取历史任务列表(HTTP GET /suanli/v1/tasks/history,支持平台筛选和分页)
|
||||||
|
export function requestTaskHistory(params) {
|
||||||
|
return service.get('/suanli/v1/tasks/history', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 平台模型 API ====================
|
||||||
|
|
||||||
|
// 获取平台模型列表(原始 HTTP 调用,不含缓存逻辑)
|
||||||
|
export function fetchPlatformModels(code) {
|
||||||
|
return service.get(`/suanli/v1/platforms/${code}/models`)
|
||||||
|
}
|
||||||
|
|||||||
@ -47,20 +47,27 @@ const contentRef = ref(null)
|
|||||||
const visible = ref(props.modelValue)
|
const visible = ref(props.modelValue)
|
||||||
const position = ref({ top: 0, left: 0 })
|
const position = ref({ top: 0, left: 0 })
|
||||||
const popoverId = ref(Math.random().toString(36).substr(2, 9))
|
const popoverId = ref(Math.random().toString(36).substr(2, 9))
|
||||||
|
let resizeObserver = null
|
||||||
|
|
||||||
if (!window.__currentOpenPopoverId__) {
|
if (!window.__currentOpenPopoverId__) {
|
||||||
window.__currentOpenPopoverId__ = null
|
window.__currentOpenPopoverId__ = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentStyle = computed(() => ({
|
const contentStyle = computed(() => {
|
||||||
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
|
const w = typeof props.width === 'number' ? `${props.width}px` : props.width
|
||||||
...position.value
|
return {
|
||||||
}))
|
...position.value,
|
||||||
|
width: w,
|
||||||
|
maxWidth: w === 'auto' ? 'none' : w,
|
||||||
|
minWidth: w === 'auto' ? '0' : w
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const togglePopover = async () => {
|
const togglePopover = async () => {
|
||||||
if (visible.value) {
|
if (visible.value) {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
window.__currentOpenPopoverId__ = null
|
window.__currentOpenPopoverId__ = null
|
||||||
|
stopResizeObserver()
|
||||||
} else {
|
} else {
|
||||||
if (window.__currentOpenPopoverId__ && window.__currentOpenPopoverId__ !== popoverId.value) {
|
if (window.__currentOpenPopoverId__ && window.__currentOpenPopoverId__ !== popoverId.value) {
|
||||||
window.dispatchEvent(new CustomEvent('close-other-popovers', { detail: { excludeId: popoverId.value } }))
|
window.dispatchEvent(new CustomEvent('close-other-popovers', { detail: { excludeId: popoverId.value } }))
|
||||||
@ -72,20 +79,21 @@ const togglePopover = async () => {
|
|||||||
window.__currentOpenPopoverId__ = popoverId.value
|
window.__currentOpenPopoverId__ = popoverId.value
|
||||||
await nextTick()
|
await nextTick()
|
||||||
updatePosition()
|
updatePosition()
|
||||||
|
startResizeObserver()
|
||||||
}
|
}
|
||||||
emit('update:modelValue', visible.value)
|
emit('update:modelValue', visible.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
if (!triggerRef.value || !contentRef.value) return
|
if (!triggerRef.value || !contentRef.value) return
|
||||||
|
|
||||||
const triggerRect = triggerRef.value.getBoundingClientRect()
|
const triggerRect = triggerRef.value.getBoundingClientRect()
|
||||||
const contentRect = contentRef.value.getBoundingClientRect()
|
const contentRect = contentRef.value.getBoundingClientRect()
|
||||||
const gap = 25
|
const gap = 25
|
||||||
|
|
||||||
let top = 0
|
let top = 0
|
||||||
let left = 0
|
let left = 0
|
||||||
|
|
||||||
switch (props.placement) {
|
switch (props.placement) {
|
||||||
case 'top':
|
case 'top':
|
||||||
top = triggerRect.top - contentRect.height - gap
|
top = triggerRect.top - contentRect.height - gap
|
||||||
@ -104,19 +112,35 @@ const updatePosition = () => {
|
|||||||
left = triggerRect.right + gap
|
left = triggerRect.right + gap
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
position.value = {
|
position.value = {
|
||||||
top: `${top}px`,
|
top: `${top}px`,
|
||||||
left: `${left}px`
|
left: `${left}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startResizeObserver = () => {
|
||||||
|
if (!contentRef.value) return
|
||||||
|
stopResizeObserver()
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
updatePosition()
|
||||||
|
})
|
||||||
|
resizeObserver.observe(contentRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopResizeObserver = () => {
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
resizeObserver = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleClickOutside = (e) => {
|
const handleClickOutside = (e) => {
|
||||||
if (!visible.value) return
|
if (!visible.value) return
|
||||||
|
|
||||||
const triggerEl = popoverRef.value
|
const triggerEl = popoverRef.value
|
||||||
const contentEl = contentRef.value
|
const contentEl = contentRef.value
|
||||||
|
|
||||||
if (
|
if (
|
||||||
triggerEl &&
|
triggerEl &&
|
||||||
!triggerEl.contains(e.target) &&
|
!triggerEl.contains(e.target) &&
|
||||||
@ -125,6 +149,7 @@ const handleClickOutside = (e) => {
|
|||||||
) {
|
) {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
window.__currentOpenPopoverId__ = null
|
window.__currentOpenPopoverId__ = null
|
||||||
|
stopResizeObserver()
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,12 +157,14 @@ const handleClickOutside = (e) => {
|
|||||||
const handleCloseOtherPopovers = (e) => {
|
const handleCloseOtherPopovers = (e) => {
|
||||||
if (e.detail.excludeId !== popoverId.value) {
|
if (e.detail.excludeId !== popoverId.value) {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
|
stopResizeObserver()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseOtherSelects = () => {
|
const handleCloseOtherSelects = () => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
window.__currentOpenPopoverId__ = null
|
window.__currentOpenPopoverId__ = null
|
||||||
|
stopResizeObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.modelValue, async (val) => {
|
watch(() => props.modelValue, async (val) => {
|
||||||
@ -145,6 +172,9 @@ watch(() => props.modelValue, async (val) => {
|
|||||||
if (val) {
|
if (val) {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
updatePosition()
|
updatePosition()
|
||||||
|
startResizeObserver()
|
||||||
|
} else {
|
||||||
|
stopResizeObserver()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -162,6 +192,7 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('scroll', updatePosition, true)
|
window.removeEventListener('scroll', updatePosition, true)
|
||||||
window.removeEventListener('close-other-popovers', handleCloseOtherPopovers)
|
window.removeEventListener('close-other-popovers', handleCloseOtherPopovers)
|
||||||
window.removeEventListener('close-other-selects', handleCloseOtherSelects)
|
window.removeEventListener('close-other-selects', handleCloseOtherSelects)
|
||||||
|
stopResizeObserver()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -261,6 +261,8 @@ onBeforeUnmount(() => {
|
|||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
animation: fadeIn 0.2s ease;
|
animation: fadeIn 0.2s ease;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
|
|||||||
@ -161,8 +161,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { generate } from '@/utils/websocket'
|
import { generate } from '@/utils/websocket'
|
||||||
import { useDisplayStore, useUserStore } from '@/stores'
|
import { useDisplayStore } from '@/stores'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import { getModelId } from '@/utils/modelApi'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: {
|
visible: {
|
||||||
@ -725,6 +726,8 @@ const handleSend = async () => {
|
|||||||
|
|
||||||
const proportion = getImageAspectRatio()
|
const proportion = getImageAspectRatio()
|
||||||
|
|
||||||
|
const modelId = await getModelId(props.type, 'GPT')
|
||||||
|
|
||||||
const generateData = {
|
const generateData = {
|
||||||
model: 'GPT-image2.0',
|
model: 'GPT-image2.0',
|
||||||
modelType: 'edit',
|
modelType: 'edit',
|
||||||
@ -737,15 +740,15 @@ const handleSend = async () => {
|
|||||||
AIGC: 'Painting',
|
AIGC: 'Painting',
|
||||||
platform: 'runninghub',
|
platform: 'runninghub',
|
||||||
modelName: 'GPT',
|
modelName: 'GPT',
|
||||||
|
modelId,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
free: useUserStore().freeTimes,
|
|
||||||
params: [
|
params: [
|
||||||
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' },
|
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' },
|
||||||
{ name: 'index', data: 1 },
|
{ name: 'index', data: 1 },
|
||||||
{ name: 'proportion', data: proportion?.aspectRatio || '4:3' },
|
{ name: 'proportion', data: proportion?.aspectRatio || '4:3' },
|
||||||
],
|
],
|
||||||
imgs: uploadedImgs,
|
imgs: uploadedImgs,
|
||||||
result: JSON.stringify(generateData)
|
request: JSON.stringify(generateData)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('send', {
|
emit('send', {
|
||||||
|
|||||||
@ -7,14 +7,14 @@
|
|||||||
<div class="sender-top">
|
<div class="sender-top">
|
||||||
<div v-if="useDisplay.Sender_variant === 'default'" class="scroll-to-bottom-text" @click.stop="handleScrollToBottom">回到底部<img src="@/assets/dialog/ArrowDown.svg"></div>
|
<div v-if="useDisplay.Sender_variant === 'default'" class="scroll-to-bottom-text" @click.stop="handleScrollToBottom">回到底部<img src="@/assets/dialog/ArrowDown.svg"></div>
|
||||||
|
|
||||||
<div v-show="modelType !== 'text'" class="upload-img-container">
|
<div v-show="showImageUploader" class="upload-img-container">
|
||||||
<div class="reference-diagram">
|
<div class="reference-diagram">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
v-if="props.type === 'Painting'"
|
v-if="props.type === 'Painting'"
|
||||||
ref="referenceDiagramRef"
|
ref="referenceDiagramRef"
|
||||||
v-model="referenceImages"
|
v-model="referenceImages"
|
||||||
:limit="4"
|
:limit="imageUploadLimit"
|
||||||
@open-canvas="handleOpenCanvas"
|
@open-canvas="handleOpenCanvas"
|
||||||
/>
|
/>
|
||||||
<VideoImageUploader
|
<VideoImageUploader
|
||||||
v-else-if="props.type === 'Video'"
|
v-else-if="props.type === 'Video'"
|
||||||
@ -31,13 +31,17 @@
|
|||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div v-if="useDisplay.Sender_variant !== 'default' && props.type === 'Painting'" class="prefix-self-wrap">
|
<div v-if="useDisplay.Sender_variant !== 'default' && props.type === 'Painting'" class="prefix-self-wrap">
|
||||||
<paintingModel v-model="model" v-model:typeValue="modelType" />
|
<paintingModel v-model="model" v-model:typeValue="modelType" />
|
||||||
<paintingProportion
|
<paintingProportion
|
||||||
v-model="proportion"
|
v-if="showProportion"
|
||||||
v-model:resolution="resolution"
|
v-model="proportion"
|
||||||
:proportion-options="proportionOptions"
|
v-model:resolution="resolution"
|
||||||
:resolution-options="resolutionOptions"
|
v-model:width="customWidth"
|
||||||
|
v-model:height="customHight"
|
||||||
|
:proportion-options="paintingProportionOpts"
|
||||||
|
:resolution-options="paintingResolutionOpts"
|
||||||
|
:allow-custom="hasCustomSize"
|
||||||
/>
|
/>
|
||||||
<Quantity v-model="quantity" />
|
<Quantity v-if="showQuantity" v-model="quantity" :max="quantityMax" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="useDisplay.Sender_variant !== 'default' && props.type === 'Video'" class="prefix-self-wrap">
|
<div v-if="useDisplay.Sender_variant !== 'default' && props.type === 'Video'" class="prefix-self-wrap">
|
||||||
@ -73,20 +77,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import paintingProportion from './proportion/painting.vue'
|
|
||||||
import videoProportion from './proportion/video.vue'
|
import videoProportion from './proportion/video.vue'
|
||||||
import paintingModel from './model/painting.vue'
|
import paintingModel from './model/painting.vue'
|
||||||
import videoModel from './model/video.vue'
|
import videoModel from './model/video.vue'
|
||||||
import Quantity from './quantity/index.vue'
|
|
||||||
import Pattern from './pattern/index.vue'
|
import Pattern from './pattern/index.vue'
|
||||||
import ImageUploader from './imageUploader/index.vue'
|
import ImageUploader from './imageUploader/index.vue'
|
||||||
import VideoImageUploader from './videoImageUploader/index.vue'
|
import VideoImageUploader from './videoImageUploader/index.vue'
|
||||||
import Time from './Time/index.vue'
|
import Time from './Time/index.vue'
|
||||||
|
import paintingProportion from './proportion/painting.vue'
|
||||||
|
import Quantity from './quantity/index.vue'
|
||||||
import { Sender } from 'vue-element-plus-x'
|
import { Sender } from 'vue-element-plus-x'
|
||||||
import { useDisplayStore, useUserStore } from '@/stores'
|
import { useDisplayStore } from '@/stores'
|
||||||
import { generate } from '@/utils/websocket'
|
import { generate } from '@/utils/websocket'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { getModelId, fetchPlatformModels, getPlatformCode } from '@/utils/modelApi'
|
||||||
import { fetchModelConfig } from '@/utils/modelConfig'
|
import { fetchModelConfig } from '@/utils/modelConfig'
|
||||||
|
import { getModelConfig } from '@/config/models/index.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
isGenerate: {
|
isGenerate: {
|
||||||
@ -105,30 +111,162 @@ const props = defineProps({
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const useDisplay = useDisplayStore()
|
const useDisplay = useDisplayStore()
|
||||||
const useUser = useUserStore()
|
|
||||||
|
|
||||||
const isgerenate = ref(false)
|
const isgerenate = ref(false)
|
||||||
|
|
||||||
const model = ref() // 模型
|
const model = ref() // 模型
|
||||||
const modelType = ref('text')
|
const modelType = ref('text')
|
||||||
|
|
||||||
const modelDisplayConfig = ref(null)
|
// 当前模型配置
|
||||||
|
const modelConfig = computed(() => {
|
||||||
|
return props.type === 'Painting' ? getModelConfig(model.value) : null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模型参数值
|
||||||
|
const paramValues = reactive({})
|
||||||
|
|
||||||
|
const showImageUploader = computed(() => {
|
||||||
|
if (props.type === 'Video') return modelType.value !== 'text'
|
||||||
|
return modelType.value !== 'text' || modelConfig.value?.inputType === 'image' || modelConfig.value?.inputType === 'both'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模型是否有数量参数(imageNum),或生成类模型显示张数选择
|
||||||
|
const showQuantity = computed(() => {
|
||||||
|
if (props.type !== 'Painting') return false
|
||||||
|
const qtyParam = modelConfig.value?.params?.find(p => p.ui === 'quantity')
|
||||||
|
if (qtyParam) return true
|
||||||
|
return modelType.value === 'text' && !modelConfig.value?.params?.find(p => p.name === 'forceSingle')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模型是否使用 proportion 组件(aspectRatio 参数)
|
||||||
|
const showProportion = computed(() => {
|
||||||
|
return !!modelConfig.value?.params?.find(p => p.ui === 'proportion')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模型是否支持自定义尺寸(aspectRatio 选项含 'custom')
|
||||||
|
const hasCustomSize = computed(() => {
|
||||||
|
const ratioParam = modelConfig.value?.params?.find(p => p.ui === 'proportion')
|
||||||
|
return ratioParam?.options?.includes('custom') || false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从模型配置派生比例选项,回退到默认值
|
||||||
|
const paintingProportionOpts = computed(() => {
|
||||||
|
const ratioParam = modelConfig.value?.params?.find(p => p.ui === 'proportion')
|
||||||
|
if (ratioParam?.options) {
|
||||||
|
return ratioParam.options
|
||||||
|
.filter(o => o !== 'custom')
|
||||||
|
.map(o => ({ value: o, label: o }))
|
||||||
|
}
|
||||||
|
return proportionOptions.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从模型配置派生分辨率选项(仅模型有 resolution 参数时显示)
|
||||||
|
const paintingResolutionOpts = computed(() => {
|
||||||
|
const resParam = modelConfig.value?.params?.find(p => p.ui === 'resolution')
|
||||||
|
if (resParam?.options) {
|
||||||
|
return resParam.options.map(o => ({ value: o, label: o.toUpperCase() }))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageUploadLimit = computed(() => {
|
||||||
|
if (!modelConfig.value) return 4
|
||||||
|
const imageParam = modelConfig.value.params.find(p => p.ui === 'imageUpload')
|
||||||
|
return imageParam?.maxCount || modelConfig.value.maxImages || 4
|
||||||
|
})
|
||||||
|
|
||||||
const promptPlaceholder = ref('描述你想生成的画面和动作。') // 提示词占位符
|
const promptPlaceholder = ref('描述你想生成的画面和动作。') // 提示词占位符
|
||||||
|
|
||||||
const prompt = ref('') // 提示词
|
const prompt = ref('') // 提示词
|
||||||
const proportion = ref('16:9') // 比例
|
const proportion = ref('16:9') // 比例(Video 用)
|
||||||
const resolution = ref('1k') // 分辨率
|
const resolution = ref('1k') // 分辨率(Video 用)
|
||||||
const referenceImages = ref([])
|
const referenceImages = ref([])
|
||||||
|
|
||||||
// 绘画
|
// 绘画
|
||||||
const quantity = ref(1) // 生成数量
|
const quantity = ref(1) // 生成数量
|
||||||
|
const customWidth = ref(1024) // 自定义宽度
|
||||||
|
const customHight = ref(1024) // 自定义高度
|
||||||
|
|
||||||
|
const quantityMax = computed(() => {
|
||||||
|
const qtyParam = modelConfig.value?.params?.find(p => p.ui === 'quantity')
|
||||||
|
if (qtyParam?.options?.length) return Math.max(...qtyParam.options)
|
||||||
|
return 4
|
||||||
|
})
|
||||||
|
|
||||||
|
// 同步模型默认值到 paramValues 和 UI refs
|
||||||
|
watch(modelConfig, (config) => {
|
||||||
|
if (!config) return
|
||||||
|
config.params.forEach(p => {
|
||||||
|
if (!(p.name in paramValues)) {
|
||||||
|
if (p.name === 'outputFormat') {
|
||||||
|
paramValues[p.name] = 'png'
|
||||||
|
} else {
|
||||||
|
paramValues[p.name] = p.default ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 同步默认值到 UI 控件
|
||||||
|
const ratioParam = config.params.find(p => p.ui === 'proportion')
|
||||||
|
if (ratioParam) proportion.value = ratioParam.default || '1:1'
|
||||||
|
const resParam = config.params.find(p => p.ui === 'resolution')
|
||||||
|
if (resParam) resolution.value = resParam.default || '2k'
|
||||||
|
const qtyParam = config.params.find(p => p.ui === 'quantity')
|
||||||
|
if (qtyParam) quantity.value = qtyParam.default || 1
|
||||||
|
const cwParam = config.params.find(p => p.name === 'customWidth')
|
||||||
|
if (cwParam) customWidth.value = cwParam.default || 1024
|
||||||
|
const chParam = config.params.find(p => p.name === 'customHight')
|
||||||
|
if (chParam) customHight.value = chParam.default || 1024
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 反向同步:UI refs → paramValues
|
||||||
|
watch(proportion, (val) => {
|
||||||
|
const p = modelConfig.value?.params?.find(param => param.ui === 'proportion')
|
||||||
|
if (p) paramValues[p.name] = val
|
||||||
|
})
|
||||||
|
watch(resolution, (val) => {
|
||||||
|
const p = modelConfig.value?.params?.find(param => param.ui === 'resolution')
|
||||||
|
if (p) paramValues[p.name] = val
|
||||||
|
})
|
||||||
|
watch(quantity, (val) => {
|
||||||
|
const p = modelConfig.value?.params?.find(param => param.ui === 'quantity')
|
||||||
|
if (p) paramValues[p.name] = val
|
||||||
|
})
|
||||||
|
watch(customWidth, (val) => {
|
||||||
|
if (modelConfig.value?.params?.find(p => p.name === 'customWidth')) {
|
||||||
|
paramValues.customWidth = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(customHight, (val) => {
|
||||||
|
if (modelConfig.value?.params?.find(p => p.name === 'customHight')) {
|
||||||
|
paramValues.customHight = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 同步参考图片到 paramValues
|
||||||
|
watch(referenceImages, (imgs) => {
|
||||||
|
const imageParam = modelConfig.value?.params?.find(p => p.ui === 'imageUpload')
|
||||||
|
if (imageParam) {
|
||||||
|
paramValues[imageParam.name] = imgs.map(img => img.url)
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
// 视频
|
// 视频
|
||||||
const duration = ref(5) // 时间
|
const duration = ref(5) // 时间
|
||||||
const videoPattern = ref('文生视频') // 视频模式下,默认值为'文生视频'
|
const videoPattern = ref('文生视频') // 视频模式下,默认值为'文生视频'
|
||||||
|
|
||||||
const resolutionOptions = ref([])
|
const resolutionOptions = ref([
|
||||||
const proportionOptions = ref([])
|
{ value: '1k', label: '标清 1K' },
|
||||||
|
{ value: '2k', label: '高清 2K' },
|
||||||
|
{ value: '4k', label: '超清 4K' },
|
||||||
|
])
|
||||||
|
const proportionOptions = ref([
|
||||||
|
{ value: '智能', label: '智能' },
|
||||||
|
{ value: '21:9', label: '21:9' },
|
||||||
|
{ value: '16:9', label: '16:9' },
|
||||||
|
{ value: '4:3', label: '4:3' },
|
||||||
|
{ value: '1:1', label: '1:1' },
|
||||||
|
{ value: '3:4', label: '3:4' },
|
||||||
|
{ value: '9:16', label: '9:16' },
|
||||||
|
])
|
||||||
const durationOptions = ref([])
|
const durationOptions = ref([])
|
||||||
|
|
||||||
const isInitialized = ref(false)
|
const isInitialized = ref(false)
|
||||||
@ -141,44 +279,38 @@ const autoSizeConfig = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadModelConfig = async (modelName, currentModelType) => {
|
const modelDisplayConfig = ref(null)
|
||||||
|
|
||||||
|
// Video: 从远程加载 workflow 配置(保留旧逻辑)
|
||||||
|
const loadVideoModelConfig = async (modelName, currentModelType) => {
|
||||||
try {
|
try {
|
||||||
const config = await fetchModelConfig(props.type, modelName, currentModelType)
|
const config = await fetchModelConfig(props.type, modelName, currentModelType)
|
||||||
modelDisplayConfig.value = config
|
modelDisplayConfig.value = config
|
||||||
|
|
||||||
if (config.display) {
|
if (config.display) {
|
||||||
const display = config.display
|
const display = config.display
|
||||||
|
|
||||||
if (display.promptPlaceholder) {
|
if (display.promptPlaceholder) {
|
||||||
promptPlaceholder.value = display.promptPlaceholder.default || '描述你想生成的画面和动作。'
|
promptPlaceholder.value = display.promptPlaceholder.default || '描述你想生成的画面和动作。'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display.prompt && !isInitialized.value) {
|
if (display.prompt && !isInitialized.value) {
|
||||||
prompt.value = display.prompt.default || ''
|
prompt.value = display.prompt.default || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display.resolution) {
|
if (display.resolution) {
|
||||||
resolution.value = display.resolution.default || '1k'
|
resolution.value = display.resolution.default || '1k'
|
||||||
resolutionOptions.value = display.resolution.options || []
|
resolutionOptions.value = display.resolution.options || []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display.proportion) {
|
if (display.proportion) {
|
||||||
proportion.value = display.proportion.default || '16:9'
|
proportion.value = display.proportion.default || '16:9'
|
||||||
proportionOptions.value = display.proportion.options || []
|
proportionOptions.value = display.proportion.options || []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (display.duration) {
|
if (display.duration) {
|
||||||
duration.value = display.duration.default || 5
|
duration.value = display.duration.default || 5
|
||||||
durationOptions.value = display.duration.options || []
|
durationOptions.value = display.duration.options || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isInitialized.value = true
|
isInitialized.value = true
|
||||||
|
|
||||||
return config
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载模型配置失败:', error)
|
console.error('加载视频模型配置失败:', error)
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +322,7 @@ const handleStart = async () => {
|
|||||||
ElMessage.primary('敬请期待 Seedance 2.0')
|
ElMessage.primary('敬请期待 Seedance 2.0')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.isGenerate) {
|
if (!props.isGenerate) {
|
||||||
router.push({ name: 'home', query: { loading: false, Generate: true, type: currentType } })
|
router.push({ name: 'home', query: { loading: false, Generate: true, type: currentType } })
|
||||||
}
|
}
|
||||||
@ -199,18 +331,22 @@ const handleStart = async () => {
|
|||||||
ElMessage.error('请输入提示词')
|
ElMessage.error('请输入提示词')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (modelType.value === 'image' && !referenceImages.value.length){
|
if (showImageUploader.value && !referenceImages.value.length){
|
||||||
ElMessage.warning('请上传图片')
|
ElMessage.warning('请上传图片')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isgerenate.value = true
|
isgerenate.value = true
|
||||||
console.log('生成开始', isgerenate.value)
|
console.log('生成开始', isgerenate.value)
|
||||||
const imgs = []
|
const imgs = []
|
||||||
referenceImages.value.forEach((img, index) => {
|
referenceImages.value.forEach((img, index) => {
|
||||||
imgs.push({ name: `image_${index + 1}`, url: img.url })
|
imgs.push({ name: `image_${index + 1}`, url: img.url })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 构建模型参数(Painting 用)
|
||||||
|
const modelParams = { ...paramValues }
|
||||||
|
if (prompt.value) modelParams.prompt = prompt.value
|
||||||
|
|
||||||
const generateData = {
|
const generateData = {
|
||||||
model: model.value,
|
model: model.value,
|
||||||
modelType: currentModelType,
|
modelType: currentModelType,
|
||||||
@ -219,27 +355,34 @@ const handleStart = async () => {
|
|||||||
referenceImages: referenceImages.value,
|
referenceImages: referenceImages.value,
|
||||||
quantity: quantity.value,
|
quantity: quantity.value,
|
||||||
resolution: resolution.value,
|
resolution: resolution.value,
|
||||||
|
customWidth: customWidth.value,
|
||||||
|
customHight: customHight.value,
|
||||||
duration: duration.value,
|
duration: duration.value,
|
||||||
videoPattern: videoPattern.value
|
videoPattern: videoPattern.value,
|
||||||
|
modelParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const modelId = await getModelId(currentType, model.value)
|
||||||
|
|
||||||
|
// Painting 用新架构扁平参数,Video 保留旧 params 数组
|
||||||
|
const isPainting = currentType === 'Painting'
|
||||||
|
const data = {
|
||||||
type: currentType,
|
type: currentType,
|
||||||
modelType: currentModelType,
|
modelType: currentModelType,
|
||||||
AIGC: currentType,
|
AIGC: currentType,
|
||||||
platform: 'runninghub',
|
platform: 'runninghub',
|
||||||
modelName: model.value,
|
modelName: model.value,
|
||||||
quantity: quantity.value,
|
modelId: modelId || '',
|
||||||
free: useUser.freeTimes,
|
modelParams: isPainting ? modelParams : {},
|
||||||
params: [
|
params: isPainting ? [] : [
|
||||||
{ name: 'prompt', data: prompt.value},
|
{ name: 'prompt', data: prompt.value },
|
||||||
{ name: 'quantity', data: quantity.value},
|
{ name: 'quantity', data: quantity.value },
|
||||||
{ name: 'proportion', data: proportion.value},
|
{ name: 'proportion', data: proportion.value },
|
||||||
{ name: 'resolution', data: resolution.value},
|
{ name: 'resolution', data: resolution.value },
|
||||||
{ name: 'duration', data: duration.value}
|
{ name: 'duration', data: duration.value },
|
||||||
],
|
],
|
||||||
imgs,
|
imgs,
|
||||||
result: JSON.stringify(generateData)
|
request: JSON.stringify(generateData)
|
||||||
}
|
}
|
||||||
await generate(data, generateData)
|
await generate(data, generateData)
|
||||||
console.log('生成中', isgerenate.value)
|
console.log('生成中', isgerenate.value)
|
||||||
@ -255,8 +398,11 @@ const fillParamsFromResult = (resultData) => {
|
|||||||
if (resultData.referenceImages !== undefined) referenceImages.value = resultData.referenceImages
|
if (resultData.referenceImages !== undefined) referenceImages.value = resultData.referenceImages
|
||||||
if (resultData.quantity !== undefined) quantity.value = resultData.quantity
|
if (resultData.quantity !== undefined) quantity.value = resultData.quantity
|
||||||
if (resultData.resolution !== undefined) resolution.value = resultData.resolution
|
if (resultData.resolution !== undefined) resolution.value = resultData.resolution
|
||||||
|
if (resultData.customWidth !== undefined) customWidth.value = resultData.customWidth
|
||||||
|
if (resultData.customHight !== undefined) customHight.value = resultData.customHight
|
||||||
if (resultData.duration !== undefined) duration.value = resultData.duration
|
if (resultData.duration !== undefined) duration.value = resultData.duration
|
||||||
if (resultData.videoPattern !== undefined) videoPattern.value = resultData.videoPattern
|
if (resultData.videoPattern !== undefined) videoPattern.value = resultData.videoPattern
|
||||||
|
if (resultData.modelParams !== undefined) Object.assign(paramValues, resultData.modelParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@ -286,27 +432,27 @@ watch(() => useDisplay.isSubGerenate, (newValue) => {
|
|||||||
|
|
||||||
watch([() => model.value, () => modelType.value], async ([newModel, newModelType]) => {
|
watch([() => model.value, () => modelType.value], async ([newModel, newModelType]) => {
|
||||||
console.log('模型或类型改变:', newModel, newModelType)
|
console.log('模型或类型改变:', newModel, newModelType)
|
||||||
if (newModel && newModelType) {
|
if (!newModel) return
|
||||||
await loadModelConfig(newModel, newModelType)
|
if (props.type !== 'Painting') {
|
||||||
|
await loadVideoModelConfig(newModel, newModelType)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 预加载平台模型列表,避免首次点击"发送"时才请求接口
|
||||||
|
const prefetchModels = () => {
|
||||||
|
const code = getPlatformCode(props.type)
|
||||||
|
fetchPlatformModels(code)
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => props.type, (newType) => {
|
watch(() => props.type, (newType) => {
|
||||||
if (newType === 'Video') {
|
if (newType === 'Video') {
|
||||||
model.value = 'LTX2.0'
|
model.value = 'LTX2.0'
|
||||||
} else {
|
} else {
|
||||||
model.value = 'flux'
|
model.value = 'flux'
|
||||||
}
|
}
|
||||||
const chargeType = newType === 'Painting' ? 1 : 4
|
prefetchModels()
|
||||||
useUser.fetchFreeTimes(chargeType)
|
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
// 组件挂载时获取免费次数
|
|
||||||
onMounted(async () => {
|
|
||||||
const chargeType = props.type === 'Painting' ? 1 : 4
|
|
||||||
await useUser.fetchFreeTimes(chargeType)
|
|
||||||
console.log('免费次数', useUser.freeTimes)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Select
|
<Select
|
||||||
v-model="model"
|
v-model="selectValue"
|
||||||
:grouped-options="modelGroups"
|
:grouped-options="modelGroups"
|
||||||
class="model-select"
|
class="model-select"
|
||||||
position="top"
|
position="top"
|
||||||
@ -13,122 +13,123 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Select from '@/components/Select/index.vue'
|
import Select from '@/components/Select/index.vue'
|
||||||
|
import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi'
|
||||||
|
import { getModelConfig } from '@/config/models/index.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: { type: String, default: 'Flux 2' },
|
||||||
type: String,
|
typeValue: { type: String, default: 'text' },
|
||||||
default: 'flux'
|
|
||||||
},
|
|
||||||
typeValue: {
|
|
||||||
type: String,
|
|
||||||
default: 'text'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'update:typeValue'])
|
const emit = defineEmits(['update:modelValue', 'update:typeValue'])
|
||||||
|
|
||||||
const paintingConfig = ref({
|
const platformModels = ref([])
|
||||||
generate: [],
|
|
||||||
edit: [],
|
|
||||||
vision: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const categoryMap = [
|
||||||
try {
|
{ tag: 'text', label: '生成模型', inputType: 'text' },
|
||||||
const url = `${import.meta.env.VITE_API_MODEL_RESOURCE}/static/public/Platform/AIGC_modelConfig/painting.json`
|
{ tag: 'edit', label: '编辑模型', inputType: 'image' },
|
||||||
const response = await fetch(url)
|
{ tag: 'vision', label: '视觉理解模型', inputType: 'vision' },
|
||||||
const data = await response.json()
|
]
|
||||||
paintingConfig.value = data
|
|
||||||
} catch (error) {
|
function parseValue(encoded) {
|
||||||
console.error('Failed to fetch painting config:', error)
|
if (!encoded) return null
|
||||||
}
|
const idx = encoded.indexOf('::')
|
||||||
|
if (idx === -1) return null
|
||||||
|
return { tag: encoded.substring(0, idx), modelName: encoded.substring(idx + 2) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeValue(tag, modelName) {
|
||||||
|
return `${tag}::${modelName}`
|
||||||
|
}
|
||||||
|
|
||||||
fetchConfig()
|
function findTagForModel(modelName) {
|
||||||
|
for (const cat of categoryMap) {
|
||||||
watch(() => paintingConfig.value, (newConfig) => {
|
const model = platformModels.value.find(m => (m.display_name || m.name) === modelName && m.tags?.includes(cat.tag))
|
||||||
const allModels = [
|
if (model) return cat.tag
|
||||||
...(newConfig.generate || []),
|
|
||||||
...(newConfig.edit || []),
|
|
||||||
...(newConfig.vision || [])
|
|
||||||
]
|
|
||||||
if (allModels.length > 0) {
|
|
||||||
const enabledModels = allModels.filter(m => !m.disabled)
|
|
||||||
if (enabledModels.length > 0) {
|
|
||||||
const currentModelExists = enabledModels.find(m => m.value === props.modelValue)
|
|
||||||
if (!currentModelExists) {
|
|
||||||
const firstEnabled = enabledModels[0].value
|
|
||||||
emit('update:modelValue', firstEnabled)
|
|
||||||
const newType = getModelType(firstEnabled)
|
|
||||||
emit('update:typeValue', newType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, { deep: true })
|
|
||||||
|
|
||||||
|
|
||||||
const model = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value) => {
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
const newType = getModelType(value)
|
|
||||||
emit('update:typeValue', newType)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const generateModels = computed(() => paintingConfig.value.generate || [])
|
|
||||||
const editModels = computed(() => paintingConfig.value.edit || [])
|
|
||||||
const visionModels = computed(() => paintingConfig.value.vision || [])
|
|
||||||
|
|
||||||
const modelGroups = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: '生成模型',
|
|
||||||
options: generateModels.value
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '编辑模型',
|
|
||||||
options: editModels.value
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '视觉理解模型',
|
|
||||||
options: visionModels.value
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const getModelType = (value) => {
|
|
||||||
if (generateModels.value.find(m => m.value === value)) {
|
|
||||||
return 'text'
|
|
||||||
}
|
|
||||||
if (editModels.value.find(m => m.value === value)) {
|
|
||||||
return 'image'
|
|
||||||
}
|
|
||||||
if (visionModels.value.find(m => m.value === value)) {
|
|
||||||
return 'vision'
|
|
||||||
}
|
}
|
||||||
return 'text'
|
return 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFirstEnabledModel = () => {
|
function tagToInputType(tag) {
|
||||||
const allModels = [...generateModels.value, ...editModels.value, ...visionModels.value]
|
const cat = categoryMap.find(c => c.tag === tag)
|
||||||
const firstEnabled = allModels.find(m => !m.disabled)
|
return cat?.inputType || 'text'
|
||||||
return firstEnabled ? firstEnabled.value : ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.modelValue, (newValue) => {
|
// Select 双向绑定值(内部编码)
|
||||||
const allModels = [...generateModels.value, ...editModels.value, ...visionModels.value]
|
const selectValue = computed({
|
||||||
const currentModel = allModels.find(m => m.value === newValue)
|
get: () => {
|
||||||
if (currentModel && currentModel.disabled) {
|
if (!props.modelValue) return ''
|
||||||
const firstEnabled = getFirstEnabledModel()
|
const tag = findTagForModel(props.modelValue)
|
||||||
|
return encodeValue(tag, props.modelValue)
|
||||||
|
},
|
||||||
|
set: (encoded) => {
|
||||||
|
const parsed = parseValue(encoded)
|
||||||
|
if (!parsed) return
|
||||||
|
emit('update:modelValue', parsed.modelName)
|
||||||
|
emit('update:typeValue', tagToInputType(parsed.tag))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从 API 加载模型列表
|
||||||
|
const loadModels = async () => {
|
||||||
|
try {
|
||||||
|
const code = getPlatformCode('Painting')
|
||||||
|
const models = await fetchPlatformModels(code)
|
||||||
|
platformModels.value = models || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载平台模型列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadModels()
|
||||||
|
|
||||||
|
// 按固定分类分组,value 编码为 tag::displayName
|
||||||
|
const modelGroups = computed(() => {
|
||||||
|
const models = platformModels.value
|
||||||
|
if (models.length === 0) return []
|
||||||
|
|
||||||
|
return categoryMap
|
||||||
|
.filter(cat => models.some(m => m.tags?.includes(cat.tag)))
|
||||||
|
.map(cat => ({
|
||||||
|
label: cat.label,
|
||||||
|
options: models
|
||||||
|
.filter(m => m.tags?.includes(cat.tag))
|
||||||
|
.map(m => ({
|
||||||
|
value: `${cat.tag}::${m.display_name || m.name}`,
|
||||||
|
label: m.display_name || m.name,
|
||||||
|
disabled: m.disabled || false,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 模型列表加载后自动纠正不可用模型
|
||||||
|
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) {
|
if (firstEnabled) {
|
||||||
emit('update:modelValue', firstEnabled)
|
emit('update:modelValue', firstEnabled.display_name || firstEnabled.name)
|
||||||
const newType = getModelType(firstEnabled)
|
emit('update:typeValue', tagToInputType(findTagForModel(firstEnabled.display_name || firstEnabled.name)))
|
||||||
emit('update:typeValue', newType)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 外部改变 modelValue 时校验是否可用
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (!newValue) return
|
||||||
|
const models = platformModels.value
|
||||||
|
if (models.length === 0) return
|
||||||
|
const currentModel = models.find(m => (m.display_name || m.name) === newValue)
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@ -139,24 +140,24 @@ watch(() => props.modelValue, (newValue) => {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid #E8E9EB;
|
border: 1px solid #E8E9EB;
|
||||||
background: #f5f6f7;
|
background: #f5f6f7;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #e9eaeb;
|
background: #e9eaeb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.select-text) {
|
:deep(.select-text) {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.dropdown-menu) {
|
:deep(.dropdown-menu) {
|
||||||
max-height: 510px;
|
max-height: 510px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.dropdown-item) {
|
:deep(.dropdown-item) {
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: rgba(0, 15, 51, 0.10);
|
background: rgba(0, 15, 51, 0.10);
|
||||||
color: #000F33;
|
color: #000F33;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="top" :width="400">
|
<Popover placement="top">
|
||||||
<div class="proportion-container">
|
<div class="proportion-container">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>选择比例</h3>
|
<h3>选择比例</h3>
|
||||||
@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div v-if="allowCustom" class="section">
|
||||||
<h3>尺寸(px)</h3>
|
<h3>尺寸(px)</h3>
|
||||||
<div class="size-inputs">
|
<div class="size-inputs">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@ -85,6 +85,10 @@ const props = defineProps({
|
|||||||
{ value: '9:16', label: '9:16' }
|
{ value: '9:16', label: '9:16' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
allowCustom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
resolutionOptions: {
|
resolutionOptions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [
|
default: () => [
|
||||||
@ -244,6 +248,7 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
|
|
||||||
.proportion-container{
|
.proportion-container{
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section{
|
.section{
|
||||||
@ -265,7 +270,7 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
.proportion-options{
|
.proportion-options{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: space-between;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background-color: #F8F9FA;
|
background-color: #F8F9FA;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -321,14 +326,13 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.resolution-item{
|
.resolution-item{
|
||||||
flex: 1;
|
padding: 10px 16px;
|
||||||
padding: 10px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
// background: #f5f5f5;
|
white-space: nowrap;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
|
||||||
&:hover{
|
&:hover{
|
||||||
@ -350,6 +354,7 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
|
|
||||||
.input-group{
|
.input-group{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
label{
|
label{
|
||||||
@ -363,6 +368,7 @@ watch(() => [props.modelValue, props.resolution], () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input{
|
input{
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 12px 12px 12px 30px;
|
padding: 12px 12px 12px 30px;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="top" :width="400">
|
<Popover placement="top">
|
||||||
<div class="proportion-container">
|
<div class="proportion-container">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>选择比例</h3>
|
<h3>选择比例</h3>
|
||||||
@ -142,6 +142,7 @@ const getProportionStyle = (value) => {
|
|||||||
|
|
||||||
.proportion-container{
|
.proportion-container{
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section{
|
.section{
|
||||||
|
|||||||
@ -18,6 +18,10 @@ const props = defineProps({
|
|||||||
modelValue: {
|
modelValue: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
default: 4
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -28,12 +32,9 @@ const quantity = computed({
|
|||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const quantityOptions = [
|
const quantityOptions = computed(() =>
|
||||||
{ value: 1, label: '1 张' },
|
Array.from({ length: props.max }, (_, i) => ({ value: i + 1, label: `${i + 1} 张` }))
|
||||||
{ value: 2, label: '2 张' },
|
)
|
||||||
{ value: 3, label: '3 张' },
|
|
||||||
{ value: 4, label: '4 张' }
|
|
||||||
]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
51
src/config/models/flux.js
Normal file
51
src/config/models/flux.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Flux 2 Dev — 文生图
|
||||||
|
export default {
|
||||||
|
name: 'Flux 2',
|
||||||
|
tag: '文生图',
|
||||||
|
inputType: 'text',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '提示词',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
ui: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'aspectRatio',
|
||||||
|
label: '比例',
|
||||||
|
type: 'select',
|
||||||
|
default: '1:1',
|
||||||
|
options: ['1:1', '3:4', '4:3', '9:16', '16:9', '2:3', '3:2', 'custom'],
|
||||||
|
ui: 'proportion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customWidth',
|
||||||
|
label: '宽度',
|
||||||
|
type: 'number',
|
||||||
|
default: 1024,
|
||||||
|
min: 256,
|
||||||
|
max: 1536,
|
||||||
|
ui: 'number',
|
||||||
|
showWhen: { aspectRatio: 'custom' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customHight',
|
||||||
|
label: '高度',
|
||||||
|
type: 'number',
|
||||||
|
default: 1024,
|
||||||
|
min: 256,
|
||||||
|
max: 1536,
|
||||||
|
ui: 'number',
|
||||||
|
showWhen: { aspectRatio: 'custom' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'outputFormat',
|
||||||
|
label: '输出格式',
|
||||||
|
type: 'string',
|
||||||
|
default: 'png',
|
||||||
|
options: ['png', 'jpeg', 'webp(lossless)', 'webp(lossy)'],
|
||||||
|
ui: 'hidden',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
48
src/config/models/gpt-image-i2i.js
Normal file
48
src/config/models/gpt-image-i2i.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// GPT-Image-2 I2I — 图片编辑
|
||||||
|
export default {
|
||||||
|
name: 'GPT-Image-2 I2I',
|
||||||
|
tag: '图片编辑',
|
||||||
|
inputType: 'image',
|
||||||
|
maxImages: 10,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'imageUrls',
|
||||||
|
label: '参考图片',
|
||||||
|
type: 'image',
|
||||||
|
required: true,
|
||||||
|
ui: 'imageUpload',
|
||||||
|
maxCount: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '编辑指令',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
ui: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'aspectRatio',
|
||||||
|
label: '比例',
|
||||||
|
type: 'select',
|
||||||
|
default: '1:1',
|
||||||
|
options: ['1:1', '1:2', '2:1', '1:3', '3:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '21:9', '9:21', '16:9'],
|
||||||
|
ui: 'proportion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resolution',
|
||||||
|
label: '分辨率',
|
||||||
|
type: 'select',
|
||||||
|
default: '2k',
|
||||||
|
options: ['1k', '2k', '4k'],
|
||||||
|
ui: 'resolution',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: '画质',
|
||||||
|
type: 'select',
|
||||||
|
default: 'medium',
|
||||||
|
options: ['low', 'medium', 'high'],
|
||||||
|
ui: 'select',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
39
src/config/models/gpt-image.js
Normal file
39
src/config/models/gpt-image.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// GPT-Image-2 — 文生图
|
||||||
|
export default {
|
||||||
|
name: 'GPT-Image-2',
|
||||||
|
tag: '文生图',
|
||||||
|
inputType: 'text',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '提示词',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
ui: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'aspectRatio',
|
||||||
|
label: '比例',
|
||||||
|
type: 'select',
|
||||||
|
default: '1:1',
|
||||||
|
options: ['1:1', '1:2', '2:1', '1:3', '3:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '21:9', '9:21', '16:9'],
|
||||||
|
ui: 'proportion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resolution',
|
||||||
|
label: '分辨率',
|
||||||
|
type: 'select',
|
||||||
|
default: '2k',
|
||||||
|
options: ['1k', '2k', '4k'],
|
||||||
|
ui: 'resolution',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: '画质',
|
||||||
|
type: 'select',
|
||||||
|
default: 'medium',
|
||||||
|
options: ['low', 'medium', 'high'],
|
||||||
|
ui: 'select',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
45
src/config/models/index.js
Normal file
45
src/config/models/index.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// 模型配置注册表 — 按模型名称查找参数 schema
|
||||||
|
import flux from './flux.js'
|
||||||
|
import zImage from './z-image.js'
|
||||||
|
import jimeng from './jimeng.js'
|
||||||
|
import qwen from './qwen.js'
|
||||||
|
import gptImage from './gpt-image.js'
|
||||||
|
import nanoPro from './nano-pro.js'
|
||||||
|
import qwenEdit from './qwen-edit.js'
|
||||||
|
import gptImageI2i from './gpt-image-i2i.js'
|
||||||
|
|
||||||
|
const configs = {
|
||||||
|
'Flux 2': flux,
|
||||||
|
'Z-Image Turbo': zImage,
|
||||||
|
'即梦4.6': jimeng,
|
||||||
|
'通义万相2.0': qwen,
|
||||||
|
'GPT-Image-2': gptImage,
|
||||||
|
'Nano Pro': nanoPro,
|
||||||
|
'通义万相2.0 Pro': qwenEdit,
|
||||||
|
'GPT-Image-2 I2i': gptImageI2i,
|
||||||
|
}
|
||||||
|
|
||||||
|
// API display_name → config key 映射(API 返回的 display_name 可能与 config 的 name 不同)
|
||||||
|
const displayNameMap = {
|
||||||
|
'flux': 'Flux 2',
|
||||||
|
'Z-image': 'Z-Image Turbo',
|
||||||
|
'Jimeng4.6': '即梦4.6',
|
||||||
|
'QwenImage2.0': '通义万相2.0',
|
||||||
|
'GPT-image-2': 'GPT-Image-2',
|
||||||
|
'Banana-Pro': 'Nano Pro',
|
||||||
|
'QwenImage2.0-Pro': '通义万相2.0 Pro',
|
||||||
|
'GPT-Image-2': 'GPT-Image-2 I2I',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据模型名称获取参数配置,支持 API display_name 和 config key 两种方式查找 */
|
||||||
|
export function getModelConfig(modelName) {
|
||||||
|
if (configs[modelName]) return configs[modelName]
|
||||||
|
const mappedKey = displayNameMap[modelName]
|
||||||
|
if (mappedKey && configs[mappedKey]) return configs[mappedKey]
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取所有模型配置 */
|
||||||
|
export function getAllModelConfigs() {
|
||||||
|
return configs
|
||||||
|
}
|
||||||
33
src/config/models/jimeng.js
Normal file
33
src/config/models/jimeng.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// 即梦 4.6 — 文生图(直接指定宽高像素)
|
||||||
|
export default {
|
||||||
|
name: '即梦4.6',
|
||||||
|
tag: '文生图',
|
||||||
|
inputType: 'text',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '提示词',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
ui: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'width',
|
||||||
|
label: '宽度',
|
||||||
|
type: 'number',
|
||||||
|
default: 1024,
|
||||||
|
min: 900,
|
||||||
|
max: 6197,
|
||||||
|
ui: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'height',
|
||||||
|
label: '高度',
|
||||||
|
type: 'number',
|
||||||
|
default: 1024,
|
||||||
|
min: 768,
|
||||||
|
max: 4096,
|
||||||
|
ui: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
40
src/config/models/nano-pro.js
Normal file
40
src/config/models/nano-pro.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Nano Pro — 图片编辑
|
||||||
|
export default {
|
||||||
|
name: 'Nano Pro',
|
||||||
|
tag: '图片编辑',
|
||||||
|
inputType: 'image',
|
||||||
|
maxImages: 10,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'imageUrls',
|
||||||
|
label: '参考图片',
|
||||||
|
type: 'image',
|
||||||
|
required: true,
|
||||||
|
ui: 'imageUpload',
|
||||||
|
maxCount: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '编辑指令',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
ui: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'aspectRatio',
|
||||||
|
label: '比例',
|
||||||
|
type: 'select',
|
||||||
|
default: '1:1',
|
||||||
|
options: ['1:1', '3:2', '2:3', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'],
|
||||||
|
ui: 'proportion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resolution',
|
||||||
|
label: '分辨率',
|
||||||
|
type: 'select',
|
||||||
|
default: '2k',
|
||||||
|
options: ['1k', '2k', '4k'],
|
||||||
|
ui: 'resolution',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
46
src/config/models/qwen-edit.js
Normal file
46
src/config/models/qwen-edit.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// 通义万相 2.0 Pro — 图片编辑
|
||||||
|
export default {
|
||||||
|
name: '通义万相2.0 Pro',
|
||||||
|
tag: '图片编辑',
|
||||||
|
inputType: 'image',
|
||||||
|
maxImages: 3,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'imageUrls',
|
||||||
|
label: '参考图片',
|
||||||
|
type: 'image',
|
||||||
|
required: true,
|
||||||
|
ui: 'imageUpload',
|
||||||
|
maxCount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '提示词',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
ui: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'size',
|
||||||
|
label: '分辨率',
|
||||||
|
type: 'select',
|
||||||
|
default: '1024*1024',
|
||||||
|
options: [
|
||||||
|
'1024*1024', '1536*1536',
|
||||||
|
'768*1152', '1024*1536', '1152*768', '1536*1024',
|
||||||
|
'960*1280', '1080*1440', '1280*960', '1440*1080',
|
||||||
|
'720*1280', '1080*1920', '1280*720', '1920*1080',
|
||||||
|
'1344*576', '2048*872',
|
||||||
|
],
|
||||||
|
ui: 'select',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'imageNum',
|
||||||
|
label: '生成张数',
|
||||||
|
type: 'select',
|
||||||
|
default: 1,
|
||||||
|
options: [1, 2, 3, 4, 5, 6],
|
||||||
|
ui: 'quantity',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
38
src/config/models/qwen.js
Normal file
38
src/config/models/qwen.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// 通义万相 2.0 — 文生图
|
||||||
|
export default {
|
||||||
|
name: '通义万相2.0',
|
||||||
|
tag: '文生图',
|
||||||
|
inputType: 'text',
|
||||||
|
maxImages: 6,
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '提示词',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
ui: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'size',
|
||||||
|
label: '分辨率',
|
||||||
|
type: 'select',
|
||||||
|
default: '1024*1024',
|
||||||
|
options: [
|
||||||
|
'1024*1024', '1536*1536',
|
||||||
|
'768*1152', '1024*1536', '1152*768', '1536*1024',
|
||||||
|
'960*1280', '1080*1440', '1280*960', '1440*1080',
|
||||||
|
'720*1280', '1080*1920', '1280*720', '1920*1080',
|
||||||
|
'1344*576', '2048*872',
|
||||||
|
],
|
||||||
|
ui: 'select',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'imageNum',
|
||||||
|
label: '生成张数',
|
||||||
|
type: 'select',
|
||||||
|
default: 1,
|
||||||
|
options: [1, 2, 3, 4, 5, 6],
|
||||||
|
ui: 'quantity',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
31
src/config/models/z-image.js
Normal file
31
src/config/models/z-image.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Z-Image Turbo — 文生图
|
||||||
|
export default {
|
||||||
|
name: 'Z-Image Turbo',
|
||||||
|
tag: '文生图',
|
||||||
|
inputType: 'text',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
label: '提示词',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
ui: 'textarea',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'aspectRatio',
|
||||||
|
label: '比例',
|
||||||
|
type: 'select',
|
||||||
|
default: '1:1',
|
||||||
|
options: ['1:1', '3:4', '4:3', '9:16', '16:9', '2:3', '3:2'],
|
||||||
|
ui: 'proportion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'outputFormat',
|
||||||
|
label: '输出格式',
|
||||||
|
type: 'string',
|
||||||
|
default: 'png',
|
||||||
|
options: ['png', 'jpeg', 'webp(lossless)', 'webp(lossy)'],
|
||||||
|
ui: 'hidden',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@ -4,7 +4,6 @@ import {
|
|||||||
getUserInfo as getUserInfoApi,
|
getUserInfo as getUserInfoApi,
|
||||||
logout as logoutApi
|
logout as logoutApi
|
||||||
} from '@/apis/auth'
|
} from '@/apis/auth'
|
||||||
import { getFreeTimes } from '@/apis/display'
|
|
||||||
import { clearToken, getToken, setToken } from '@/utils/auth'
|
import { clearToken, getToken, setToken } from '@/utils/auth'
|
||||||
|
|
||||||
const storeSetup = () => {
|
const storeSetup = () => {
|
||||||
@ -34,7 +33,6 @@ const storeSetup = () => {
|
|||||||
|
|
||||||
const dept = ref({}) // 当前用户所在部门集合
|
const dept = ref({}) // 当前用户所在部门集合
|
||||||
const isLogin = ref(false)
|
const isLogin = ref(false)
|
||||||
const freeTimes = ref(0) // 免费次数
|
|
||||||
|
|
||||||
// 重置token
|
// 重置token
|
||||||
const resetToken = () => {
|
const resetToken = () => {
|
||||||
@ -45,49 +43,40 @@ const storeSetup = () => {
|
|||||||
// 检查token有效性
|
// 检查token有效性
|
||||||
const checkTokenValid = async () => {
|
const checkTokenValid = async () => {
|
||||||
const res = await checkUsertokenApi()
|
const res = await checkUsertokenApi()
|
||||||
console.log('checkTokenValid:', res) // 打印响应数据以进行调试
|
console.log('checkTokenValid:', res)
|
||||||
if (res.code === '401' || res.success === false) {
|
if (res.code === '401' || res.status === '401' || res.success === false) {
|
||||||
// 检查响应数据是否存在,以避免空响应导致的错误
|
console.error('Token is invalid:', res.message)
|
||||||
console.error('Token is invalid:', res.message)// 打印错误信息以进行调试
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
console.log('Token is valid') // 打印成功信息以进行调试
|
console.log('Token is valid')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const getInfo = async () => {
|
const getInfo = async () => {
|
||||||
const res = await getUserInfoApi()
|
const res = await getUserInfoApi()
|
||||||
Object.assign(userInfo, res.data)
|
// 兼容新旧格式:新格式 data.userInfo 嵌套,旧格式 data 扁平
|
||||||
// userInfo.avatar = getAvatar(res.data.avatar, res.data.gender)
|
const u = res.data.userInfo || res.data
|
||||||
userInfo.username = res.data.username
|
Object.assign(userInfo, u)
|
||||||
if (typeof res.data.routers === 'string' && res.data.routers.trim() !== '') {
|
userInfo.id = u.userId || u.id
|
||||||
userInfo.routers = res.data.routers.split(',').map((item) => item.trim()) // 补充trim处理更完善
|
userInfo.username = u.userName || u.username
|
||||||
|
if (typeof u.routers === 'string' && u.routers.trim() !== '') {
|
||||||
|
userInfo.routers = u.routers.split(',').map((item) => item.trim())
|
||||||
} else {
|
} else {
|
||||||
userInfo.routers = []
|
userInfo.routers = []
|
||||||
}
|
}
|
||||||
if (res.data.roles && res.data.roles.length) {
|
// 角色和权限在 data 层级(非 userInfo 内)
|
||||||
roles.value = res.data.roles
|
const roleList = res.data.roles || u.roles
|
||||||
permissions.value = res.data.permissions
|
if (roleList?.length) {
|
||||||
|
roles.value = roleList
|
||||||
|
permissions.value = res.data.permissions || u.permissions || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取免费次数
|
|
||||||
const fetchFreeTimes = async (chargeType = 1) => {
|
|
||||||
if (userInfo.id) {
|
|
||||||
const res = await getFreeTimes(userInfo.id)
|
|
||||||
const balanceList = res.data || []
|
|
||||||
const target = balanceList.find((item) => item.chargeType === chargeType)
|
|
||||||
freeTimes.value = target?.balance || 0
|
|
||||||
return freeTimes.value
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
const accountLogin = async (req) => {
|
const accountLogin = async (req) => {
|
||||||
const res = await accountLoginApi(req)
|
const res = await accountLoginApi(req)
|
||||||
if (res.data == null || res.code === '500' || res.success === false) {
|
if (res.data == null || res.code === '500' || res.status === 500 || res.success === false) {
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
ElMessage({
|
ElMessage({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
@ -134,14 +123,12 @@ const storeSetup = () => {
|
|||||||
dept,
|
dept,
|
||||||
username,
|
username,
|
||||||
isLogin,
|
isLogin,
|
||||||
freeTimes,
|
|
||||||
accountLogin,
|
accountLogin,
|
||||||
logout,
|
logout,
|
||||||
logoutCallBack,
|
logoutCallBack,
|
||||||
getInfo,
|
getInfo,
|
||||||
resetToken,
|
resetToken,
|
||||||
checkTokenValid,
|
checkTokenValid
|
||||||
fetchFreeTimes
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,15 @@
|
|||||||
import outPlatform from '@/config/index'
|
import outPlatform from '@/config/index'
|
||||||
|
|
||||||
// 处理音频生成任务的数据并返回
|
// 构造任务 body
|
||||||
export async function createTask(data, taskId, token) {
|
export async function createTask(data) {
|
||||||
console.log(data)
|
// Painting 使用新架构:直接使用动态模型参数
|
||||||
const payload = await outPlatform[data.platform].Playload(data)
|
if (data.type === 'Painting') {
|
||||||
|
return data.modelParams || {}
|
||||||
return {
|
|
||||||
AIGC: data.AIGC,
|
|
||||||
platform: data.platform,
|
|
||||||
taskType: data.modelType === 'text' ? 1 : 2,
|
|
||||||
modelName: data.modelName,
|
|
||||||
payload,
|
|
||||||
taskId,
|
|
||||||
token,
|
|
||||||
quantity: data.quantity,
|
|
||||||
free: data.free,
|
|
||||||
result: data.result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Video 继续使用旧 workflow 适配器
|
||||||
|
const payload = await outPlatform[data.platform].Playload(data)
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取结果
|
// 获取结果
|
||||||
@ -26,4 +19,4 @@ export async function getTask(result) {
|
|||||||
return { type: true, urls: urls }
|
return { type: true, urls: urls }
|
||||||
}
|
}
|
||||||
return { type: false, message: result.data.exception_message || '生成失败' }
|
return { type: false, message: result.data.exception_message || '生成失败' }
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/utils/modelApi.js
Normal file
110
src/utils/modelApi.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { fetchPlatformModels as fetchModelsRaw } from '@/apis/display'
|
||||||
|
|
||||||
|
const CACHE_PREFIX = 'platform_models_'
|
||||||
|
const CACHE_TTL = 30 * 1000 // 30秒有效期
|
||||||
|
|
||||||
|
function getCacheKey(code) {
|
||||||
|
return `${CACHE_PREFIX}${code}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFromCache(code) {
|
||||||
|
try {
|
||||||
|
const key = getCacheKey(code)
|
||||||
|
const stored = localStorage.getItem(key)
|
||||||
|
if (!stored) return null
|
||||||
|
|
||||||
|
const data = JSON.parse(stored)
|
||||||
|
// 清理旧格式缓存(无 timestamp 字段)或过期缓存
|
||||||
|
if (!data.timestamp || Date.now() - data.timestamp > CACHE_TTL) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return data.models
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveToCache(code, models) {
|
||||||
|
try {
|
||||||
|
const key = getCacheKey(code)
|
||||||
|
localStorage.setItem(key, JSON.stringify({
|
||||||
|
models,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存模型列表缓存失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型 → 平台编码映射
|
||||||
|
export function getPlatformCode(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'Painting':
|
||||||
|
return 'ai_painting_talk'
|
||||||
|
case 'Video':
|
||||||
|
return 'ai_video_talk'
|
||||||
|
default:
|
||||||
|
return 'ai_painting_talk'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingRequests = new Map() // 并发请求去重
|
||||||
|
|
||||||
|
// 获取平台模型列表(localStorage 缓存,30秒有效)
|
||||||
|
export async function fetchPlatformModels(code) {
|
||||||
|
const cached = getFromCache(code)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已有进行中的请求则复用,避免并发重复请求
|
||||||
|
if (pendingRequests.has(code)) {
|
||||||
|
return pendingRequests.get(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
try {
|
||||||
|
const result = await fetchModelsRaw(code)
|
||||||
|
|
||||||
|
if (result.code === 0 && result.data?.models) {
|
||||||
|
saveToCache(code, result.data.models)
|
||||||
|
return result.data.models
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('获取模型列表失败:', result.message)
|
||||||
|
return []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取模型列表失败:', error)
|
||||||
|
return []
|
||||||
|
} finally {
|
||||||
|
pendingRequests.delete(code)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
pendingRequests.set(code, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据模型名称查找 model_id
|
||||||
|
export async function getModelId(type, modelName) {
|
||||||
|
if (!modelName) return ''
|
||||||
|
|
||||||
|
const code = getPlatformCode(type)
|
||||||
|
const models = await fetchPlatformModels(code)
|
||||||
|
|
||||||
|
const found = models.find(m => m.name === modelName || m.display_name === modelName)
|
||||||
|
return found?.id || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有平台模型缓存
|
||||||
|
export function clearPlatformModelCache() {
|
||||||
|
const keysToRemove = []
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i)
|
||||||
|
if (key && key.startsWith(CACHE_PREFIX)) {
|
||||||
|
keysToRemove.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysToRemove.forEach(key => localStorage.removeItem(key))
|
||||||
|
}
|
||||||
@ -29,21 +29,19 @@ const StatusCodeMessage = {
|
|||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (token) {
|
if (!config.headers) {
|
||||||
if (!config.headers) {
|
config.headers = {}
|
||||||
config.headers = {}
|
|
||||||
}
|
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
|
||||||
}
|
}
|
||||||
// console.log(config.baseURL)
|
|
||||||
if (config.url?.startsWith(import.meta.env.VITE_API_PAY_PREFIX)) { // 支付服务路由
|
// 统一 Auth 头不带 Bearer 前缀
|
||||||
|
if (token) config.headers.Authorization = token
|
||||||
|
|
||||||
|
if (config.url?.startsWith(import.meta.env.VITE_API_TASK_PREFIX)) { // 算力调度后端
|
||||||
|
config.baseURL = import.meta.env.VITE_API_TASK_TARGET
|
||||||
|
} else if (config.url?.startsWith(import.meta.env.VITE_API_PAY_PREFIX)) { // 支付服务路由
|
||||||
config.baseURL = import.meta.env.VITE_API_PAY_TARGET
|
config.baseURL = import.meta.env.VITE_API_PAY_TARGET
|
||||||
} else if (config.url?.startsWith(import.meta.env.VITE_API_AIGC_PREFIX)) { // 资源服务路由
|
} else if (config.url?.startsWith(import.meta.env.VITE_API_AIGC_PREFIX)) { // 资源服务路由
|
||||||
// config.url = config.url.replace(import.meta.env.VITE_API_AIGC_PREFIX, '')
|
|
||||||
config.baseURL = import.meta.env.VITE_API_AIGC_TARGET
|
config.baseURL = import.meta.env.VITE_API_AIGC_TARGET
|
||||||
} else if (config.url?.startsWith(import.meta.env.VITE_API_MUSIC_WORKFLOW_PREFIX)) { // 音频生成平台工作流服务路由
|
|
||||||
config.url = config.url.replace(import.meta.env.VITE_API_MUSIC_WORKFLOW_PREFIX, '')
|
|
||||||
config.baseURL = import.meta.env.VITE_API_MUSIC_WORKFLOW_TARGET
|
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
@ -57,11 +55,11 @@ service.interceptors.request.use(
|
|||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
const { data } = response
|
const { data } = response
|
||||||
const { success, code, msg } = data
|
const { success, code, status, msg, message } = data
|
||||||
if (success || code === 0) {
|
if (success || code === 0 || status === 0) {
|
||||||
console.log('msg: \n', msg)
|
console.log('msg: \n', msg)
|
||||||
return response.data
|
return response.data
|
||||||
} else if (code === 401 && response.config.url !== '/auth/check/token`') { // 判断code=401时进行页面刷新,但是不对检验token这个路由的请求判断,防止出现死循环
|
} else if (code === 401 && response.config.url !== '/login/validateToken`') { // 判断code=401时进行页面刷新,但是不对检验token这个路由的请求判断,防止出现死循环
|
||||||
userError()
|
userError()
|
||||||
}
|
}
|
||||||
console.log('CodeMessage: \n', StatusCodeMessage[code])
|
console.log('CodeMessage: \n', StatusCodeMessage[code])
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { ElNotification } from 'element-plus'
|
import { ElNotification } from 'element-plus'
|
||||||
import { h, ref } from 'vue'
|
import { h } from 'vue'
|
||||||
import { useDisplayStore, useUserStore } from '@/stores'
|
import { useDisplayStore, useUserStore } from '@/stores'
|
||||||
import { getToken } from '@/utils/auth'
|
import { createTask } from '@/utils/createTask'
|
||||||
import { createTask, getTask } from '@/utils/createTask'
|
|
||||||
import { userError } from '@/utils/tokenError'
|
import { userError } from '@/utils/tokenError'
|
||||||
|
import { requestCreateTask, requestTaskStatus } from '@/apis/display'
|
||||||
|
|
||||||
export function getChargeType(chargeType) {
|
export function getChargeType(chargeType) {
|
||||||
switch (chargeType) {
|
switch (chargeType) {
|
||||||
@ -23,13 +23,13 @@ export function websocketError(code, msg) {
|
|||||||
message = '用户身份验证失败'
|
message = '用户身份验证失败'
|
||||||
userError()
|
userError()
|
||||||
break
|
break
|
||||||
case 4401: // 后端返回常规错误
|
case 4401:
|
||||||
message = msg
|
message = msg
|
||||||
break
|
break
|
||||||
case 4402: // 后端返回外部平台提交时的错误
|
case 4402:
|
||||||
message = JSON.parse(msg)
|
message = JSON.parse(msg)
|
||||||
break
|
break
|
||||||
case 4403: // 外部平台的任务结果的错误
|
case 4403:
|
||||||
message = msg
|
message = msg
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@ -38,15 +38,13 @@ export function websocketError(code, msg) {
|
|||||||
|
|
||||||
ElNotification({
|
ElNotification({
|
||||||
title: '生成失败',
|
title: '生成失败',
|
||||||
|
|
||||||
message: h('i', { style: 'color: teal' }, message),
|
message: h('i', { style: 'color: teal' }, message),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
duration: 6000 // 增加持续时间以适应更多信息
|
duration: 6000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function websocketSuccess() {
|
export function websocketSuccess() {
|
||||||
// 合并两个通知为一个
|
|
||||||
ElNotification({
|
ElNotification({
|
||||||
title: '生成成功',
|
title: '生成成功',
|
||||||
message: h('div', [
|
message: h('div', [
|
||||||
@ -55,135 +53,140 @@ export function websocketSuccess() {
|
|||||||
h('div', { style: 'color: orange; margin-top: 5px;' }, '内测状态,请及时下载生成的文件,云端储存与历史记录保留24小时!')
|
h('div', { style: 'color: orange; margin-top: 5px;' }, '内测状态,请及时下载生成的文件,云端储存与历史记录保留24小时!')
|
||||||
]),
|
]),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
duration: 6000 // 增加持续时间以适应更多信息
|
duration: 6000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 当前活跃的轮询定时器集合,用于页面卸载时清理
|
||||||
|
const activePollIntervals = new Set()
|
||||||
|
|
||||||
export async function generate(data, generateData) {
|
export async function generate(data, generateData) {
|
||||||
const progress_text = ref('')
|
|
||||||
const message = ref('')
|
|
||||||
const useDisplay = useDisplayStore()
|
const useDisplay = useDisplayStore()
|
||||||
const token = getToken()
|
let taskId = null
|
||||||
const taskId = crypto.randomUUID()
|
let pollInterval = null
|
||||||
let currentTaskId = null
|
|
||||||
|
if (!data.modelId) {
|
||||||
|
ElNotification({
|
||||||
|
title: '生成失败',
|
||||||
|
message: h('i', { style: 'color: teal' }, '未找到模型ID,请联系管理员配置'),
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
useDisplay.isSubGerenate = true
|
useDisplay.isSubGerenate = true
|
||||||
|
|
||||||
const result = await createTask(data, taskId, token)
|
// 从登录态获取 sessionId
|
||||||
console.log(result)
|
const sessionId = useUserStore().userInfo.sessionId
|
||||||
// const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0'
|
if (!sessionId) {
|
||||||
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=Bearer ${token}`
|
ElNotification({
|
||||||
const socket = new WebSocket(wsURL)
|
title: '生成失败',
|
||||||
console.log('WebSocket连接已建立')
|
message: h('i', { style: 'color: teal' }, '用户身份已过期,请重新登录'),
|
||||||
|
type: 'error'
|
||||||
// 心跳机制相关变量
|
})
|
||||||
let heartbeatInterval = null
|
useDisplay.isSubGerenate = false
|
||||||
const heartbeatIntervalTime = 20000 // 30秒发送一次心跳
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 接收服务器消息
|
// 通过 createTask 获取 body 内容(RunningHub workflow payload)
|
||||||
socket.onmessage = async (event) => {
|
const body = await createTask(data)
|
||||||
// 处理pong响应
|
|
||||||
if (event.data === 'pong') {
|
// 构造请求体
|
||||||
console.log('收到心跳响应')
|
const requestBody = {
|
||||||
return
|
model_id: data.modelId,
|
||||||
} else if (event.data === 'please give me taskId') {
|
body,
|
||||||
socket.send(`setTaskId:${taskId}`)
|
request: data.request
|
||||||
progress_text.value = '信息提交中...'
|
|
||||||
return
|
|
||||||
} else if (event.data === 'OK! Please continue. ') {
|
|
||||||
socket.send(JSON.stringify({
|
|
||||||
type: 'generate',
|
|
||||||
data: result
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
} else if (event.data === '任务提交成功,正在排队中...') {
|
|
||||||
progress_text.value = '视频生成中...'
|
|
||||||
currentTaskId = taskId
|
|
||||||
|
|
||||||
useDisplay.addGeneratingItem({
|
|
||||||
taskId: taskId,
|
|
||||||
type: data.type,
|
|
||||||
generateData: generateData
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
useDisplay.scrollToBottom()
|
|
||||||
}, 100)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
message.value = event.data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理链接错误
|
// POST 创建任务
|
||||||
socket.onerror = (error) => {
|
const createResult = await requestCreateTask(requestBody, sessionId)
|
||||||
console.error('WebSocket链接出错:', error)
|
|
||||||
|
|
||||||
// 清理心跳定时器
|
if (createResult.code !== 0) {
|
||||||
if (heartbeatInterval) {
|
|
||||||
clearInterval(heartbeatInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
ElNotification({
|
ElNotification({
|
||||||
title: '生成通知',
|
title: '生成失败',
|
||||||
// eslint-disable-next-line no-undef
|
message: h('i', { style: 'color: teal' }, createResult.message || '任务创建失败'),
|
||||||
message: h('i', { style: 'color: teal' }, '生成视频失败'),
|
|
||||||
type: 'error'
|
type: 'error'
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// 处理链接关闭
|
|
||||||
socket.onclose = async (event) => {
|
|
||||||
console.log('WebSocket已关闭:', event)
|
|
||||||
useDisplay.isSubGerenate = false
|
useDisplay.isSubGerenate = false
|
||||||
if (heartbeatInterval) {
|
return
|
||||||
clearInterval(heartbeatInterval)
|
}
|
||||||
}
|
|
||||||
const res = JSON.parse(message.value)
|
taskId = createResult.data.task_id
|
||||||
if (event.code === 1006) {
|
|
||||||
console.error('用户身份验证失败')
|
// 在列表中插入"生成中"条目
|
||||||
userError()
|
useDisplay.addGeneratingItem({
|
||||||
} else if (event.code === 1000 && event.reason === 'success') {
|
taskId,
|
||||||
console.log('收到服务器消息:', res)
|
type: data.type,
|
||||||
const result = await getTask(res)
|
generateData
|
||||||
if(useUserStore().freeTimes) await useUserStore().fetchFreeTimes()
|
})
|
||||||
if (result.type) {
|
setTimeout(() => {
|
||||||
if (currentTaskId) {
|
useDisplay.scrollToBottom()
|
||||||
useDisplay.updateItemToSuccess(currentTaskId, result.urls)
|
}, 100)
|
||||||
|
|
||||||
|
// 轮询任务状态
|
||||||
|
const pollTask = async () => {
|
||||||
|
try {
|
||||||
|
const pollResult = await requestTaskStatus(taskId)
|
||||||
|
|
||||||
|
if (pollResult.code !== 0) return
|
||||||
|
|
||||||
|
const taskData = pollResult.data
|
||||||
|
|
||||||
|
if (taskData.status === 'completed') {
|
||||||
|
clearInterval(pollInterval)
|
||||||
|
activePollIntervals.delete(pollInterval)
|
||||||
|
useDisplay.isSubGerenate = false
|
||||||
|
|
||||||
|
// 提取结果 URL
|
||||||
|
const urls = taskData.outputs?.map(img => img.url) || []
|
||||||
|
if (urls.length > 0) {
|
||||||
|
useDisplay.updateItemToSuccess(taskId, urls)
|
||||||
|
websocketSuccess()
|
||||||
|
} else {
|
||||||
|
websocketError(4403, '未获取到生成结果')
|
||||||
}
|
}
|
||||||
|
} else if (taskData.status === 'failed') {
|
||||||
websocketSuccess()
|
clearInterval(pollInterval)
|
||||||
} else {
|
activePollIntervals.delete(pollInterval)
|
||||||
websocketError(4403, result.message)
|
useDisplay.isSubGerenate = false
|
||||||
|
websocketError(4403, taskData.vendor_error || '生成失败')
|
||||||
}
|
}
|
||||||
} else {
|
// queued / processing 状态继续轮询
|
||||||
websocketError(event.code, event.reason)
|
} catch (error) {
|
||||||
}
|
console.error('轮询任务状态失败:', error)
|
||||||
if (heartbeatInterval) {
|
|
||||||
clearInterval(heartbeatInterval)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待 WebSocket 连接打开
|
// 每 20 秒轮询一次
|
||||||
socket.onopen = () => {
|
pollInterval = setInterval(pollTask, 20000)
|
||||||
console.log('WebSocket连接已建立')
|
activePollIntervals.add(pollInterval)
|
||||||
|
|
||||||
|
// 5 秒后先做第一次轮询
|
||||||
|
setTimeout(pollTask, 5000)
|
||||||
|
|
||||||
// 启动心跳机制
|
|
||||||
heartbeatInterval = setInterval(() => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
socket.send('ping')
|
|
||||||
console.log('发送心跳包')
|
|
||||||
}
|
|
||||||
}, heartbeatIntervalTime)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error creating AI3D_file:', error)
|
console.error('创建任务失败:', error)
|
||||||
// eslint-disable-next-line no-undef
|
useDisplay.isSubGerenate = false
|
||||||
ElNotification({
|
ElNotification({
|
||||||
title: '生成通知',
|
title: '生成通知',
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
message: h('i', { style: 'color: teal' }, '生成失败,请检查参数后重新提交任务'),
|
message: h('i', { style: 'color: teal' }, '生成失败,请检查参数后重新提交任务'),
|
||||||
type: 'error'
|
type: 'error'
|
||||||
})
|
})
|
||||||
|
if (pollInterval) {
|
||||||
|
clearInterval(pollInterval)
|
||||||
|
activePollIntervals.delete(pollInterval)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面卸载时清理所有轮询
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
for (const interval of activePollIntervals) {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
activePollIntervals.clear()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -73,9 +73,10 @@ import RefreshOverlay from './components/RefreshOverlay.vue'
|
|||||||
import Select from '@/components/Select/index.vue'
|
import Select from '@/components/Select/index.vue'
|
||||||
import { VirtualScroller } from '@/components/virtual-scroller'
|
import { VirtualScroller } from '@/components/virtual-scroller'
|
||||||
import Canvas from '@/components/canvas/index.vue'
|
import Canvas from '@/components/canvas/index.vue'
|
||||||
import { getGenerateHistoryList } from '@/apis/display'
|
import { requestTaskHistory } from '@/apis/display'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { getChargeType } from '@/utils/websocket'
|
import { getChargeType } from '@/utils/websocket'
|
||||||
|
import { getPlatformCode } from '@/utils/modelApi'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
if: {
|
if: {
|
||||||
@ -163,12 +164,11 @@ const fetchHistory = async (isLoadMore = false) => {
|
|||||||
try {
|
try {
|
||||||
const pageToFetch = isLoadMore ? currentPage.value + 1 : 1
|
const pageToFetch = isLoadMore ? currentPage.value + 1 : 1
|
||||||
|
|
||||||
const result = await getGenerateHistoryList({
|
const result = await requestTaskHistory({
|
||||||
userId: userStore.userInfo.id,
|
user_id: userStore.userInfo.id,
|
||||||
chargeType: chargeType.value,
|
platform_code: getPlatformCode(props.type),
|
||||||
page: pageToFetch,
|
page: pageToFetch,
|
||||||
size: 10,
|
pageSize: 10
|
||||||
sort: 'createTime,desc'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const dataList = result.data?.list || result.data || []
|
const dataList = result.data?.list || result.data || []
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user