增加即梦AI功能页面

This commit is contained in:
GeekMaster
2025-07-18 18:04:32 +08:00
parent 66776556d8
commit 76d32c78d8
40 changed files with 4511 additions and 118 deletions

View File

@@ -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('注销失败!')

View File

@@ -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
View 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>

View File

@@ -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>

View 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>

View File

@@ -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