mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-22 02:54:28 +08:00
增加即梦AI功能页面
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1752731646117') format('woff2'),
|
||||
url('iconfont.woff?t=1752731646117') format('woff'),
|
||||
url('iconfont.ttf?t=1752731646117') format('truetype');
|
||||
src: url('iconfont.woff2?t=1752831319382') format('woff2'),
|
||||
url('iconfont.woff?t=1752831319382') format('woff'),
|
||||
url('iconfont.ttf?t=1752831319382') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,14 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-jimeng2:before {
|
||||
content: "\eabc";
|
||||
}
|
||||
|
||||
.icon-jimeng:before {
|
||||
content: "\eabb";
|
||||
}
|
||||
|
||||
.icon-video:before {
|
||||
content: "\e63f";
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,20 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "42693930",
|
||||
"name": "即梦AI-02",
|
||||
"font_class": "jimeng2",
|
||||
"unicode": "eabc",
|
||||
"unicode_decimal": 60092
|
||||
},
|
||||
{
|
||||
"icon_id": "42693927",
|
||||
"name": "即梦AI-01",
|
||||
"font_class": "jimeng",
|
||||
"unicode": "eabb",
|
||||
"unicode_decimal": 60091
|
||||
},
|
||||
{
|
||||
"icon_id": "1283",
|
||||
"name": "视频",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
291
web/src/components/ImageUpload.vue
Normal file
291
web/src/components/ImageUpload.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="image-upload">
|
||||
<div class="upload-list" v-if="imageList.length > 0">
|
||||
<div v-for="(image, index) in imageList" :key="index" class="upload-item">
|
||||
<el-image
|
||||
:src="image"
|
||||
:preview-src-list="imageList"
|
||||
:initial-index="index"
|
||||
fit="cover"
|
||||
class="upload-image"
|
||||
/>
|
||||
<div class="upload-overlay">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="removeImage(index)"
|
||||
class="remove-btn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传按钮 -->
|
||||
<div v-if="!multiple || imageList.length < maxCount" class="upload-btn">
|
||||
<el-upload
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
accept="image/*"
|
||||
class="uploader"
|
||||
>
|
||||
<div class="upload-placeholder">
|
||||
<el-icon :size="20"><Plus /></el-icon>
|
||||
<span>上传图片</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 初始上传区域 -->
|
||||
<div v-else class="upload-area">
|
||||
<el-upload
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
accept="image/*"
|
||||
class="uploader"
|
||||
>
|
||||
<div class="upload-placeholder">
|
||||
<el-icon :size="40"><Plus /></el-icon>
|
||||
<div class="upload-text">
|
||||
<p>点击上传图片</p>
|
||||
<p class="upload-tip">支持 JPG、PNG 格式,最大 10MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<!-- 上传进度 -->
|
||||
<el-progress
|
||||
v-if="uploading"
|
||||
:percentage="uploadProgress"
|
||||
:stroke-width="4"
|
||||
class="upload-progress"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: '',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'upload-success'])
|
||||
|
||||
// 上传状态
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
|
||||
// 图片列表
|
||||
const imageList = computed({
|
||||
get() {
|
||||
if (props.multiple) {
|
||||
return Array.isArray(props.modelValue) ? props.modelValue : []
|
||||
} else {
|
||||
return props.modelValue ? [props.modelValue] : []
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
if (props.multiple) {
|
||||
emit('update:modelValue', value)
|
||||
} else {
|
||||
emit('update:modelValue', value[0] || '')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = async (uploadFile) => {
|
||||
const file = uploadFile.file
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
ElMessage.error('请选择图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小 (10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
ElMessage.error('图片大小不能超过 10MB')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数量限制
|
||||
if (props.multiple && imageList.value.length >= props.maxCount) {
|
||||
ElMessage.error(`最多只能上传 ${props.maxCount} 张图片`)
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
// 模拟上传进度
|
||||
const progressTimer = setInterval(() => {
|
||||
if (uploadProgress.value < 90) {
|
||||
uploadProgress.value += 10
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const response = await httpPost('/api/upload', formData)
|
||||
|
||||
clearInterval(progressTimer)
|
||||
uploadProgress.value = 100
|
||||
|
||||
const imageUrl = response.data.url
|
||||
|
||||
// 更新图片列表
|
||||
if (props.multiple) {
|
||||
const newList = [...imageList.value, imageUrl]
|
||||
imageList.value = newList
|
||||
} else {
|
||||
imageList.value = [imageUrl]
|
||||
}
|
||||
|
||||
emit('upload-success', imageUrl)
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('上传失败: ' + (error.message || '网络错误'))
|
||||
} finally {
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 移除图片
|
||||
const removeImage = (index) => {
|
||||
const newList = [...imageList.value]
|
||||
newList.splice(index, 1)
|
||||
imageList.value = newList
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.image-upload
|
||||
width 100%
|
||||
|
||||
.upload-list
|
||||
display flex
|
||||
flex-wrap wrap
|
||||
gap 10px
|
||||
|
||||
.upload-item
|
||||
position relative
|
||||
width 100px
|
||||
height 100px
|
||||
border-radius 6px
|
||||
overflow hidden
|
||||
border 1px solid #dcdfe6
|
||||
|
||||
.upload-image
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
.upload-overlay
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
right 0
|
||||
bottom 0
|
||||
background rgba(0, 0, 0, 0.5)
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
opacity 0
|
||||
transition opacity 0.3s
|
||||
|
||||
.remove-btn
|
||||
background rgba(245, 108, 108, 0.8)
|
||||
border none
|
||||
color white
|
||||
|
||||
&:hover .upload-overlay
|
||||
opacity 1
|
||||
|
||||
.upload-btn
|
||||
width 100px
|
||||
height 100px
|
||||
border 2px dashed #dcdfe6
|
||||
border-radius 6px
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
cursor pointer
|
||||
transition all 0.3s
|
||||
|
||||
&:hover
|
||||
border-color #409eff
|
||||
color #409eff
|
||||
|
||||
.uploader
|
||||
width 100%
|
||||
height 100%
|
||||
|
||||
.upload-placeholder
|
||||
display flex
|
||||
flex-direction column
|
||||
align-items center
|
||||
gap 5px
|
||||
font-size 12px
|
||||
color #8c939d
|
||||
|
||||
.upload-area
|
||||
border 2px dashed #dcdfe6
|
||||
border-radius 6px
|
||||
padding 40px
|
||||
text-align center
|
||||
cursor pointer
|
||||
transition all 0.3s
|
||||
|
||||
&:hover
|
||||
border-color #409eff
|
||||
|
||||
.uploader
|
||||
width 100%
|
||||
|
||||
.upload-placeholder
|
||||
display flex
|
||||
flex-direction column
|
||||
align-items center
|
||||
gap 10px
|
||||
color #8c939d
|
||||
|
||||
.upload-text
|
||||
p
|
||||
margin 5px 0
|
||||
|
||||
.upload-tip
|
||||
font-size 12px
|
||||
color #c0c4cc
|
||||
|
||||
.upload-progress
|
||||
margin-top 10px
|
||||
|
||||
:deep(.el-upload)
|
||||
width 100%
|
||||
height 100%
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
</style>
|
||||
@@ -159,6 +159,11 @@ const items = [
|
||||
index: '/admin/medias',
|
||||
title: '音视频记录',
|
||||
},
|
||||
{
|
||||
icon: 'image',
|
||||
index: '/admin/jimeng',
|
||||
title: '即梦AI任务',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -109,6 +109,12 @@ const routes = [
|
||||
meta: { title: '视频创作中心' },
|
||||
component: () => import('@/views/Video.vue'),
|
||||
},
|
||||
{
|
||||
name: 'jimeng',
|
||||
path: '/jimeng',
|
||||
meta: { title: '即梦AI' },
|
||||
component: () => import('@/views/Jimeng.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -252,6 +258,12 @@ const routes = [
|
||||
meta: { title: '音视频管理' },
|
||||
component: () => import('@/views/admin/records/Medias.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/jimeng',
|
||||
name: 'admin-jimeng',
|
||||
meta: { title: '即梦AI管理' },
|
||||
component: () => import('@/views/admin/JimengJobs.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/powerLog',
|
||||
name: 'admin-power-log',
|
||||
|
||||
513
web/src/store/jimeng.js
Normal file
513
web/src/store/jimeng.js
Normal file
@@ -0,0 +1,513 @@
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg, substr, dateFormat } from '@/utils/libs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
export const useJimengStore = defineStore('jimeng', () => {
|
||||
// 当前激活的功能分类和具体功能
|
||||
const activeCategory = ref('image_generation')
|
||||
const activeFunction = ref('text_to_image')
|
||||
const useImageInput = ref(false)
|
||||
|
||||
// 共同状态
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(false)
|
||||
const pullHandler = ref(null)
|
||||
const taskFilter = ref('all')
|
||||
const currentList = ref([])
|
||||
|
||||
// 用户信息
|
||||
const isLogin = ref(false)
|
||||
const userPower = ref(100)
|
||||
|
||||
// 视频预览
|
||||
const showDialog = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
|
||||
// 功能分类配置
|
||||
const categories = [
|
||||
{ key: 'image_generation', name: '图片生成' },
|
||||
{ key: 'image_editing', name: 'AI修图' },
|
||||
{ key: 'image_effects', name: '图像特效' },
|
||||
{ key: 'video_generation', name: '视频生成' },
|
||||
]
|
||||
|
||||
// 功能配置
|
||||
const functions = [
|
||||
{ key: 'text_to_image', name: '文生图', category: 'image_generation', needsPrompt: true, needsImage: false, power: 20 },
|
||||
{ key: 'image_to_image_portrait', name: '图生图', category: 'image_generation', needsPrompt: true, needsImage: true, power: 30 },
|
||||
{ key: 'image_edit', name: '图像编辑', category: 'image_editing', needsPrompt: true, needsImage: true, multiple: true, power: 25 },
|
||||
{ key: 'image_effects', name: '图像特效', category: 'image_effects', needsPrompt: false, needsImage: true, power: 15 },
|
||||
{ key: 'text_to_video', name: '文生视频', category: 'video_generation', needsPrompt: true, needsImage: false, power: 100 },
|
||||
{ key: 'image_to_video', name: '图生视频', category: 'video_generation', needsPrompt: true, needsImage: true, multiple: true, power: 120 },
|
||||
]
|
||||
|
||||
// 各功能的参数
|
||||
const textToImageParams = reactive({
|
||||
prompt: '',
|
||||
size: '1328x1328',
|
||||
scale: 2.5,
|
||||
seed: -1,
|
||||
use_pre_llm: false,
|
||||
})
|
||||
|
||||
const imageToImageParams = reactive({
|
||||
image_input: '',
|
||||
prompt: '演唱会现场的合照,闪光灯拍摄',
|
||||
size: '1328x1328',
|
||||
gpen: 0.4,
|
||||
skin: 0.3,
|
||||
skin_unifi: 0,
|
||||
gen_mode: 'creative',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
const imageEditParams = reactive({
|
||||
image_urls: [],
|
||||
prompt: '',
|
||||
scale: 0.5,
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
const imageEffectsParams = reactive({
|
||||
image_input1: '',
|
||||
template_id: '',
|
||||
size: '1328x1328',
|
||||
})
|
||||
|
||||
const textToVideoParams = reactive({
|
||||
prompt: '',
|
||||
aspect_ratio: '16:9',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
const imageToVideoParams = reactive({
|
||||
image_urls: [],
|
||||
prompt: '',
|
||||
aspect_ratio: '16:9',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const currentFunction = computed(() => {
|
||||
return functions.find(f => f.key === activeFunction.value) || functions[0]
|
||||
})
|
||||
|
||||
const currentFunctions = computed(() => {
|
||||
return functions.filter(f => f.category === activeCategory.value)
|
||||
})
|
||||
|
||||
const needsPrompt = computed(() => currentFunction.value.needsPrompt)
|
||||
const needsImage = computed(() => currentFunction.value.needsImage)
|
||||
const needsMultipleImages = computed(() => currentFunction.value.multiple)
|
||||
const currentPowerCost = computed(() => currentFunction.value.power)
|
||||
|
||||
// 初始化方法
|
||||
const init = async () => {
|
||||
try {
|
||||
const user = await checkSession()
|
||||
isLogin.value = true
|
||||
userPower.value = user.power
|
||||
|
||||
// 获取任务列表
|
||||
await fetchData(1)
|
||||
|
||||
// 检查是否需要开始轮询
|
||||
const pendingCount = await getPendingCount()
|
||||
if (pendingCount > 0) {
|
||||
startPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换功能分类
|
||||
const switchCategory = (category) => {
|
||||
activeCategory.value = category
|
||||
const categoryFunctions = functions.filter(f => f.category === category)
|
||||
if (categoryFunctions.length > 0) {
|
||||
if (category === 'image_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_image_portrait' : 'text_to_image'
|
||||
} else if (category === 'video_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video'
|
||||
} else {
|
||||
activeFunction.value = categoryFunctions[0].key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换输入模式
|
||||
const switchInputMode = () => {
|
||||
if (activeCategory.value === 'image_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_image_portrait' : 'text_to_image'
|
||||
} else if (activeCategory.value === 'video_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video'
|
||||
}
|
||||
}
|
||||
|
||||
// 切换功能
|
||||
const switchFunction = (functionKey) => {
|
||||
activeFunction.value = functionKey
|
||||
}
|
||||
|
||||
// 获取当前算力消耗
|
||||
const getCurrentPowerCost = () => {
|
||||
return currentFunction.value.power
|
||||
}
|
||||
|
||||
// 获取功能名称
|
||||
const getFunctionName = (type) => {
|
||||
const func = functions.find(f => f.key === type)
|
||||
return func ? func.name : type
|
||||
}
|
||||
|
||||
// 获取任务状态文本
|
||||
const getTaskStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'processing': '处理中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
'pending': 'info',
|
||||
'processing': 'warning',
|
||||
'completed': 'success',
|
||||
'failed': 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 切换任务筛选
|
||||
const switchTaskFilter = (filter) => {
|
||||
taskFilter.value = filter
|
||||
updateCurrentList()
|
||||
}
|
||||
|
||||
// 更新当前列表
|
||||
const updateCurrentList = () => {
|
||||
if (taskFilter.value === 'all') {
|
||||
currentList.value = list.value
|
||||
} else if (taskFilter.value === 'image') {
|
||||
currentList.value = list.value.filter(item =>
|
||||
['text_to_image', 'image_to_image_portrait', 'image_edit', 'image_effects'].includes(item.type)
|
||||
)
|
||||
} else if (taskFilter.value === 'video') {
|
||||
currentList.value = list.value.filter(item =>
|
||||
['text_to_video', 'image_to_video'].includes(item.type)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
const fetchData = async (pageNum = 1) => {
|
||||
try {
|
||||
loading.value = true
|
||||
page.value = pageNum
|
||||
|
||||
const response = await httpGet('/api/jimeng/jobs', {
|
||||
page: pageNum,
|
||||
page_size: pageSize.value
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
list.value = response.data.jobs || []
|
||||
total.value = response.data.total || 0
|
||||
noData.value = list.value.length === 0
|
||||
updateCurrentList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error)
|
||||
showMessageError('获取任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交任务
|
||||
const submitTask = async () => {
|
||||
if (!isLogin.value) {
|
||||
showMessageError('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
if (userPower.value < currentPowerCost.value) {
|
||||
showMessageError('算力不足')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
submitting.value = true
|
||||
let apiUrl = ''
|
||||
let requestData = {}
|
||||
|
||||
switch (activeFunction.value) {
|
||||
case 'text_to_image':
|
||||
apiUrl = '/api/jimeng/text-to-image'
|
||||
requestData = {
|
||||
prompt: textToImageParams.prompt,
|
||||
width: parseInt(textToImageParams.size.split('x')[0]),
|
||||
height: parseInt(textToImageParams.size.split('x')[1]),
|
||||
scale: textToImageParams.scale,
|
||||
seed: textToImageParams.seed,
|
||||
use_pre_llm: textToImageParams.use_pre_llm,
|
||||
}
|
||||
break
|
||||
|
||||
case 'image_to_image_portrait':
|
||||
apiUrl = '/api/jimeng/image-to-image-portrait'
|
||||
requestData = {
|
||||
image_input: imageToImageParams.image_input,
|
||||
prompt: imageToImageParams.prompt,
|
||||
width: parseInt(imageToImageParams.size.split('x')[0]),
|
||||
height: parseInt(imageToImageParams.size.split('x')[1]),
|
||||
gpen: imageToImageParams.gpen,
|
||||
skin: imageToImageParams.skin,
|
||||
skin_unifi: imageToImageParams.skin_unifi,
|
||||
gen_mode: imageToImageParams.gen_mode,
|
||||
seed: imageToImageParams.seed,
|
||||
}
|
||||
break
|
||||
|
||||
case 'image_edit':
|
||||
apiUrl = '/api/jimeng/image-edit'
|
||||
requestData = {
|
||||
image_urls: imageEditParams.image_urls,
|
||||
prompt: imageEditParams.prompt,
|
||||
scale: imageEditParams.scale,
|
||||
seed: imageEditParams.seed,
|
||||
}
|
||||
break
|
||||
|
||||
case 'image_effects':
|
||||
apiUrl = '/api/jimeng/image-effects'
|
||||
requestData = {
|
||||
image_input1: imageEffectsParams.image_input1,
|
||||
template_id: imageEffectsParams.template_id,
|
||||
width: parseInt(imageEffectsParams.size.split('x')[0]),
|
||||
height: parseInt(imageEffectsParams.size.split('x')[1]),
|
||||
}
|
||||
break
|
||||
|
||||
case 'text_to_video':
|
||||
apiUrl = '/api/jimeng/text-to-video'
|
||||
requestData = {
|
||||
prompt: textToVideoParams.prompt,
|
||||
aspect_ratio: textToVideoParams.aspect_ratio,
|
||||
seed: textToVideoParams.seed,
|
||||
}
|
||||
break
|
||||
|
||||
case 'image_to_video':
|
||||
apiUrl = '/api/jimeng/image-to-video'
|
||||
requestData = {
|
||||
image_urls: imageToVideoParams.image_urls,
|
||||
prompt: imageToVideoParams.prompt,
|
||||
aspect_ratio: imageToVideoParams.aspect_ratio,
|
||||
seed: imageToVideoParams.seed,
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const response = await httpPost(apiUrl, requestData)
|
||||
|
||||
if (response.data) {
|
||||
showMessageOK('任务提交成功')
|
||||
// 重新获取任务列表
|
||||
await fetchData(1)
|
||||
// 开始轮询
|
||||
startPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交任务失败:', error)
|
||||
showMessageError(error.message || '提交任务失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取待处理任务数量
|
||||
const getPendingCount = async () => {
|
||||
try {
|
||||
const response = await httpGet('/api/jimeng/pending-count')
|
||||
return response.data?.count || 0
|
||||
} catch (error) {
|
||||
console.error('获取待处理任务数量失败:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 开始轮询
|
||||
const startPolling = () => {
|
||||
if (taskPulling.value) return
|
||||
|
||||
taskPulling.value = true
|
||||
pullHandler.value = setInterval(async () => {
|
||||
const pendingCount = await getPendingCount()
|
||||
if (pendingCount > 0) {
|
||||
await fetchData(page.value)
|
||||
} else {
|
||||
stopPolling()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 停止轮询
|
||||
const stopPolling = () => {
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value)
|
||||
pullHandler.value = null
|
||||
}
|
||||
taskPulling.value = false
|
||||
}
|
||||
|
||||
// 重试任务
|
||||
const retryTask = async (taskId) => {
|
||||
try {
|
||||
const response = await httpPost(`/api/jimeng/retry/${taskId}`)
|
||||
if (response.data) {
|
||||
showMessageOK('重试任务已提交')
|
||||
await fetchData(page.value)
|
||||
startPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重试任务失败:', error)
|
||||
showMessageError(error.message || '重试任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const removeJob = async (item) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除这个任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
|
||||
const response = await httpGet('/api/jimeng/remove', { id: item.id })
|
||||
if (response.data) {
|
||||
showMessageOK('删除成功')
|
||||
await fetchData(page.value)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除任务失败:', error)
|
||||
showMessageError(error.message || '删除任务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 播放视频
|
||||
const playVideo = (item) => {
|
||||
currentVideoUrl.value = item.video_url
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (item) => {
|
||||
const url = item.video_url || item.img_url
|
||||
if (url) {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `jimeng_${item.id}.${item.video_url ? 'mp4' : 'jpg'}`
|
||||
link.click()
|
||||
}
|
||||
}
|
||||
|
||||
// 清理
|
||||
const cleanup = () => {
|
||||
stopPolling()
|
||||
}
|
||||
|
||||
// 返回所有状态和方法
|
||||
return {
|
||||
// 状态
|
||||
activeCategory,
|
||||
activeFunction,
|
||||
useImageInput,
|
||||
loading,
|
||||
submitting,
|
||||
list,
|
||||
noData,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
taskFilter,
|
||||
currentList,
|
||||
isLogin,
|
||||
userPower,
|
||||
showDialog,
|
||||
currentVideoUrl,
|
||||
nodata,
|
||||
|
||||
// 配置
|
||||
categories,
|
||||
functions,
|
||||
currentFunctions,
|
||||
|
||||
// 参数
|
||||
textToImageParams,
|
||||
imageToImageParams,
|
||||
imageEditParams,
|
||||
imageEffectsParams,
|
||||
textToVideoParams,
|
||||
imageToVideoParams,
|
||||
|
||||
// 计算属性
|
||||
currentFunction,
|
||||
needsPrompt,
|
||||
needsImage,
|
||||
needsMultipleImages,
|
||||
currentPowerCost,
|
||||
|
||||
// 方法
|
||||
init,
|
||||
switchCategory,
|
||||
switchFunction,
|
||||
switchInputMode,
|
||||
getCurrentPowerCost,
|
||||
getFunctionName,
|
||||
getTaskStatusText,
|
||||
getStatusType,
|
||||
switchTaskFilter,
|
||||
updateCurrentList,
|
||||
fetchData,
|
||||
submitTask,
|
||||
getPendingCount,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
retryTask,
|
||||
removeJob,
|
||||
playVideo,
|
||||
downloadFile,
|
||||
cleanup,
|
||||
|
||||
// 工具函数
|
||||
substr,
|
||||
replaceImg,
|
||||
}
|
||||
})
|
||||
@@ -255,3 +255,8 @@ export function isChrome() {
|
||||
const userAgent = navigator.userAgent.toLowerCase()
|
||||
return /chrome/.test(userAgent) && !/edg/.test(userAgent)
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
export function formatDateTime(timestamp, format = 'yyyy-MM-dd HH:mm:ss') {
|
||||
return dateFormat(timestamp, format)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<el-popover placement="right-end" trigger="hover" v-if="loginUser.id">
|
||||
<template #reference>
|
||||
<li class="menu-list-item flex-center-col">
|
||||
<i class="iconfont icon-config" />
|
||||
<i class="iconfont icon-user-circle" />
|
||||
</li>
|
||||
</template>
|
||||
<template #default>
|
||||
@@ -97,6 +97,11 @@
|
||||
</ul>
|
||||
</template>
|
||||
</el-popover>
|
||||
<div v-else class="mb-2 flex justify-center">
|
||||
<el-button @click="store.setShowLoginDialog(true)" type="primary" size="small">
|
||||
登录
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="menu-bot-item">
|
||||
<a @click="router.push('/')" class="link-button">
|
||||
<i class="iconfont icon-house"></i>
|
||||
@@ -109,14 +114,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-scrollbar class="right-main">
|
||||
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
|
||||
<!-- <div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
|
||||
<el-button
|
||||
@click="router.push('/login')"
|
||||
class="btn-go animate__animated animate__pulse animate__infinite"
|
||||
round
|
||||
>登录</el-button
|
||||
>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="content custom-scroll">
|
||||
<router-view :key="routerViewKey" v-slot="{ Component }">
|
||||
<transition name="move" mode="out-in">
|
||||
@@ -281,7 +286,9 @@ const logout = function () {
|
||||
httpGet('/api/user/logout')
|
||||
.then(() => {
|
||||
removeUserToken()
|
||||
router.push('/login')
|
||||
// 刷新组件
|
||||
routerViewKey.value += 1
|
||||
loginUser.value = {}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('注销失败!')
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
class="nav-item-box"
|
||||
@click="router.push(item.url)"
|
||||
>
|
||||
<i :class="'iconfont ' + iconMap[item.url]"></i>
|
||||
<i :class="'iconfont ' + item.icon"></i>
|
||||
<div>{{ item.name }}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
@@ -107,20 +107,6 @@ const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
|
||||
const giteeURL = ref(import.meta.env.VITE_GITEE_URL)
|
||||
const navs = ref([])
|
||||
|
||||
const iconMap = ref({
|
||||
'/chat': 'icon-chat',
|
||||
'/mj': 'icon-mj',
|
||||
'/sd': 'icon-sd',
|
||||
'/dalle': 'icon-dalle',
|
||||
'/images-wall': 'icon-image',
|
||||
'/suno': 'icon-suno',
|
||||
'/xmind': 'icon-xmind',
|
||||
'/apps': 'icon-app',
|
||||
'/member': 'icon-vip-user',
|
||||
'/invite': 'icon-share',
|
||||
'/luma': 'icon-luma',
|
||||
})
|
||||
|
||||
const displayedChars = ref([])
|
||||
const initAnimation = ref('')
|
||||
let timer = null // 定时器句柄
|
||||
|
||||
799
web/src/views/Jimeng.vue
Normal file
799
web/src/views/Jimeng.vue
Normal file
@@ -0,0 +1,799 @@
|
||||
<template>
|
||||
<div class="page-jimeng">
|
||||
<!-- 左侧参数设置面板 -->
|
||||
<div class="params-panel">
|
||||
<h2>即梦AI</h2>
|
||||
|
||||
<!-- 功能分类按钮组 -->
|
||||
<div class="category-buttons">
|
||||
<div class="category-label">
|
||||
<el-icon><Star /></el-icon>
|
||||
功能分类
|
||||
</div>
|
||||
<div class="category-grid">
|
||||
<div
|
||||
v-for="category in store.categories"
|
||||
:key="category.key"
|
||||
:class="['category-btn', { active: store.activeCategory === category.key }]"
|
||||
@click="store.switchCategory(category.key)"
|
||||
>
|
||||
<div class="category-icon">
|
||||
<i :class="getCategoryIcon(category.key)"></i>
|
||||
</div>
|
||||
<div class="category-name">{{ category.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能开关 -->
|
||||
<div class="function-switch" v-if="store.activeCategory === 'image_generation' || store.activeCategory === 'video_generation'">
|
||||
<div class="switch-label">
|
||||
<el-icon><Switch /></el-icon>
|
||||
生成模式
|
||||
</div>
|
||||
<div class="switch-container">
|
||||
<div class="switch-info">
|
||||
<div class="switch-title">
|
||||
{{ store.useImageInput ? (store.activeCategory === 'image_generation' ? '图生图' : '图生视频') : (store.activeCategory === 'image_generation' ? '文生图' : '文生视频') }}
|
||||
</div>
|
||||
<div class="switch-desc">
|
||||
{{ store.useImageInput ? '使用图片作为输入' : '使用文字作为输入' }}
|
||||
</div>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="store.useImageInput"
|
||||
@change="store.switchInputMode"
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参数容器 -->
|
||||
<div class="params-container">
|
||||
<!-- 文生图 -->
|
||||
<div v-if="store.activeFunction === 'text_to_image'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
<el-tooltip content="输入你想要的图片内容描述" placement="right">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.textToImageParams.prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="请输入图片描述,越详细越好"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">图片尺寸:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.textToImageParams.size" placeholder="选择尺寸">
|
||||
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">创意度:</span>
|
||||
<el-slider v-model="store.textToImageParams.scale" :min="1" :max="10" :step="0.5" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number v-model="store.textToImageParams.seed" :min="-1" :max="999999" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="item-group flex justify-between">
|
||||
<span class="label">智能优化提示词</span>
|
||||
<el-switch v-model="store.textToImageParams.use_pre_llm" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生图 -->
|
||||
<div v-if="store.activeFunction === 'image_to_image_portrait'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<ImageUpload v-model="store.imageToImageParams.image_input" />
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.imageToImageParams.prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的图片效果"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">图片尺寸:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageToImageParams.size" placeholder="选择尺寸">
|
||||
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">GPEN强度:</span>
|
||||
<el-slider v-model="store.imageToImageParams.gpen" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">肌肤质感:</span>
|
||||
<el-slider v-model="store.imageToImageParams.skin" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number v-model="store.imageToImageParams.seed" :min="-1" :max="999999" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像编辑 -->
|
||||
<div v-if="store.activeFunction === 'image_edit'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<ImageUpload v-model="store.imageEditParams.image_urls" :multiple="true" />
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">编辑提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.imageEditParams.prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的编辑效果"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">编辑强度:</span>
|
||||
<el-slider v-model="store.imageEditParams.scale" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number v-model="store.imageEditParams.seed" :min="-1" :max="999999" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像特效 -->
|
||||
<div v-if="store.activeFunction === 'image_effects'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<ImageUpload v-model="store.imageEffectsParams.image_input1" />
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">特效模板:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageEffectsParams.template_id" placeholder="选择特效模板">
|
||||
<el-option label="经典特效" value="classic" />
|
||||
<el-option label="艺术风格" value="artistic" />
|
||||
<el-option label="现代科技" value="modern" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">输出尺寸:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageEffectsParams.size" placeholder="选择尺寸">
|
||||
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文生视频 -->
|
||||
<div v-if="store.activeFunction === 'text_to_video'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.textToVideoParams.prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的视频内容"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">视频比例:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.textToVideoParams.aspect_ratio" placeholder="选择比例">
|
||||
<el-option label="16:9 (横版)" value="16:9" />
|
||||
<el-option label="9:16 (竖版)" value="9:16" />
|
||||
<el-option label="1:1 (正方形)" value="1:1" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number v-model="store.textToVideoParams.seed" :min="-1" :max="999999" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生视频 -->
|
||||
<div v-if="store.activeFunction === 'image_to_video'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<ImageUpload v-model="store.imageToVideoParams.image_urls" :multiple="true" />
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.imageToVideoParams.prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的视频效果"
|
||||
maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line pt">
|
||||
<span class="label">视频比例:</span>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageToVideoParams.aspect_ratio" placeholder="选择比例">
|
||||
<el-option label="16:9 (横版)" value="16:9" />
|
||||
<el-option label="9:16 (竖版)" value="9:16" />
|
||||
<el-option label="1:1 (正方形)" value="1:1" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number v-model="store.imageToVideoParams.seed" :min="-1" :max="999999" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 算力显示 -->
|
||||
<div class="text-info">
|
||||
<el-tag type="primary">当前算力: {{ store.userPower }}</el-tag>
|
||||
<el-tag type="warning">消耗: {{ store.currentPowerCost }}</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div class="submit-btn">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="store.submitTask"
|
||||
:loading="store.submitting"
|
||||
:disabled="!store.isLogin || store.userPower < store.currentPowerCost"
|
||||
size="large"
|
||||
>
|
||||
立即生成 ({{ store.currentPowerCost }}<i class="iconfont icon-vip2"></i>)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧任务列表 -->
|
||||
<div class="main-content" v-loading="store.loading">
|
||||
<div class="works-header">
|
||||
<h2 class="h-title">你的作品</h2>
|
||||
<div class="filter-buttons">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'all' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('all')"
|
||||
size="small"
|
||||
>
|
||||
全部
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'image' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('image')"
|
||||
size="small"
|
||||
>
|
||||
图片
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="store.taskFilter === 'video' ? 'primary' : 'default'"
|
||||
@click="store.switchTaskFilter('video')"
|
||||
size="small"
|
||||
>
|
||||
视频
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-list">
|
||||
<div class="list-box" v-if="!store.noData">
|
||||
<div v-for="item in store.currentList" :key="item.id" class="task-item">
|
||||
<div class="task-left">
|
||||
<div class="task-preview">
|
||||
<el-image
|
||||
v-if="item.img_url"
|
||||
:src="item.img_url"
|
||||
fit="cover"
|
||||
class="preview-image"
|
||||
/>
|
||||
<video
|
||||
v-else-if="item.video_url"
|
||||
:src="item.video_url"
|
||||
class="preview-video"
|
||||
preload="metadata"
|
||||
/>
|
||||
<div v-else class="preview-placeholder">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>{{ store.getTaskStatusText(item.status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-center">
|
||||
<div class="task-info">
|
||||
<el-tag size="small" :type="store.getStatusType(item.status)">
|
||||
{{ store.getTaskStatusText(item.status) }}
|
||||
</el-tag>
|
||||
<el-tag size="small">{{ store.getFunctionName(item.type) }}</el-tag>
|
||||
</div>
|
||||
<div class="task-prompt">
|
||||
{{ store.substr(item.prompt, 200) }}
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>{{ dateFormat(item.created_at) }}</span>
|
||||
<span v-if="item.power">{{ item.power }}算力</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-right">
|
||||
<div class="task-actions">
|
||||
<el-button
|
||||
v-if="item.status === 'failed'"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="store.retryTask(item.id)"
|
||||
>
|
||||
重试
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.video_url || item.img_url"
|
||||
type="default"
|
||||
size="small"
|
||||
@click="store.downloadFile(item)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.video_url"
|
||||
type="default"
|
||||
size="small"
|
||||
@click="store.playVideo(item)"
|
||||
>
|
||||
播放
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="store.removeJob(item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-else
|
||||
:image="store.nodata"
|
||||
description="暂无任务,快去创建吧!"
|
||||
/>
|
||||
|
||||
<div class="pagination" v-if="store.total > store.pageSize">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, prev, pager, next"
|
||||
:current-page="store.page"
|
||||
:page-size="store.pageSize"
|
||||
:total="store.total"
|
||||
@current-change="store.fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频预览对话框 -->
|
||||
<el-dialog
|
||||
v-model="store.showDialog"
|
||||
title="视频预览"
|
||||
width="70%"
|
||||
center
|
||||
>
|
||||
<video
|
||||
:src="store.currentVideoUrl"
|
||||
controls
|
||||
style="width: 100%; max-height: 60vh;"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useJimengStore } from '@/store/jimeng'
|
||||
import { dateFormat } from '@/utils/libs'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
import { InfoFilled, Star, Switch, Picture } from '@element-plus/icons-vue'
|
||||
|
||||
const store = useJimengStore()
|
||||
|
||||
// 获取分类图标
|
||||
const getCategoryIcon = (category) => {
|
||||
const iconMap = {
|
||||
'image_generation': 'iconfont icon-image',
|
||||
'image_editing': 'iconfont icon-edit',
|
||||
'image_effects': 'iconfont icon-magic',
|
||||
'video_generation': 'iconfont icon-video'
|
||||
}
|
||||
return iconMap[category] || 'iconfont icon-image'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
store.init()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
store.cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.page-jimeng {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--chat-bg);
|
||||
|
||||
// 左侧参数面板
|
||||
.params-panel {
|
||||
min-width: 380px;
|
||||
max-width: 380px;
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
overflow: auto;
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
// 功能分类按钮组
|
||||
.category-buttons {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.category-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
color: #5865f2;
|
||||
}
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
|
||||
.category-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 15px 10px;
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: #fafafa;
|
||||
|
||||
&:hover {
|
||||
border-color: #5865f2;
|
||||
background: #f8f9ff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #5865f2;
|
||||
background: linear-gradient(135deg, #5865f2 0%, #7289da 100%);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(88, 101, 242, 0.3);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 功能开关
|
||||
.function-switch {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.switch-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
color: #5865f2;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
background: #f9f9f9;
|
||||
|
||||
.switch-info {
|
||||
flex: 1;
|
||||
|
||||
.switch-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.switch-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 参数容器
|
||||
.params-container {
|
||||
.function-panel {
|
||||
.param-line {
|
||||
margin-bottom: 15px;
|
||||
|
||||
&.pt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.item-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.label {
|
||||
margin-right: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-info {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: #f0f8ff;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #5865f2;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 30px;
|
||||
|
||||
.el-button {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧主要内容区域
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background: var(--chat-bg);
|
||||
color: var(--text-theme-color);
|
||||
|
||||
.works-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.h-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-theme-color);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
.list-box {
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.task-left {
|
||||
margin-right: 20px;
|
||||
|
||||
.task-preview {
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.preview-image, .preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-center {
|
||||
flex: 1;
|
||||
|
||||
.task-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.task-prompt {
|
||||
font-size: 14px;
|
||||
color: var(--text-theme-color);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.task-right {
|
||||
.task-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-jimeng {
|
||||
flex-direction: column;
|
||||
|
||||
.params-panel {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -115,12 +115,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Luma 特有参数设置 -->
|
||||
<div class="item-group">
|
||||
<div class="item-group flex justify-between">
|
||||
<span class="label">循环参考图</span>
|
||||
<el-switch v-model="store.lumaParams.loop" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<div class="item-group flex justify-between">
|
||||
<span class="label">提示词优化</span>
|
||||
<el-switch v-model="store.lumaParams.expand_prompt" size="small" />
|
||||
</div>
|
||||
|
||||
543
web/src/views/admin/JimengJobs.vue
Normal file
543
web/src/views/admin/JimengJobs.vue
Normal file
@@ -0,0 +1,543 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2>即梦AI任务管理</h2>
|
||||
<p>管理所有用户的即梦AI任务,查看任务详情和统计信息</p>
|
||||
</div>
|
||||
|
||||
<!-- 搜索筛选 -->
|
||||
<el-card class="filter-card" shadow="never">
|
||||
<el-form :model="queryForm" ref="queryFormRef" :inline="true" label-width="80px">
|
||||
<el-form-item label="用户ID">
|
||||
<el-input
|
||||
v-model="queryForm.user_id"
|
||||
placeholder="请输入用户ID"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型">
|
||||
<el-select v-model="queryForm.type" placeholder="请选择任务类型" clearable style="width: 150px">
|
||||
<el-option label="文生图" value="text_to_image" />
|
||||
<el-option label="图生图" value="image_to_image_portrait" />
|
||||
<el-option label="图像编辑" value="image_edit" />
|
||||
<el-option label="图像特效" value="image_effects" />
|
||||
<el-option label="文生视频" value="text_to_video" />
|
||||
<el-option label="图生视频" value="image_to_video" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务状态">
|
||||
<el-select v-model="queryForm.status" placeholder="请选择状态" clearable style="width: 120px">
|
||||
<el-option label="等待中" value="pending" />
|
||||
<el-option label="处理中" value="processing" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery" :loading="loading">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleBatchDelete" :disabled="!multipleSelection.length">
|
||||
<el-icon><Delete /></el-icon>
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ stats.totalTasks }}</div>
|
||||
<div class="stat-label">总任务数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number success">{{ stats.completedTasks }}</div>
|
||||
<div class="stat-label">已完成</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number warning">{{ stats.processingTasks }}</div>
|
||||
<div class="stat-label">处理中</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number danger">{{ stats.failedTasks }}</div>
|
||||
<div class="stat-label">失败</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<el-card class="table-card">
|
||||
<el-table
|
||||
:data="taskList"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
stripe
|
||||
border
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="user_id" label="用户ID" width="80" />
|
||||
<el-table-column prop="type" label="任务类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag size="small">{{ getTaskTypeName(scope.row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="prompt" label="提示词" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusColor(scope.row.status)" size="small">
|
||||
{{ getStatusName(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="progress" label="进度" width="100">
|
||||
<template #default="scope">
|
||||
<el-progress :percentage="scope.row.progress" :stroke-width="4" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="power" label="算力" width="80" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
text
|
||||
@click="handleViewDetail(scope.row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
text
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 任务详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialog.visible"
|
||||
:title="`任务详情 - ${detailDialog.data.id}`"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="detail-content" v-if="detailDialog.data">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="任务ID">{{ detailDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ detailDialog.data.user_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务类型">{{ getTaskTypeName(detailDialog.data.type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusColor(detailDialog.data.status)">
|
||||
{{ getStatusName(detailDialog.data.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="进度">{{ detailDialog.data.progress }}%</el-descriptions-item>
|
||||
<el-descriptions-item label="算力消耗">{{ detailDialog.data.power }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDateTime(detailDialog.data.created_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDateTime(detailDialog.data.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-section">
|
||||
<h4>提示词</h4>
|
||||
<div class="prompt-content">{{ detailDialog.data.prompt || '无' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.data.task_params">
|
||||
<h4>任务参数</h4>
|
||||
<el-input
|
||||
v-model="detailDialog.data.task_params"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
readonly
|
||||
class="params-content"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.data.err_msg">
|
||||
<h4>错误信息</h4>
|
||||
<el-alert :title="detailDialog.data.err_msg" type="error" :closable="false" />
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.data.img_url || detailDialog.data.video_url">
|
||||
<h4>生成结果</h4>
|
||||
<div class="result-content">
|
||||
<div v-if="detailDialog.data.img_url" class="result-item">
|
||||
<label>图片:</label>
|
||||
<el-image
|
||||
:src="detailDialog.data.img_url"
|
||||
:preview-src-list="[detailDialog.data.img_url]"
|
||||
fit="cover"
|
||||
style="width: 100px; height: 100px; border-radius: 4px"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="detailDialog.data.video_url" class="result-item">
|
||||
<label>视频:</label>
|
||||
<video
|
||||
:src="detailDialog.data.video_url"
|
||||
controls
|
||||
style="width: 200px; height: 150px; border-radius: 4px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="detailDialog.data.raw_data">
|
||||
<h4>原始响应数据</h4>
|
||||
<el-input
|
||||
v-model="formattedRawData"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
readonly
|
||||
class="raw-data-content"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Delete } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/libs'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
|
||||
// 查询表单
|
||||
const queryForm = reactive({
|
||||
user_id: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据
|
||||
const taskList = ref([])
|
||||
const loading = ref(false)
|
||||
const multipleSelection = ref([])
|
||||
const queryFormRef = ref(null)
|
||||
|
||||
// 统计信息
|
||||
const stats = reactive({
|
||||
totalTasks: 0,
|
||||
completedTasks: 0,
|
||||
processingTasks: 0,
|
||||
failedTasks: 0
|
||||
})
|
||||
|
||||
// 详情对话框
|
||||
const detailDialog = reactive({
|
||||
visible: false,
|
||||
data: {}
|
||||
})
|
||||
|
||||
// 格式化原始数据
|
||||
const formattedRawData = computed(() => {
|
||||
if (!detailDialog.data.raw_data) return ''
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(detailDialog.data.raw_data), null, 2)
|
||||
} catch (error) {
|
||||
return detailDialog.data.raw_data
|
||||
}
|
||||
})
|
||||
|
||||
// 获取任务类型名称
|
||||
const getTaskTypeName = (type) => {
|
||||
const typeMap = {
|
||||
'text_to_image': '文生图',
|
||||
'image_to_image_portrait': '图生图',
|
||||
'image_edit': '图像编辑',
|
||||
'image_effects': '图像特效',
|
||||
'text_to_video': '文生视频',
|
||||
'image_to_video': '图生视频'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取状态名称
|
||||
const getStatusName = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'processing': '处理中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
'pending': '',
|
||||
'processing': 'warning',
|
||||
'completed': 'success',
|
||||
'failed': 'danger'
|
||||
}
|
||||
return colorMap[status] || ''
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
const getTaskList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.size,
|
||||
...queryForm
|
||||
}
|
||||
|
||||
const response = await httpGet('/api/admin/jimeng/jobs', params)
|
||||
taskList.value = response.data.jobs || []
|
||||
pagination.total = response.data.total || 0
|
||||
} catch (error) {
|
||||
ElMessage.error('获取任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
const getStats = async () => {
|
||||
try {
|
||||
const response = await httpGet('/api/admin/jimeng/stats')
|
||||
Object.assign(stats, response.data)
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleQuery = () => {
|
||||
pagination.page = 1
|
||||
getTaskList()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
Object.assign(queryForm, {
|
||||
user_id: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
pagination.page = 1
|
||||
getTaskList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
multipleSelection.value = selection
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = async (row) => {
|
||||
try {
|
||||
const response = await httpGet(`/api/admin/jimeng/job/${row.id}`)
|
||||
detailDialog.data = response.data
|
||||
detailDialog.visible = true
|
||||
} catch (error) {
|
||||
ElMessage.error('获取任务详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除这个任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await httpPost(`/api/admin/jimeng/job/${row.id}`, {}, { method: 'DELETE' })
|
||||
ElMessage.success('删除成功')
|
||||
getTaskList()
|
||||
getStats()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (!multipleSelection.value.length) {
|
||||
ElMessage.warning('请选择要删除的任务')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除选中的 ${multipleSelection.value.length} 个任务吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const jobIds = multipleSelection.value.map(item => item.id)
|
||||
await httpPost('/api/admin/jimeng/batch-remove', { job_ids: jobIds })
|
||||
ElMessage.success('批量删除成功')
|
||||
getTaskList()
|
||||
getStats()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
pagination.size = size
|
||||
pagination.page = 1
|
||||
getTaskList()
|
||||
}
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page) => {
|
||||
pagination.page = page
|
||||
getTaskList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getTaskList()
|
||||
getStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.app-container
|
||||
padding 20px
|
||||
|
||||
.page-header
|
||||
margin-bottom 20px
|
||||
|
||||
h2
|
||||
margin 0 0 8px 0
|
||||
color #303133
|
||||
|
||||
p
|
||||
margin 0
|
||||
color #606266
|
||||
font-size 14px
|
||||
|
||||
.filter-card
|
||||
margin-bottom 20px
|
||||
|
||||
.stats-row
|
||||
margin-bottom 20px
|
||||
|
||||
.stat-card
|
||||
.stat-item
|
||||
text-align center
|
||||
padding 20px
|
||||
|
||||
.stat-number
|
||||
font-size 28px
|
||||
font-weight bold
|
||||
color #303133
|
||||
margin-bottom 8px
|
||||
|
||||
&.success
|
||||
color #67c23a
|
||||
|
||||
&.warning
|
||||
color #e6a23c
|
||||
|
||||
&.danger
|
||||
color #f56c6c
|
||||
|
||||
.stat-label
|
||||
font-size 14px
|
||||
color #909399
|
||||
|
||||
.table-card
|
||||
.pagination-container
|
||||
margin-top 20px
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.detail-content
|
||||
.detail-section
|
||||
margin-bottom 20px
|
||||
|
||||
h4
|
||||
margin 0 0 10px 0
|
||||
color #303133
|
||||
font-size 16px
|
||||
|
||||
.prompt-content
|
||||
background #f5f7fa
|
||||
padding 12px
|
||||
border-radius 4px
|
||||
color #606266
|
||||
line-height 1.6
|
||||
|
||||
.params-content, .raw-data-content
|
||||
font-family monospace
|
||||
|
||||
.result-content
|
||||
.result-item
|
||||
margin-bottom 10px
|
||||
display flex
|
||||
align-items center
|
||||
gap 10px
|
||||
|
||||
label
|
||||
font-weight bold
|
||||
color #303133
|
||||
min-width 50px
|
||||
</style>
|
||||
@@ -169,10 +169,10 @@
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
默认翻译模型
|
||||
系统辅助AI模型
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="选择一个默认模型来翻译提示词"
|
||||
content="用来辅助用户生成提示词,翻译的AI模型,默认使用 gpt-4o-mini"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
@@ -183,9 +183,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<el-select
|
||||
v-model.number="system['translate_model_id']"
|
||||
v-model.number="system['assistant_model_id']"
|
||||
:filterable="true"
|
||||
placeholder="选择一个默认模型来翻译提示词"
|
||||
placeholder="选择一个系统辅助AI模型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
|
||||
Reference in New Issue
Block a user