mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-28 22:14:28 +08:00
即梦 AI 管理后台功能完成
This commit is contained in:
@@ -194,7 +194,6 @@ func (h *ImageHandler) Remove(c *gin.Context) {
|
|||||||
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg)
|
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %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:%d,Err: %s", job.Id, job.ErrMsg)
|
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %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:%d,Err: %s", job.Id, job.ErrMsg)
|
remark = fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %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
|
||||||
|
|||||||
@@ -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:%d,Err: %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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"` // 时间戳
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +343,9 @@ 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)
|
||||||
taskList.value = response.data.jobs || []
|
taskList.value = response.data.jobs || []
|
||||||
pagination.total = response.data.total || 0
|
pagination.total = response.data.total || 0
|
||||||
@@ -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,42 +388,26 @@ 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) {
|
||||||
ElMessage.warning('请选择要删除的任务')
|
ElMessage.warning('请选择要删除的任务')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确定要删除选中的 ${multipleSelection.value.length} 个任务吗?`, '提示', {
|
await ElMessageBox.confirm(
|
||||||
confirmButtonText: '确定',
|
`确定要删除选中的 ${multipleSelection.value.length} 个任务吗?`,
|
||||||
cancelButtonText: '取消',
|
'提示',
|
||||||
type: 'warning'
|
{
|
||||||
})
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
const jobIds = multipleSelection.value.map(item => item.id)
|
type: 'warning',
|
||||||
await httpPost('/api/admin/jimeng/batch-remove', { job_ids: jobIds })
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const jobIds = multipleSelection.value.map((item) => item.id)
|
||||||
|
await httpPost('/api/admin/jimeng/jobs/remove', { job_ids: jobIds })
|
||||||
ElMessage.success('批量删除成功')
|
ElMessage.success('批量删除成功')
|
||||||
getTaskList()
|
getTaskList()
|
||||||
getStats()
|
getStats()
|
||||||
@@ -458,17 +438,20 @@ 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
|
||||||
|
|
||||||
h2
|
h2
|
||||||
margin 0 0 8px 0
|
margin 0 0 8px 0
|
||||||
color #303133
|
color #303133
|
||||||
|
|
||||||
p
|
p
|
||||||
margin 0
|
margin 0
|
||||||
color #606266
|
color #606266
|
||||||
@@ -484,22 +467,22 @@ onMounted(() => {
|
|||||||
.stat-item
|
.stat-item
|
||||||
text-align center
|
text-align center
|
||||||
padding 20px
|
padding 20px
|
||||||
|
|
||||||
.stat-number
|
.stat-number
|
||||||
font-size 28px
|
font-size 28px
|
||||||
font-weight bold
|
font-weight bold
|
||||||
color #303133
|
color #303133
|
||||||
margin-bottom 8px
|
margin-bottom 8px
|
||||||
|
|
||||||
&.success
|
&.success
|
||||||
color #67c23a
|
color #67c23a
|
||||||
|
|
||||||
&.warning
|
&.warning
|
||||||
color #e6a23c
|
color #e6a23c
|
||||||
|
|
||||||
&.danger
|
&.danger
|
||||||
color #f56c6c
|
color #f56c6c
|
||||||
|
|
||||||
.stat-label
|
.stat-label
|
||||||
font-size 14px
|
font-size 14px
|
||||||
color #909399
|
color #909399
|
||||||
@@ -513,31 +496,31 @@ onMounted(() => {
|
|||||||
.detail-content
|
.detail-content
|
||||||
.detail-section
|
.detail-section
|
||||||
margin-bottom 20px
|
margin-bottom 20px
|
||||||
|
|
||||||
h4
|
h4
|
||||||
margin 0 0 10px 0
|
margin 0 0 10px 0
|
||||||
color #303133
|
color #303133
|
||||||
font-size 16px
|
font-size 16px
|
||||||
|
|
||||||
.prompt-content
|
.prompt-content
|
||||||
background #f5f7fa
|
background #f5f7fa
|
||||||
padding 12px
|
padding 12px
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
color #606266
|
color #606266
|
||||||
line-height 1.6
|
line-height 1.6
|
||||||
|
|
||||||
.params-content, .raw-data-content
|
.params-content, .raw-data-content
|
||||||
font-family monospace
|
font-family monospace
|
||||||
|
|
||||||
.result-content
|
.result-content
|
||||||
.result-item
|
.result-item
|
||||||
margin-bottom 10px
|
margin-bottom 10px
|
||||||
display flex
|
display flex
|
||||||
align-items center
|
align-items center
|
||||||
gap 10px
|
gap 10px
|
||||||
|
|
||||||
label
|
label
|
||||||
font-weight bold
|
font-weight bold
|
||||||
color #303133
|
color #303133
|
||||||
min-width 50px
|
min-width 50px
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user