即梦 AI 管理后台功能完成

This commit is contained in:
GeekMaster
2025-07-22 15:12:49 +08:00
parent 3156701d4e
commit 454dfc1aa7
10 changed files with 474 additions and 451 deletions

View File

@@ -194,7 +194,6 @@ func (h *ImageHandler) Remove(c *gin.Context) {
remark = fmt.Sprintf("任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark = fmt.Sprintf("任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress progress = job.Progress
imgURL = job.ImgURL imgURL = job.ImgURL
break
case "sd": case "sd":
var job model.SdJob var job model.SdJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil { if res := h.DB.Where("id", id).First(&job); res.Error != nil {
@@ -210,7 +209,6 @@ func (h *ImageHandler) Remove(c *gin.Context) {
remark = fmt.Sprintf("任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark = fmt.Sprintf("任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress progress = job.Progress
imgURL = job.ImgURL imgURL = job.ImgURL
break
case "dall": case "dall":
var job model.DallJob var job model.DallJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil { if res := h.DB.Where("id", id).First(&job); res.Error != nil {
@@ -226,7 +224,6 @@ func (h *ImageHandler) Remove(c *gin.Context) {
remark = fmt.Sprintf("任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark = fmt.Sprintf("任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress progress = job.Progress
imgURL = job.ImgURL imgURL = job.ImgURL
break
default: default:
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return

View File

@@ -1,12 +1,15 @@
package admin package admin
import ( import (
"fmt"
"strconv" "strconv"
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/service"
"geekai/service/jimeng" "geekai/service/jimeng"
"geekai/service/oss"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
@@ -19,13 +22,17 @@ import (
type AdminJimengHandler struct { type AdminJimengHandler struct {
handler.BaseHandler handler.BaseHandler
jimengService *jimeng.Service jimengService *jimeng.Service
userService *service.UserService
uploader *oss.UploaderManager
} }
// NewAdminJimengHandler 创建管理后台即梦AI处理器 // NewAdminJimengHandler 创建管理后台即梦AI处理器
func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengService *jimeng.Service) *AdminJimengHandler { func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengService *jimeng.Service, userService *service.UserService, uploader *oss.UploaderManager) *AdminJimengHandler {
return &AdminJimengHandler{ return &AdminJimengHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db}, BaseHandler: handler.BaseHandler{App: app, DB: db},
jimengService: jimengService, jimengService: jimengService,
userService: userService,
uploader: uploader,
} }
} }
@@ -34,8 +41,7 @@ func (h *AdminJimengHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/admin/jimeng/") rg := h.App.Engine.Group("/api/admin/jimeng/")
rg.GET("/jobs", h.Jobs) rg.GET("/jobs", h.Jobs)
rg.GET("/jobs/:id", h.JobDetail) rg.GET("/jobs/:id", h.JobDetail)
rg.DELETE("/jobs/:id", h.Remove) rg.POST("/jobs/remove", h.BatchRemove)
rg.POST("/jobs/batch-remove", h.BatchRemove)
rg.GET("/stats", h.Stats) rg.GET("/stats", h.Stats)
rg.GET("/config", h.GetConfig) rg.GET("/config", h.GetConfig)
rg.POST("/config/update", h.UpdateConfig) rg.POST("/config/update", h.UpdateConfig)
@@ -107,24 +113,6 @@ func (h *AdminJimengHandler) JobDetail(c *gin.Context) {
resp.SUCCESS(c, job) resp.SUCCESS(c, job)
} }
// Remove 删除任务
func (h *AdminJimengHandler) Remove(c *gin.Context) {
idStr := c.Param("id")
jobId, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
resp.ERROR(c, "参数错误")
return
}
err = h.DB.Where("id = ?", jobId).Delete(&model.JimengJob{}).Error
if err != nil {
resp.ERROR(c, "删除任务失败")
return
}
resp.SUCCESS(c, gin.H{})
}
// BatchRemove 批量删除任务 // BatchRemove 批量删除任务
func (h *AdminJimengHandler) BatchRemove(c *gin.Context) { func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
var req struct { var req struct {
@@ -136,23 +124,57 @@ func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
return return
} }
result := h.DB.Where("id IN ?", req.JobIds).Delete(&model.JimengJob{}) var deletedCount int64 = 0
if result.Error != nil { for _, jobId := range req.JobIds {
resp.ERROR(c, "批量删除失败") var job model.JimengJob
return err := h.DB.Where("id = ?", jobId).First(&job).Error
if err != nil {
continue // 跳过不存在的
}
tx := h.DB.Begin()
if job.Status != model.JMTaskStatusSuccess && job.Power > 0 {
remark := fmt.Sprintf("任务未成功退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "jimeng",
Remark: remark,
})
if err != nil {
tx.Rollback()
continue
}
}
err = tx.Where("id = ?", jobId).Delete(&model.JimengJob{}).Error
if err != nil {
tx.Rollback()
continue
}
tx.Commit()
deletedCount++
if job.ImgURL != "" {
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
}
if job.VideoURL != "" {
err = h.uploader.GetUploadHandler().Delete(job.VideoURL)
if err != nil {
logger.Error("remove video failed: ", err)
}
}
} }
resp.SUCCESS(c, gin.H{ resp.SUCCESS(c, gin.H{
"message": "批量删除成功", "message": "批量删除成功",
"deleted_count": result.RowsAffected, "deleted_count": deletedCount,
}) })
} }
// Stats 获取统计信息 // Stats 获取统计信息
func (h *AdminJimengHandler) Stats(c *gin.Context) { func (h *AdminJimengHandler) Stats(c *gin.Context) {
type StatResult struct { type StatResult struct {
Status string `json:"status"` Status model.JMTaskStatus `json:"status"`
Count int64 `json:"count"` Count int64 `json:"count"`
} }
var stats []StatResult var stats []StatResult
@@ -177,14 +199,14 @@ func (h *AdminJimengHandler) Stats(c *gin.Context) {
for _, stat := range stats { for _, stat := range stats {
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
switch stat.Status { switch stat.Status {
case "completed": case model.JMTaskStatusInQueue:
result["completedTasks"] = stat.Count
case "processing":
result["processingTasks"] = stat.Count
case "failed":
result["failedTasks"] = stat.Count
case "pending":
result["pendingTasks"] = stat.Count result["pendingTasks"] = stat.Count
case model.JMTaskStatusSuccess:
result["completedTasks"] = stat.Count
case model.JMTaskStatusGenerating:
result["processingTasks"] = stat.Count
case model.JMTaskStatusFailed:
result["failedTasks"] = stat.Count
} }
} }
@@ -193,33 +215,7 @@ func (h *AdminJimengHandler) Stats(c *gin.Context) {
// GetConfig 获取即梦AI配置 // GetConfig 获取即梦AI配置
func (h *AdminJimengHandler) GetConfig(c *gin.Context) { func (h *AdminJimengHandler) GetConfig(c *gin.Context) {
var config model.Config jimengConfig := h.jimengService.GetConfig()
err := h.DB.Debug().Where("name", "jimeng").First(&config).Error
if err != nil {
// 如果配置不存在,返回默认配置
defaultConfig := types.JimengConfig{
AccessKey: "",
SecretKey: "",
Power: types.JimengPower{
TextToImage: 10,
ImageToImage: 15,
ImageEdit: 20,
ImageEffects: 25,
TextToVideo: 30,
ImageToVideo: 35,
},
}
resp.SUCCESS(c, defaultConfig)
return
}
var jimengConfig types.JimengConfig
err = utils.JsonDecode(config.Value, &jimengConfig)
if err != nil {
resp.ERROR(c, "解析配置失败: "+err.Error())
return
}
resp.SUCCESS(c, jimengConfig) resp.SUCCESS(c, jimengConfig)
} }

View File

@@ -8,6 +8,8 @@ import (
"geekai/core/types" "geekai/core/types"
"geekai/service/jimeng" "geekai/service/jimeng"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -33,7 +35,7 @@ func (h *JimengHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/jimeng") rg := h.App.Engine.Group("/api/jimeng")
rg.POST("task", h.CreateTask) // 只保留统一任务接口 rg.POST("task", h.CreateTask) // 只保留统一任务接口
rg.GET("power-config", h.GetPowerConfig) // 新增算力配置接口 rg.GET("power-config", h.GetPowerConfig) // 新增算力配置接口
rg.GET("jobs", h.Jobs) rg.POST("jobs", h.Jobs)
rg.GET("remove", h.Remove) rg.GET("remove", h.Remove)
rg.GET("retry", h.Retry) rg.GET("retry", h.Retry)
} }
@@ -253,28 +255,66 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
// Jobs 获取任务列表 // Jobs 获取任务列表
func (h *JimengHandler) Jobs(c *gin.Context) { func (h *JimengHandler) Jobs(c *gin.Context) {
user, err := h.GetLoginUser(c) userId := h.GetLoginUserId(c)
if err != nil {
resp.NotAuth(c) var req struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Filter string `json:"filter"`
Ids []uint `json:"ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, types.InvalidArgs)
return return
} }
page := h.GetInt(c, "page", 1) var jobs []model.JimengJob
pageSize := h.GetInt(c, "page_size", 20) var total int64
query := h.DB.Model(&model.JimengJob{}).Where("user_id = ?", userId)
if req.Filter == "image" {
query = query.Where("type IN (?)", []model.JMTaskType{
model.JMTaskTypeTextToImage,
model.JMTaskTypeImageToImage,
model.JMTaskTypeImageEdit,
model.JMTaskTypeImageEffects,
})
} else if req.Filter == "video" {
query = query.Where("type IN (?)", []model.JMTaskType{
model.JMTaskTypeTextToVideo,
model.JMTaskTypeImageToVideo,
})
}
jobs, total, err := h.jimengService.GetUserJobs(user.Id, page, pageSize) if len(req.Ids) > 0 {
if err != nil { query = query.Where("id IN (?)", req.Ids)
logger.Errorf("get user jimeng jobs failed: %v", err) }
resp.ERROR(c, "获取任务列表失败")
// 统计总数
if err := query.Count(&total).Error; err != nil {
resp.ERROR(c, err.Error())
return return
} }
resp.SUCCESS(c, gin.H{ // 分页查询
"jobs": jobs, offset := (req.Page - 1) * req.PageSize
"total": total, if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&jobs).Error; err != nil {
"page": page, resp.ERROR(c, err.Error())
"page_size": pageSize, return
}) }
// 填充 VO
var jobVos []vo.JimengJob
for _, job := range jobs {
var jobVo vo.JimengJob
err := utils.CopyObject(job, &jobVo)
if err != nil {
continue
}
jobVo.CreatedAt = job.CreatedAt.Unix()
jobVos = append(jobVos, jobVo)
}
resp.SUCCESS(c, vo.NewPage(total, req.Page, req.PageSize, jobVos))
} }
// Remove 删除任务 // Remove 删除任务
@@ -355,7 +395,7 @@ func (h *JimengHandler) Retry(c *gin.Context) {
} }
// 重新推送到队列 // 重新推送到队列
task := map[string]interface{}{ task := map[string]any{
"job_id": jobId, "job_id": jobId,
"type": job.Type, "type": job.Type,
} }
@@ -393,27 +433,7 @@ func (h *JimengHandler) subUserPower(userId uint, power int, powerLog model.Powe
// getPowerFromConfig 从配置中获取指定类型的算力消耗 // getPowerFromConfig 从配置中获取指定类型的算力消耗
func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int { func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
config, err := h.jimengService.GetConfig() config := h.jimengService.GetConfig()
if err != nil {
logger.Errorf("获取即梦AI配置失败: %v", err)
// 返回默认值
switch taskType {
case model.JMTaskTypeTextToImage:
return 10
case model.JMTaskTypeImageToImage:
return 15
case model.JMTaskTypeImageEdit:
return 20
case model.JMTaskTypeImageEffects:
return 25
case model.JMTaskTypeTextToVideo:
return 30
case model.JMTaskTypeImageToVideo:
return 35
default:
return 10
}
}
switch taskType { switch taskType {
case model.JMTaskTypeTextToImage: case model.JMTaskTypeTextToImage:
@@ -435,11 +455,7 @@ func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
// GetPowerConfig 获取即梦各任务类型算力消耗配置 // GetPowerConfig 获取即梦各任务类型算力消耗配置
func (h *JimengHandler) GetPowerConfig(c *gin.Context) { func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
config, err := h.jimengService.GetConfig() config := h.jimengService.GetConfig()
if err != nil || config == nil {
resp.ERROR(c, "获取算力配置失败")
return
}
resp.SUCCESS(c, gin.H{ resp.SUCCESS(c, gin.H{
"text_to_image": config.Power.TextToImage, "text_to_image": config.Power.TextToImage,
"image_to_image": config.Power.ImageToImage, "image_to_image": config.Power.ImageToImage,

View File

@@ -609,12 +609,15 @@ func (s *Service) GetJob(jobId uint) (*model.JimengJob, error) {
return &job, nil return &job, nil
} }
// GetUserJobs 获取用户任务列表 // GetJobByPage 分页获取任务列表
func (s *Service) GetUserJobs(userId uint, page, pageSize int) ([]*model.JimengJob, int64, error) { func (s *Service) GetJobByPage(userId uint, page, pageSize int) ([]*model.JimengJob, int64, error) {
var jobs []*model.JimengJob var jobs []*model.JimengJob
var total int64 var total int64
query := s.db.Model(&model.JimengJob{}).Where("user_id = ?", userId) query := s.db.Model(&model.JimengJob{})
if userId > 0 {
query = query.Where("user_id = ?", userId)
}
// 统计总数 // 统计总数
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
@@ -688,8 +691,17 @@ func (s *Service) UpdateClientConfig(accessKey, secretKey string) error {
return nil return nil
} }
var defaultPower = types.JimengPower{
TextToImage: 20,
ImageToImage: 20,
ImageEdit: 20,
ImageEffects: 20,
TextToVideo: 300,
ImageToVideo: 300,
}
// GetConfig 获取即梦AI配置 // GetConfig 获取即梦AI配置
func (s *Service) GetConfig() (*types.JimengConfig, error) { func (s *Service) GetConfig() *types.JimengConfig {
var config model.Config var config model.Config
err := s.db.Where("name", "jimeng").First(&config).Error err := s.db.Where("name", "jimeng").First(&config).Error
if err != nil { if err != nil {
@@ -697,22 +709,19 @@ func (s *Service) GetConfig() (*types.JimengConfig, error) {
return &types.JimengConfig{ return &types.JimengConfig{
AccessKey: "", AccessKey: "",
SecretKey: "", SecretKey: "",
Power: types.JimengPower{ Power: defaultPower,
TextToImage: 10, }
ImageToImage: 15,
ImageEdit: 20,
ImageEffects: 25,
TextToVideo: 30,
ImageToVideo: 35,
},
}, nil
} }
var jimengConfig types.JimengConfig var jimengConfig types.JimengConfig
err = utils.JsonDecode(config.Value, &jimengConfig) err = utils.JsonDecode(config.Value, &jimengConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err) return &types.JimengConfig{
AccessKey: "",
SecretKey: "",
Power: defaultPower,
}
} }
return &jimengConfig, nil return &jimengConfig
} }

View File

@@ -1,21 +1,23 @@
package vo package vo
import "geekai/store/model"
// JimengJob 即梦AI任务VO // JimengJob 即梦AI任务VO
type JimengJob struct { type JimengJob struct {
Id uint `json:"id"` Id uint `json:"id"`
UserId uint `json:"user_id"` UserId uint `json:"user_id"`
TaskId string `json:"task_id"` TaskId string `json:"task_id"`
Type string `json:"type"` Type model.JMTaskType `json:"type"`
ReqKey string `json:"req_key"` ReqKey string `json:"req_key"`
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
TaskParams string `json:"task_params"` TaskParams string `json:"task_params"`
ImgURL string `json:"img_url"` ImgURL string `json:"img_url"`
VideoURL string `json:"video_url"` VideoURL string `json:"video_url"`
RawData string `json:"raw_data"` RawData string `json:"raw_data"`
Progress int `json:"progress"` Progress int `json:"progress"`
Status string `json:"status"` Status model.JMTaskStatus `json:"status"`
ErrMsg string `json:"err_msg"` ErrMsg string `json:"err_msg"`
Power int `json:"power"` Power int `json:"power"`
CreatedAt int64 `json:"created_at"` // 时间戳 CreatedAt int64 `json:"created_at"` // 时间戳
UpdatedAt int64 `json:"updated_at"` // 时间戳 UpdatedAt int64 `json:"updated_at"` // 时间戳
} }

View File

@@ -11,9 +11,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/nfnt/resize"
"github.com/skip2/go-qrcode"
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
@@ -22,11 +19,22 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/nfnt/resize"
"github.com/skip2/go-qrcode"
) )
// CopyObject 拷贝对象 // CopyObject 拷贝对象
func CopyObject(src interface{}, dst interface{}) error { func CopyObject(src interface{}, dst interface{}) error {
// 这里做异常处理
defer func() {
if r := recover(); r != nil {
logger.Errorf("copy object failed: %v", r)
}
}()
srcType := reflect.TypeOf(src) srcType := reflect.TypeOf(src)
srcValue := reflect.ValueOf(src) srcValue := reflect.ValueOf(src)
dstValue := reflect.ValueOf(dst).Elem() dstValue := reflect.ValueOf(dst).Elem()

View File

@@ -235,8 +235,6 @@
border-radius: 12px; border-radius: 12px;
box-shadow: var(--card-shadow, 0 2px 8px rgba(0,0,0,0.1)); box-shadow: var(--card-shadow, 0 2px 8px rgba(0,0,0,0.1));
overflow: hidden; overflow: hidden;
min-height: 420px;
height: 100%;
transition: box-shadow 0.2s; transition: box-shadow 0.2s;
&:hover { &:hover {
box-shadow: 0 4px 24px rgba(88,101,242,0.12); box-shadow: 0 4px 24px rgba(88,101,242,0.12);

View File

@@ -5,7 +5,6 @@
// * @Author yangjian102621@163.com // * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import nodata from '@/assets/img/no-data.png'
import { checkSession } from '@/store/cache' import { checkSession } from '@/store/cache'
import { showMessageError, showMessageOK } from '@/utils/dialog' import { showMessageError, showMessageOK } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http' import { httpGet, httpPost } from '@/utils/http'
@@ -26,8 +25,6 @@ export const useJimengStore = defineStore('jimeng', () => {
// 共同状态 // 共同状态
const loading = ref(false) const loading = ref(false)
const submitting = ref(false) const submitting = ref(false)
const list = ref([])
const noData = ref(true)
const page = ref(1) const page = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
const total = ref(0) const total = ref(0)
@@ -186,6 +183,8 @@ export const useJimengStore = defineStore('jimeng', () => {
userPower.value = user.power userPower.value = user.power
// 获取任务列表 // 获取任务列表
await fetchData(1) await fetchData(1)
// 开始轮询
startPolling()
} catch (error) { } catch (error) {
console.error('初始化失败:', error) console.error('初始化失败:', error)
} }
@@ -257,58 +256,40 @@ export const useJimengStore = defineStore('jimeng', () => {
// 切换任务筛选 // 切换任务筛选
const switchTaskFilter = (filter) => { const switchTaskFilter = (filter) => {
taskFilter.value = filter taskFilter.value = filter
updateCurrentList() fetchData(1)
}
// 更新当前列表
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)
)
}
} }
// 轮询定时器 // 轮询定时器
let pollHandler = null let pollHandler = null
// 获取任务列表 // 获取任务列表
const fetchData = async (pageNum = 1) => { const fetchData = async (pageNum = 1) => {
try { try {
loading.value = true loading.value = true
page.value = pageNum page.value = pageNum
const response = await httpGet('/api/jimeng/jobs', { const response = await httpPost('/api/jimeng/jobs', {
page: pageNum, page: pageNum,
page_size: pageSize.value, page_size: pageSize.value,
filter: taskFilter.value,
}) })
const data = response.data
if (data.total === 0) {
isOver.value = true
currentList.value = []
return
}
if (response.data) { total.value = data.total || 0
list.value = response.data.jobs || [] if (data.items.length < pageSize.value) {
total.value = response.data.total || 0 isOver.value = true
noData.value = list.value.length === 0 }
updateCurrentList() if (pageNum === 1) {
// 判断是否有未完成任务 currentList.value = data.items
const hasPending = list.value.some( } else {
(item) => item.status === 'in_queue' || item.status === 'processing' currentList.value = currentList.value.concat(data.items)
)
if (hasPending) {
startPolling()
} else {
stopPolling()
}
} }
} catch (error) { } catch (error) {
console.error('获取任务列表失败:', error) showMessageError('获取任务列表失败:' + error.message)
showMessageError('获取任务列表失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -317,8 +298,30 @@ export const useJimengStore = defineStore('jimeng', () => {
// 简单轮询逻辑 // 简单轮询逻辑
const startPolling = () => { const startPolling = () => {
if (pollHandler) return if (pollHandler) return
pollHandler = setInterval(() => { pollHandler = setInterval(async () => {
fetchData(page.value) const response = await httpPost('/api/jimeng/jobs', {
page: 1,
page_size: 20,
})
const data = response.data
if (data.items.length === 0) {
stopPolling()
return
}
const todoList = data.items.filter(
(item) => item.status === 'in_queue' || item.status === 'generating'
)
// 更新当前列表
currentList.value.forEach((item) => {
const index = data.items.findIndex((i) => i.id === item.id)
if (index !== -1) {
Object.assign(item, data.items[index])
}
})
if (todoList.length === 0) {
stopPolling()
}
}, 5000) }, 5000)
} }
@@ -533,18 +536,16 @@ export const useJimengStore = defineStore('jimeng', () => {
useImageInput, useImageInput,
loading, loading,
submitting, submitting,
list,
noData,
page, page,
pageSize, pageSize,
total, total,
taskFilter, taskFilter,
currentList, currentList,
isOver,
isLogin, isLogin,
userPower, userPower,
showDialog, showDialog,
currentVideoUrl, currentVideoUrl,
nodata,
// 配置 // 配置
categories, categories,
@@ -577,7 +578,6 @@ export const useJimengStore = defineStore('jimeng', () => {
getTaskStatusText, getTaskStatusText,
getStatusType, getStatusType,
switchTaskFilter, switchTaskFilter,
updateCurrentList,
fetchData, fetchData,
submitTask, submitTask,
retryTask, retryTask,

View File

@@ -276,7 +276,7 @@
</div> </div>
<!-- 右侧任务列表 --> <!-- 右侧任务列表 -->
<div class="main-content" v-loading="store.loading"> <div class="main-content">
<div class="works-header"> <div class="works-header">
<h2 class="h-title">你的作品</h2> <h2 class="h-title">你的作品</h2>
<div class="filter-buttons"> <div class="filter-buttons">
@@ -306,119 +306,134 @@
</div> </div>
</div> </div>
<div class="task-list"> <div class="task-list" v-loading="store.loading">
<Waterfall <div v-if="store.currentList.length > 0">
:list="store.currentList" <Waterfall
v-bind="waterfallOptions" :list="store.currentList"
:is-loading="store.loading" v-bind="waterfallOptions"
:is-over="store.currentList.length >= store.total" :is-loading="store.loading"
@afterRender="onWaterfallAfterRender" :is-over="store.isOver"
> @afterRender="onWaterfallAfterRender"
<template #default="{ item }"> >
<div class="task-item"> <template #default="{ item }">
<!-- 保持原有内容 --> <div class="task-item">
<div class="task-left"> <!-- 保持原有内容 -->
<div class="task-preview"> <div class="task-left">
<el-image <div class="task-preview">
v-if="item.img_url" <el-image
:src="item.img_url" v-if="item.img_url"
fit="cover" :src="item.img_url"
class="preview-image" fit="cover"
/> class="preview-image"
<video />
v-else-if="item.video_url" <video
:src="item.video_url" v-else-if="item.video_url"
class="preview-video" :src="item.video_url"
preload="metadata" class="preview-video"
/> preload="metadata"
<div v-else class="preview-placeholder"> />
<i class="iconfont icon-dalle text-2xl" v-if="item.type.includes('image')"></i> <div v-else class="preview-placeholder">
<i <i
class="iconfont icon-video text-2xl" class="iconfont icon-video text-2xl"
v-else-if="item.type.includes('video')" v-if="item.type.includes('video')"
></i> ></i>
<span>{{ store.getTaskStatusText(item.status) }}</span> <i class="iconfont icon-dalle text-2xl" v-else></i>
<span>{{ store.getTaskStatusText(item.status) }}</span>
</div>
</div> </div>
</div> </div>
</div> <div class="task-center">
<div class="task-center"> <div class="task-info flex justify-between">
<div class="task-info flex justify-between"> <div class="flex gap-2">
<div class="flex gap-2"> <el-tag size="small" :type="store.getStatusType(item.status)">
<el-tag size="small" :type="store.getStatusType(item.status)"> {{ store.getTaskStatusText(item.status) }}
{{ store.getTaskStatusText(item.status) }} </el-tag>
</el-tag> <el-tag size="small">{{ store.getFunctionName(item.type) }}</el-tag>
<el-tag size="small">{{ store.getFunctionName(item.type) }}</el-tag> </div>
</div> <div class="flex gap-2">
<div class="flex gap-2"> <span>
<span> <el-tooltip content="复制提示词" placement="top">
<el-tooltip content="复制提示词" placement="top"> <i
<i class="iconfont icon-copy cursor-pointer"
class="iconfont icon-copy cursor-pointer" @click="copyPrompt(item.prompt)"
@click="copyPrompt(item.prompt)" ></i>
></i> </el-tooltip>
</el-tooltip> </span>
</span>
<span class="ml-1"> <span class="ml-1">
<el-tooltip content="画同款" placement="top"> <el-tooltip content="画同款" placement="top">
<i <i
class="iconfont icon-image-list cursor-pointer" class="iconfont icon-image-list cursor-pointer"
@click="store.drawSame(item)" @click="store.drawSame(item)"
></i> ></i>
</el-tooltip> </el-tooltip>
</span> </span>
</div>
</div>
<div
class="task-prompt line-clamp-2 min-h-[40px] text-[14px] text-theme mb-2 leading-snug break-all"
>
{{ 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> </div>
<div <div class="task-right">
class="task-prompt line-clamp-2 min-h-[40px] text-[14px] text-theme mb-2 leading-snug break-all" <div class="task-actions">
> <el-button
{{ store.substr(item.prompt, 200) }} v-if="item.status === 'failed'"
</div> type="primary"
<div class="task-meta"> size="small"
<span>{{ dateFormat(item.created_at) }}</span> @click="store.retryTask(item.id)"
<span v-if="item.power">{{ item.power }}算力</span> >
重试
</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"
v-if="item.status === 'failed'"
size="small"
@click="store.removeJob(item)"
>
删除
</el-button>
</div>
</div> </div>
</div> </div>
<div class="task-right"> </template>
<div class="task-actions"> </Waterfall>
<el-button <div class="flex justify-center py-10">
v-if="item.status === 'failed'" <img
type="primary" :src="waterfallOptions.loadProps.loading"
size="small" class="max-w-[50px] max-h-[50px]"
@click="store.retryTask(item.id)" v-if="store.loading"
> />
重试 <div v-else>
</el-button> <div class="no-more-data" v-if="store.isOver">
<el-button <span class="text-gray-500 mr-2">没有更多数据了</span>
v-if="item.video_url || item.img_url" <i class="iconfont icon-face"></i>
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"
v-if="item.status === 'failed'"
size="small"
@click="store.removeJob(item)"
>
删除
</el-button>
</div>
</div> </div>
</div> </div>
</template> </div>
</Waterfall> </div>
<el-empty v-if="store.noData" :image="store.nodata" description="暂无任务,快去创建吧!" /> <el-empty v-else :image-size="100" description="暂无记录" />
</div> </div>
</div> </div>
@@ -467,9 +482,8 @@ onUnmounted(() => {
store.cleanup() store.cleanup()
}) })
// 自动加载下一页逻辑
function onWaterfallAfterRender() { function onWaterfallAfterRender() {
if (!store.loading && store.currentList.length < store.total) { if (!store.loading && !store.isOver) {
store.fetchData(store.page + 1) store.fetchData(store.page + 1)
} }
} }

View File

@@ -1,10 +1,48 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 页面标题 --> <!-- 统计信息 -->
<div class="page-header"> <el-row :gutter="20" class="stats-row">
<h2>即梦AI任务管理</h2> <el-col :span="4">
<p>管理所有用户的即梦AI任务查看任务详情和统计信息</p> <el-card class="stat-card">
</div> <div class="stat-item">
<div class="stat-number">{{ stats.totalTasks }}</div>
<div class="stat-label">总任务数</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card class="stat-card">
<div class="stat-item">
<div class="stat-number !text-blue-500">{{ stats.pendingTasks }}</div>
<div class="stat-label">排队中</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<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="4">
<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="4">
<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="filter-card" shadow="never"> <el-card class="filter-card" shadow="never">
@@ -18,9 +56,15 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="任务类型"> <el-form-item label="任务类型">
<el-select v-model="queryForm.type" placeholder="请选择任务类型" clearable style="width: 150px"> <el-select
v-model="queryForm.type"
placeholder="请选择任务类型"
clearable
style="width: 150px"
@change="handleQuery"
>
<el-option label="文生图" value="text_to_image" /> <el-option label="文生图" value="text_to_image" />
<el-option label="图生图" value="image_to_image_portrait" /> <el-option label="图生图" value="image_to_image" />
<el-option label="图像编辑" value="image_edit" /> <el-option label="图像编辑" value="image_edit" />
<el-option label="图像特效" value="image_effects" /> <el-option label="图像特效" value="image_effects" />
<el-option label="文生视频" value="text_to_video" /> <el-option label="文生视频" value="text_to_video" />
@@ -28,66 +72,32 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="任务状态"> <el-form-item label="任务状态">
<el-select v-model="queryForm.status" placeholder="请选择状态" clearable style="width: 120px"> <el-select
<el-option label="等待中" value="pending" /> v-model="queryForm.status"
<el-option label="处理中" value="processing" /> placeholder="请选择状态"
<el-option label="已完成" value="completed" /> clearable
style="width: 120px"
@change="handleQuery"
>
<el-option label="等待中" value="in_queue" />
<el-option label="处理中" value="generating" />
<el-option label="已完成" value="success" />
<el-option label="失败" value="failed" /> <el-option label="失败" value="failed" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleQuery" :loading="loading"> <el-button type="primary" @click="handleQuery" :loading="loading">
<el-icon><Search /></el-icon> <i class="iconfont icon-search mr-1" />
搜索 搜索
</el-button> </el-button>
<el-button @click="resetQuery">
<el-icon><Refresh /></el-icon>
重置
</el-button>
<el-button type="danger" @click="handleBatchDelete" :disabled="!multipleSelection.length"> <el-button type="danger" @click="handleBatchDelete" :disabled="!multipleSelection.length">
<el-icon><Delete /></el-icon> <i class="iconfont icon-remove mr-1" />
批量删除 批量删除
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </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-card class="table-card">
<el-table <el-table
@@ -126,22 +136,9 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="150" fixed="right"> <el-table-column label="操作" width="150" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button <el-button type="primary" size="small" text @click="handleViewDetail(scope.row)">
type="primary"
size="small"
text
@click="handleViewDetail(scope.row)"
>
详情 详情
</el-button> </el-button>
<el-button
type="danger"
size="small"
text
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -170,21 +167,33 @@
<div class="detail-content" v-if="detailDialog.data"> <div class="detail-content" v-if="detailDialog.data">
<el-descriptions :column="2" border> <el-descriptions :column="2" border>
<el-descriptions-item label="任务ID">{{ detailDialog.data.id }}</el-descriptions-item> <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="用户ID">{{
<el-descriptions-item label="任务类型">{{ getTaskTypeName(detailDialog.data.type) }}</el-descriptions-item> detailDialog.data.user_id
}}</el-descriptions-item>
<el-descriptions-item label="任务类型">{{
getTaskTypeName(detailDialog.data.type)
}}</el-descriptions-item>
<el-descriptions-item label="状态"> <el-descriptions-item label="状态">
<el-tag :type="getStatusColor(detailDialog.data.status)"> <el-tag :type="getStatusColor(detailDialog.data.status)">
{{ getStatusName(detailDialog.data.status) }} {{ getStatusName(detailDialog.data.status) }}
</el-tag> </el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="进度">{{ detailDialog.data.progress }}%</el-descriptions-item> <el-descriptions-item label="进度"
<el-descriptions-item label="算力消耗">{{ detailDialog.data.power }}</el-descriptions-item> >{{ detailDialog.data.progress }}%</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-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> </el-descriptions>
<div class="detail-section"> <div class="detail-section">
<h4>提示词</h4> <h4 class="text-base pt-2 font-bold">提示词</h4>
<div class="prompt-content">{{ detailDialog.data.prompt || '无' }}</div> <div class="prompt-content">{{ detailDialog.data.prompt || '无' }}</div>
</div> </div>
@@ -243,24 +252,23 @@
</template> </template>
<script setup> <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' import { httpGet, httpPost } from '@/utils/http'
import { formatDateTime } from '@/utils/libs'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, onMounted, reactive, ref } from 'vue'
// 查询表单 // 查询表单
const queryForm = reactive({ const queryForm = reactive({
user_id: '', user_id: '',
type: '', type: '',
status: '' status: '',
}) })
// 分页信息 // 分页信息
const pagination = reactive({ const pagination = reactive({
page: 1, page: 1,
size: 20, size: 20,
total: 0 total: 0,
}) })
// 数据 // 数据
@@ -274,13 +282,13 @@ const stats = reactive({
totalTasks: 0, totalTasks: 0,
completedTasks: 0, completedTasks: 0,
processingTasks: 0, processingTasks: 0,
failedTasks: 0 failedTasks: 0,
}) })
// 详情对话框 // 详情对话框
const detailDialog = reactive({ const detailDialog = reactive({
visible: false, visible: false,
data: {} data: {},
}) })
// 格式化原始数据 // 格式化原始数据
@@ -296,12 +304,12 @@ const formattedRawData = computed(() => {
// 获取任务类型名称 // 获取任务类型名称
const getTaskTypeName = (type) => { const getTaskTypeName = (type) => {
const typeMap = { const typeMap = {
'text_to_image': '文生图', text_to_image: '文生图',
'image_to_image_portrait': '图生图', image_to_image: '图生图',
'image_edit': '图像编辑', image_edit: '图像编辑',
'image_effects': '图像特效', image_effects: '图像特效',
'text_to_video': '文生视频', text_to_video: '文生视频',
'image_to_video': '图生视频' image_to_video: '图生视频',
} }
return typeMap[type] || type return typeMap[type] || type
} }
@@ -309,10 +317,10 @@ const getTaskTypeName = (type) => {
// 获取状态名称 // 获取状态名称
const getStatusName = (status) => { const getStatusName = (status) => {
const statusMap = { const statusMap = {
'pending': '等待中', in_queue: '等待中',
'processing': '处理中', generating: '处理中',
'completed': '已完成', success: '已完成',
'failed': '失败' failed: '失败',
} }
return statusMap[status] || status return statusMap[status] || status
} }
@@ -320,10 +328,10 @@ const getStatusName = (status) => {
// 获取状态颜色 // 获取状态颜色
const getStatusColor = (status) => { const getStatusColor = (status) => {
const colorMap = { const colorMap = {
'pending': '', in_queue: '',
'processing': 'warning', generating: 'warning',
'completed': 'success', success: 'success',
'failed': 'danger' failed: 'danger',
} }
return colorMap[status] || '' return colorMap[status] || ''
} }
@@ -335,7 +343,7 @@ const getTaskList = async () => {
const params = { const params = {
page: pagination.page, page: pagination.page,
page_size: pagination.size, page_size: pagination.size,
...queryForm ...queryForm,
} }
const response = await httpGet('/api/admin/jimeng/jobs', params) const response = await httpGet('/api/admin/jimeng/jobs', params)
@@ -364,18 +372,6 @@ const handleQuery = () => {
getTaskList() getTaskList()
} }
// 重置查询
const resetQuery = () => {
queryFormRef.value?.resetFields()
Object.assign(queryForm, {
user_id: '',
type: '',
status: ''
})
pagination.page = 1
getTaskList()
}
// 选择变化 // 选择变化
const handleSelectionChange = (selection) => { const handleSelectionChange = (selection) => {
multipleSelection.value = selection multipleSelection.value = selection
@@ -384,7 +380,7 @@ const handleSelectionChange = (selection) => {
// 查看详情 // 查看详情
const handleViewDetail = async (row) => { const handleViewDetail = async (row) => {
try { try {
const response = await httpGet(`/api/admin/jimeng/job/${row.id}`) const response = await httpGet(`/api/admin/jimeng/jobs/${row.id}`)
detailDialog.data = response.data detailDialog.data = response.data
detailDialog.visible = true detailDialog.visible = true
} catch (error) { } catch (error) {
@@ -392,26 +388,6 @@ const handleViewDetail = async (row) => {
} }
} }
// 删除任务
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 () => { const handleBatchDelete = async () => {
if (!multipleSelection.value.length) { if (!multipleSelection.value.length) {
@@ -420,14 +396,18 @@ const handleBatchDelete = async () => {
} }
try { try {
await ElMessageBox.confirm(`确定要删除选中的 ${multipleSelection.value.length} 个任务吗?`, '提示', { await ElMessageBox.confirm(
confirmButtonText: '确定', `确定要删除选中的 ${multipleSelection.value.length} 个任务吗?`,
cancelButtonText: '取消', '提示',
type: 'warning' {
}) confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
const jobIds = multipleSelection.value.map(item => item.id) const jobIds = multipleSelection.value.map((item) => item.id)
await httpPost('/api/admin/jimeng/batch-remove', { job_ids: jobIds }) await httpPost('/api/admin/jimeng/jobs/remove', { job_ids: jobIds })
ElMessage.success('批量删除成功') ElMessage.success('批量删除成功')
getTaskList() getTaskList()
getStats() getStats()
@@ -458,10 +438,13 @@ onMounted(() => {
}) })
</script> </script>
<style lang="stylus" scoped> <style lang="stylus">
.app-container .app-container
padding 20px padding 20px
.el-form-item
margin-bottom 0
.page-header .page-header
margin-bottom 20px margin-bottom 20px