提交最后一次稳定版,下个提交准备修改任务请求连接到新版算力转发后端
This commit is contained in:
parent
5e4fc0a1d1
commit
6e67acca66
78
CLAUDE.md
Normal file
78
CLAUDE.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev # 启动 Vite 开发服务器
|
||||||
|
pnpm build # 生产构建
|
||||||
|
pnpm preview # 预览生产构建
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + Less + pnpm
|
||||||
|
|
||||||
|
## 架构概览
|
||||||
|
|
||||||
|
这是一个 AI 绘画/视频生成的前端操作平台,通过 WebSocket 连接后端和第三方 AI 平台(RunningHub)提交生成任务并接收结果。
|
||||||
|
|
||||||
|
### 关键目录
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/VueVirtualScroller
|
||||||
|
├── router/index.js # 路由定义 + token 验证守卫
|
||||||
|
├── stores/ # Pinia 状态管理
|
||||||
|
│ ├── user.js # 用户认证、信息、免费次数
|
||||||
|
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
|
||||||
|
│ └── param.js # 占位 store(当前为空)
|
||||||
|
├── apis/ # HTTP API 模块
|
||||||
|
│ ├── auth/ # 登录/登出/用户信息/验证码
|
||||||
|
│ └── display/ # 获取历史列表/收藏/删除
|
||||||
|
├── components/ # 通用组件
|
||||||
|
│ ├── dialogBox/ # 生成参数输入面板(核心交互入口),含模型选择、比例、上传等子组件
|
||||||
|
│ ├── virtual-scroller/# 虚拟滚动列表组件(自定义实现)
|
||||||
|
│ ├── canvas/ # 图片画布编辑(圆/矩形选区,局部重绘)
|
||||||
|
│ └── ...
|
||||||
|
├── views/ # 页面
|
||||||
|
│ ├── home/index.vue # 主页面容器(dialogBox + display)
|
||||||
|
│ ├── home/display/ # 历史记录展示区
|
||||||
|
│ └── login/ # 登录页(跳转外部登录)
|
||||||
|
├── utils/
|
||||||
|
│ ├── request.js # Axios 实例,拦截器处理 token 和不同服务的 baseURL 路由
|
||||||
|
│ ├── websocket.js # WebSocket 生成任务的核心流程(心跳、提交流程、结果处理)
|
||||||
|
│ ├── createTask.js # 根据配置构造任务 payload
|
||||||
|
│ ├── modelConfig.js # 从远程 JSON 加载模型配置,localStorage 每日缓存
|
||||||
|
│ ├── auth.ts # token 存取工具(localStorage)
|
||||||
|
│ └── encrypt.ts # 加密工具(Base64/MD5/RSA/AES)
|
||||||
|
├── config/
|
||||||
|
│ ├── index.js # 平台配置入口
|
||||||
|
│ └── runninghub/ # RunningHub 平台适配器:Payload 构造和 Result 解析
|
||||||
|
└── config/ # 项目根目录下
|
||||||
|
└── plugins.js # Vite 插件配置(自动导入/组件注册/图标)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心数据流
|
||||||
|
|
||||||
|
1. 用户在 `dialogBox` 中设置参数(模型、提示词、比例、上传图片等)
|
||||||
|
2. 点击生成 → `websocket.js:generate()` 被调用
|
||||||
|
3. 先通过 `createTask.js` 调用 `config/runninghub` 的 `Playload()` 构造任务数据(从远程 JSON 加载 workflow 配置)
|
||||||
|
4. 建立 WebSocket 连接,经过握手协议(`please give me taskId` → `OK! Please continue.`)提交任务
|
||||||
|
5. 任务排队中 → `displayStore.addGeneratingItem()` 在前端列表中插入 "生成中" 条目
|
||||||
|
6. 完成后 WebSocket 关闭(code=1000 reason=success) → `getTask()` 解析结果 URL → `updateItemToSuccess()` 更新列表
|
||||||
|
|
||||||
|
### 自动导入
|
||||||
|
|
||||||
|
- `unplugin-auto-import` 自动导入 Vue/VRouter/Pinia API,无需在 `.vue` 文件中手动 `import { ref, computed, watch } from 'vue'`
|
||||||
|
- `unplugin-vue-components` 自动注册 `src/components/` 下的组件和 Element Plus 组件
|
||||||
|
- 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。
|
||||||
@ -545,96 +545,6 @@ const handleClose = () => {
|
|||||||
promptHistoryIndex.value = -1
|
promptHistoryIndex.value = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSend = async () => {
|
|
||||||
if (isSending.value) return
|
|
||||||
if (!inputText.value) {
|
|
||||||
ElMessage.error('请输入提示词')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isSending.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const canvas = canvasRef.value
|
|
||||||
const imageData = canvas.toDataURL('image/png')
|
|
||||||
|
|
||||||
const imgs = []
|
|
||||||
if (imageData) {
|
|
||||||
imgs.push({ name: 'image_1', url: imageData })
|
|
||||||
}
|
|
||||||
|
|
||||||
allReferenceImages.value.forEach((img, index) => {
|
|
||||||
imgs.push({ name: `image_${index + 2}`, url: img })
|
|
||||||
})
|
|
||||||
|
|
||||||
const uploadImg = async (imgItem) => {
|
|
||||||
if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) {
|
|
||||||
return imgItem
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(imgItem.url)
|
|
||||||
const blob = await response.blob()
|
|
||||||
const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' })
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await request({
|
|
||||||
url: import.meta.env.VITE_API_WORKFLOW_UPLOAD,
|
|
||||||
method: 'POST',
|
|
||||||
data: formData,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (result.success || result.code === 0) {
|
|
||||||
return { name: imgItem.name, url: result.url }
|
|
||||||
}
|
|
||||||
return imgItem
|
|
||||||
} catch (error) {
|
|
||||||
console.error('上传失败:', error)
|
|
||||||
return imgItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadedImgs = await Promise.all(imgs.map(uploadImg))
|
|
||||||
|
|
||||||
const generateData = {
|
|
||||||
model: 'banana',
|
|
||||||
modelType: 'edit',
|
|
||||||
prompt: inputText.value,
|
|
||||||
proportion: '比例自动'
|
|
||||||
}
|
|
||||||
const data = {
|
|
||||||
type: props.type,
|
|
||||||
modelType: 'edit',
|
|
||||||
AIGC: 'Painting',
|
|
||||||
platform: 'runninghub',
|
|
||||||
modelName: 'banana',
|
|
||||||
quantity: 1,
|
|
||||||
free: useUserStore().freeTimes,
|
|
||||||
params: [
|
|
||||||
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' },
|
|
||||||
{ name: 'index', data: 1 },
|
|
||||||
],
|
|
||||||
imgs: uploadedImgs,
|
|
||||||
result: JSON.stringify(generateData)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('send', {
|
|
||||||
image: imageData,
|
|
||||||
text: inputText.value,
|
|
||||||
shapes: shapes.value
|
|
||||||
})
|
|
||||||
|
|
||||||
await generate(data, generateData)
|
|
||||||
handleClose()
|
|
||||||
} finally {
|
|
||||||
isSending.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeReferenceImage = (index) => {
|
const removeReferenceImage = (index) => {
|
||||||
allReferenceImages.value.splice(index, 1)
|
allReferenceImages.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
@ -719,6 +629,137 @@ const handleBrushConfirm = () => {
|
|||||||
selectedReferenceImages.value = []
|
selectedReferenceImages.value = []
|
||||||
currentEditingContent.value = ''
|
currentEditingContent.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getImageAspectRatio = () => {
|
||||||
|
if (!bgImage.value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = bgImage.value.width
|
||||||
|
const height = bgImage.value.height
|
||||||
|
|
||||||
|
const aspectRatios = [
|
||||||
|
{ ratio: '4:3', value: 4 / 3 },
|
||||||
|
{ ratio: '16:9', value: 16 / 9 },
|
||||||
|
{ ratio: '9:16', value: 9 / 16 },
|
||||||
|
{ ratio: '3:4', value: 3 / 4 },
|
||||||
|
{ ratio: '1:1', value: 1 / 1 },
|
||||||
|
{ ratio: '2:3', value: 2 / 3 },
|
||||||
|
{ ratio: '3:2', value: 3 / 2 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentRatio = width / height
|
||||||
|
|
||||||
|
let closest = aspectRatios[0]
|
||||||
|
let minDiff = Math.abs(currentRatio - closest.value)
|
||||||
|
|
||||||
|
for (const item of aspectRatios) {
|
||||||
|
const diff = Math.abs(currentRatio - item.value)
|
||||||
|
if (diff < minDiff) {
|
||||||
|
minDiff = diff
|
||||||
|
closest = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
aspectRatio: closest.ratio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (isSending.value) return
|
||||||
|
if (!inputText.value) {
|
||||||
|
ElMessage.error('请输入提示词')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSending.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
const imageData = canvas.toDataURL('image/png')
|
||||||
|
|
||||||
|
const imgs = []
|
||||||
|
if (imageData) {
|
||||||
|
imgs.push({ name: 'image_1', url: imageData })
|
||||||
|
}
|
||||||
|
|
||||||
|
allReferenceImages.value.forEach((img, index) => {
|
||||||
|
imgs.push({ name: `image_${index + 2}`, url: img })
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadImg = async (imgItem) => {
|
||||||
|
if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) {
|
||||||
|
return imgItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(imgItem.url)
|
||||||
|
const blob = await response.blob()
|
||||||
|
const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' })
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await request({
|
||||||
|
url: import.meta.env.VITE_API_WORKFLOW_UPLOAD,
|
||||||
|
method: 'POST',
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (result.success || result.code === 0) {
|
||||||
|
return { name: imgItem.name, url: result.url }
|
||||||
|
}
|
||||||
|
return imgItem
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error)
|
||||||
|
return imgItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadedImgs = await Promise.all(imgs.map(uploadImg))
|
||||||
|
|
||||||
|
const proportion = getImageAspectRatio()
|
||||||
|
|
||||||
|
const generateData = {
|
||||||
|
model: 'GPT-image2.0',
|
||||||
|
modelType: 'edit',
|
||||||
|
prompt: inputText.value,
|
||||||
|
proportion: '比例自动'
|
||||||
|
}
|
||||||
|
const data = {
|
||||||
|
type: props.type,
|
||||||
|
modelType: 'edit',
|
||||||
|
AIGC: 'Painting',
|
||||||
|
platform: 'runninghub',
|
||||||
|
modelName: 'GPT',
|
||||||
|
quantity: 1,
|
||||||
|
free: useUserStore().freeTimes,
|
||||||
|
params: [
|
||||||
|
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' },
|
||||||
|
{ name: 'index', data: 1 },
|
||||||
|
{ name: 'proportion', data: proportion?.aspectRatio || '4:3' },
|
||||||
|
],
|
||||||
|
imgs: uploadedImgs,
|
||||||
|
result: JSON.stringify(generateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('send', {
|
||||||
|
image: imageData,
|
||||||
|
text: inputText.value,
|
||||||
|
shapes: shapes.value
|
||||||
|
})
|
||||||
|
|
||||||
|
await generate(data, generateData)
|
||||||
|
handleClose()
|
||||||
|
} finally {
|
||||||
|
isSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
@ -357,6 +357,8 @@ const handleWheel = (event) => {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollCleanupTimeout = ref(null)
|
||||||
|
|
||||||
const handleScroll = (event) => {
|
const handleScroll = (event) => {
|
||||||
const target = event.target
|
const target = event.target
|
||||||
scrollTop.value = target.scrollTop
|
scrollTop.value = target.scrollTop
|
||||||
@ -370,6 +372,14 @@ const handleScroll = (event) => {
|
|||||||
isScrolling.value = false
|
isScrolling.value = false
|
||||||
}, 150)
|
}, 150)
|
||||||
|
|
||||||
|
// 滚动时添加防抖清理,每100ms最多执行一次
|
||||||
|
if (scrollCleanupTimeout.value) {
|
||||||
|
clearTimeout(scrollCleanupTimeout.value)
|
||||||
|
}
|
||||||
|
scrollCleanupTimeout.value = setTimeout(() => {
|
||||||
|
cleanupExtraItems(visibleItems.value)
|
||||||
|
}, 300)
|
||||||
|
|
||||||
const st = target.scrollTop
|
const st = target.scrollTop
|
||||||
const scrollHeight = target.scrollHeight
|
const scrollHeight = target.scrollHeight
|
||||||
const clientHeight = target.clientHeight
|
const clientHeight = target.clientHeight
|
||||||
@ -515,40 +525,40 @@ watch(visibleItems, (newItems) => {
|
|||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
const cleanupExtraItems = (visibleItems) => {
|
const cleanupExtraItems = (currentVisibleItems) => {
|
||||||
if (!itemRefs.size || !visibleItems.length) return
|
if (!renderContainerRef.value || !currentVisibleItems.length) return
|
||||||
|
|
||||||
const visibleIndices = new Set(visibleItems.map(item => item.index))
|
// 构建当前应该可见的索引集合
|
||||||
const existingIndices = Array.from(itemRefs.keys()).sort((a, b) => a - b)
|
const visibleIndices = new Set(currentVisibleItems.map(item => item.index))
|
||||||
|
|
||||||
|
// 直接获取 render-container 内所有实际渲染的 .virtual-scroller-item 元素
|
||||||
|
const renderedItems = renderContainerRef.value.querySelectorAll('.virtual-scroller-item')
|
||||||
|
|
||||||
const toRemove = []
|
const toRemove = []
|
||||||
let lastValidIndex = -1
|
|
||||||
|
|
||||||
for (const index of existingIndices) {
|
for (const el of renderedItems) {
|
||||||
if (visibleIndices.has(index)) {
|
const dataIndex = parseInt(el.getAttribute('data-index'), 10)
|
||||||
if (lastValidIndex !== -1 && index !== lastValidIndex + 1) {
|
|
||||||
for (let i = lastValidIndex + 1; i < index; i++) {
|
// 如果元素的 data-index 不在可见范围内,标记为删除
|
||||||
if (itemRefs.has(i)) {
|
if (!isNaN(dataIndex) && !visibleIndices.has(dataIndex)) {
|
||||||
toRemove.push(i)
|
toRemove.push(el)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastValidIndex = index
|
|
||||||
} else {
|
|
||||||
toRemove.push(index)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const index of toRemove) {
|
// 从 DOM 中删除多余元素
|
||||||
const element = itemRefs.get(index)
|
for (const el of toRemove) {
|
||||||
if (element && element.parentNode) {
|
if (el.parentNode) {
|
||||||
element.parentNode.removeChild(element)
|
el.parentNode.removeChild(el)
|
||||||
|
}
|
||||||
|
// 同步清理 itemRefs Map
|
||||||
|
const index = parseInt(el.getAttribute('data-index'), 10)
|
||||||
|
if (!isNaN(index)) {
|
||||||
|
itemRefs.delete(index)
|
||||||
}
|
}
|
||||||
itemRefs.delete(index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toRemove.length > 0) {
|
if (toRemove.length > 0) {
|
||||||
console.log(`清理了 ${toRemove.length} 个多余项目:`, toRemove)
|
console.log(`[VirtualScroller] 清理了 ${toRemove.length} 个多余DOM元素`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,6 +587,10 @@ onBeforeUnmount(() => {
|
|||||||
clearTimeout(scrollTimeout.value)
|
clearTimeout(scrollTimeout.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scrollCleanupTimeout.value) {
|
||||||
|
clearTimeout(scrollCleanupTimeout.value)
|
||||||
|
}
|
||||||
|
|
||||||
itemRefs.clear()
|
itemRefs.clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,10 @@ export async function Playload(data) {
|
|||||||
json.nodeInfoList[key.name].fieldValue = key.url
|
json.nodeInfoList[key.name].fieldValue = key.url
|
||||||
nodeInfoList.push(json.nodeInfoList[key.name])
|
nodeInfoList.push(json.nodeInfoList[key.name])
|
||||||
}
|
}
|
||||||
|
if (json.imageIndex && json.imageIndex[key.name]) {
|
||||||
|
json.imageIndex[key.name].fieldValue = key.index
|
||||||
|
nodeInfoList.push(json.imageIndex[key.name])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (json.nodeInfoList.index) {
|
if (json.nodeInfoList.index) {
|
||||||
json.nodeInfoList.index.fieldValue = data.imgs.length - 1
|
json.nodeInfoList.index.fieldValue = data.imgs.length - 1
|
||||||
|
|||||||
189
src/utils/websocket copy.js
Normal file
189
src/utils/websocket copy.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { ElNotification } from 'element-plus'
|
||||||
|
import { h, ref } from 'vue'
|
||||||
|
import { useDisplayStore, useUserStore } from '@/stores'
|
||||||
|
import { getToken } from '@/utils/auth'
|
||||||
|
import { createTask, getTask } from '@/utils/createTask'
|
||||||
|
import { userError } from '@/utils/tokenError'
|
||||||
|
|
||||||
|
export function getChargeType(chargeType) {
|
||||||
|
switch (chargeType) {
|
||||||
|
case 'Painting':
|
||||||
|
return 1
|
||||||
|
case 'Video':
|
||||||
|
return 4
|
||||||
|
default:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function websocketError(code, msg) {
|
||||||
|
let message
|
||||||
|
switch (code) {
|
||||||
|
case 1006:
|
||||||
|
message = '用户身份验证失败'
|
||||||
|
userError()
|
||||||
|
break
|
||||||
|
case 4401: // 后端返回常规错误
|
||||||
|
message = msg
|
||||||
|
break
|
||||||
|
case 4402: // 后端返回外部平台提交时的错误
|
||||||
|
message = JSON.parse(msg)
|
||||||
|
break
|
||||||
|
case 4403: // 外部平台的任务结果的错误
|
||||||
|
message = msg
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message = '连接异常,请稍后重试'
|
||||||
|
}
|
||||||
|
|
||||||
|
ElNotification({
|
||||||
|
title: '生成失败',
|
||||||
|
|
||||||
|
message: h('i', { style: 'color: teal' }, message),
|
||||||
|
type: 'error',
|
||||||
|
duration: 6000 // 增加持续时间以适应更多信息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function websocketSuccess() {
|
||||||
|
// 合并两个通知为一个
|
||||||
|
ElNotification({
|
||||||
|
title: '生成成功',
|
||||||
|
message: h('div', [
|
||||||
|
h('div', { style: 'font-weight: bold; color: teal;' }, '生成成功!'),
|
||||||
|
h('br'),
|
||||||
|
h('div', { style: 'color: orange; margin-top: 5px;' }, '内测状态,请及时下载生成的文件,云端储存与历史记录保留24小时!')
|
||||||
|
]),
|
||||||
|
type: 'success',
|
||||||
|
duration: 6000 // 增加持续时间以适应更多信息
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generate(data, generateData) {
|
||||||
|
const progress_text = ref('')
|
||||||
|
const message = ref('')
|
||||||
|
const useDisplay = useDisplayStore()
|
||||||
|
const token = getToken()
|
||||||
|
const taskId = crypto.randomUUID()
|
||||||
|
let currentTaskId = null
|
||||||
|
|
||||||
|
useDisplay.isSubGerenate = true
|
||||||
|
|
||||||
|
const result = await createTask(data, taskId, token)
|
||||||
|
console.log(result)
|
||||||
|
// const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0'
|
||||||
|
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=Bearer ${token}`
|
||||||
|
const socket = new WebSocket(wsURL)
|
||||||
|
console.log('WebSocket连接已建立')
|
||||||
|
|
||||||
|
// 心跳机制相关变量
|
||||||
|
let heartbeatInterval = null
|
||||||
|
const heartbeatIntervalTime = 20000 // 30秒发送一次心跳
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 接收服务器消息
|
||||||
|
socket.onmessage = async (event) => {
|
||||||
|
// 处理pong响应
|
||||||
|
if (event.data === 'pong') {
|
||||||
|
console.log('收到心跳响应')
|
||||||
|
return
|
||||||
|
} else if (event.data === 'please give me taskId') {
|
||||||
|
socket.send(`setTaskId:${taskId}`)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理链接错误
|
||||||
|
socket.onerror = (error) => {
|
||||||
|
console.error('WebSocket链接出错:', error)
|
||||||
|
|
||||||
|
// 清理心跳定时器
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
clearInterval(heartbeatInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
ElNotification({
|
||||||
|
title: '生成通知',
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
message: h('i', { style: 'color: teal' }, '生成视频失败'),
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理链接关闭
|
||||||
|
socket.onclose = async (event) => {
|
||||||
|
console.log('WebSocket已关闭:', event)
|
||||||
|
useDisplay.isSubGerenate = false
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
clearInterval(heartbeatInterval)
|
||||||
|
}
|
||||||
|
const res = JSON.parse(message.value)
|
||||||
|
if (event.code === 1006) {
|
||||||
|
console.error('用户身份验证失败')
|
||||||
|
userError()
|
||||||
|
} else if (event.code === 1000 && event.reason === 'success') {
|
||||||
|
console.log('收到服务器消息:', res)
|
||||||
|
const result = await getTask(res)
|
||||||
|
if(useUserStore().freeTimes) await useUserStore().fetchFreeTimes()
|
||||||
|
if (result.type) {
|
||||||
|
if (currentTaskId) {
|
||||||
|
useDisplay.updateItemToSuccess(currentTaskId, result.urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketSuccess()
|
||||||
|
} else {
|
||||||
|
websocketError(4403, result.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
websocketError(event.code, event.reason)
|
||||||
|
}
|
||||||
|
if (heartbeatInterval) {
|
||||||
|
clearInterval(heartbeatInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待 WebSocket 连接打开
|
||||||
|
socket.onopen = () => {
|
||||||
|
console.log('WebSocket连接已建立')
|
||||||
|
|
||||||
|
// 启动心跳机制
|
||||||
|
heartbeatInterval = setInterval(() => {
|
||||||
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
|
socket.send('ping')
|
||||||
|
console.log('发送心跳包')
|
||||||
|
}
|
||||||
|
}, heartbeatIntervalTime)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error creating AI3D_file:', error)
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
ElNotification({
|
||||||
|
title: '生成通知',
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
message: h('i', { style: 'color: teal' }, '生成失败,请检查参数后重新提交任务'),
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user