mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-06 11:14:24 +08:00
即梦AI绘图功能前端页面完成
This commit is contained in:
@@ -38,7 +38,7 @@ func (h *AdminJimengHandler) RegisterRoutes() {
|
||||
rg.POST("/jobs/batch-remove", h.BatchRemove)
|
||||
rg.GET("/stats", h.Stats)
|
||||
rg.GET("/config", h.GetConfig)
|
||||
rg.POST("/config", h.UpdateConfig)
|
||||
rg.POST("/config/update", h.UpdateConfig)
|
||||
}
|
||||
|
||||
// Jobs 获取任务列表
|
||||
@@ -241,12 +241,6 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
testErr := h.jimengService.TestConnection(req.AccessKey, req.SecretKey)
|
||||
if testErr != nil {
|
||||
resp.ERROR(c, "连接测试失败: "+testErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 验证算力配置
|
||||
if req.Power.TextToImage <= 0 {
|
||||
resp.ERROR(c, "文生图算力必须大于0")
|
||||
@@ -274,10 +268,11 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
tx := h.DB.Begin()
|
||||
value := utils.JsonEncode(&req)
|
||||
config := model.Config{Name: "jimeng", Value: value}
|
||||
|
||||
err := h.DB.FirstOrCreate(&config, model.Config{Name: "jimeng"}).Error
|
||||
err := tx.FirstOrCreate(&config, model.Config{Name: "jimeng"}).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "保存配置失败: "+err.Error())
|
||||
return
|
||||
@@ -285,7 +280,7 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
|
||||
|
||||
if config.Id > 0 {
|
||||
config.Value = value
|
||||
err = h.DB.Updates(&config).Error
|
||||
err = tx.Updates(&config).Error
|
||||
if err != nil {
|
||||
resp.ERROR(c, "更新配置失败: "+err.Error())
|
||||
return
|
||||
@@ -295,9 +290,11 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
|
||||
// 更新服务中的客户端配置
|
||||
updateErr := h.jimengService.UpdateClientConfig(req.AccessKey, req.SecretKey)
|
||||
if updateErr != nil {
|
||||
// 配置已保存,但客户端更新失败,记录日志但不返回错误
|
||||
logger.Errorf("更新即梦AI客户端配置失败: %v", updateErr)
|
||||
resp.ERROR(c, updateErr.Error())
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
|
||||
}
|
||||
|
||||
@@ -21,504 +21,230 @@ type JimengHandler struct {
|
||||
}
|
||||
|
||||
// NewJimengHandler 创建即梦AI处理器
|
||||
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service) *JimengHandler {
|
||||
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *gorm.DB) *JimengHandler {
|
||||
return &JimengHandler{
|
||||
BaseHandler: BaseHandler{App: app},
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
jimengService: jimengService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由,新增统一任务接口
|
||||
func (h *JimengHandler) RegisterRoutes() {
|
||||
rg := h.App.Engine.Group("/api/jimeng")
|
||||
rg.POST("text-to-image", h.TextToImage)
|
||||
rg.POST("image-to-image-portrait", h.ImageToImagePortrait)
|
||||
rg.POST("image-edit", h.ImageEdit)
|
||||
rg.POST("image-effects", h.ImageEffects)
|
||||
rg.POST("text-to-video", h.TextToVideo)
|
||||
rg.POST("image-to-video", h.ImageToVideo)
|
||||
rg.POST("task", h.CreateTask) // 只保留统一任务接口
|
||||
rg.GET("power-config", h.GetPowerConfig) // 新增算力配置接口
|
||||
rg.GET("jobs", h.Jobs)
|
||||
rg.GET("pending-count", h.PendingCount)
|
||||
rg.GET("remove", h.Remove)
|
||||
rg.GET("retry", h.Retry)
|
||||
}
|
||||
|
||||
// TextToImage 文生图
|
||||
func (h *JimengHandler) TextToImage(c *gin.Context) {
|
||||
var req struct {
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
Seed int64 `json:"seed"`
|
||||
Scale float64 `json:"scale"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
UsePreLLM bool `json:"use_pre_llm"`
|
||||
}
|
||||
// JimengTaskRequest 统一任务请求结构体
|
||||
// 支持所有生图和生成视频类型
|
||||
type JimengTaskRequest struct {
|
||||
TaskType string `json:"task_type" binding:"required"`
|
||||
Prompt string `json:"prompt"`
|
||||
ImageInput string `json:"image_input"`
|
||||
ImageUrls []string `json:"image_urls"`
|
||||
BinaryDataBase64 []string `json:"binary_data_base64"`
|
||||
Scale float64 `json:"scale"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Gpen float64 `json:"gpen"`
|
||||
Skin float64 `json:"skin"`
|
||||
SkinUnifi float64 `json:"skin_unifi"`
|
||||
GenMode string `json:"gen_mode"`
|
||||
Seed int64 `json:"seed"`
|
||||
UsePreLLM bool `json:"use_pre_llm"`
|
||||
TemplateId string `json:"template_id"`
|
||||
AspectRatio string `json:"aspect_ratio"`
|
||||
}
|
||||
|
||||
// CreateTask 统一任务创建接口
|
||||
func (h *JimengHandler) CreateTask(c *gin.Context) {
|
||||
var req JimengTaskRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
// 新增:除图像特效外,其他任务类型必须有提示词
|
||||
if req.TaskType != "image_effects" && req.Prompt == "" {
|
||||
resp.ERROR(c, "提示词不能为空")
|
||||
return
|
||||
}
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置中的算力消耗
|
||||
powerCost := h.getPowerFromConfig(model.JMTaskTypeTextToImage)
|
||||
var powerCost int
|
||||
var taskType model.JMTaskType
|
||||
var params map[string]interface{}
|
||||
var reqKey string
|
||||
var modelName string
|
||||
|
||||
// 检查用户算力
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认参数
|
||||
if req.Scale == 0 {
|
||||
req.Scale = 2.5
|
||||
}
|
||||
if req.Width == 0 {
|
||||
req.Width = 1328
|
||||
}
|
||||
if req.Height == 0 {
|
||||
req.Height = 1328
|
||||
}
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
|
||||
// 构建任务参数
|
||||
params := map[string]interface{}{
|
||||
"seed": req.Seed,
|
||||
"scale": req.Scale,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
"use_pre_llm": req.UsePreLLM,
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
taskReq := &jimeng.CreateTaskRequest{
|
||||
Type: model.JMTaskTypeTextToImage,
|
||||
Prompt: req.Prompt,
|
||||
Params: params,
|
||||
ReqKey: jimeng.ReqKeyTextToImage,
|
||||
Power: powerCost,
|
||||
}
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng text to image task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 扣除用户算力
|
||||
h.subUserPower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "即梦文生图",
|
||||
Remark: fmt.Sprintf("任务ID:%d", job.Id),
|
||||
})
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// ImageToImagePortrait 图生图人像写真
|
||||
func (h *JimengHandler) ImageToImagePortrait(c *gin.Context) {
|
||||
var req struct {
|
||||
ImageInput string `json:"image_input" binding:"required"`
|
||||
Prompt string `json:"prompt"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Gpen float64 `json:"gpen"`
|
||||
Skin float64 `json:"skin"`
|
||||
SkinUnifi float64 `json:"skin_unifi"`
|
||||
GenMode string `json:"gen_mode"`
|
||||
Seed int64 `json:"seed"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置中的算力消耗
|
||||
powerCost := h.getPowerFromConfig(model.JMTaskTypeImageToImage)
|
||||
|
||||
// 检查用户算力
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认参数
|
||||
if req.Width == 0 {
|
||||
req.Width = 1328
|
||||
}
|
||||
if req.Height == 0 {
|
||||
req.Height = 1328
|
||||
}
|
||||
if req.Gpen == 0 {
|
||||
req.Gpen = 0.4
|
||||
}
|
||||
if req.Skin == 0 {
|
||||
req.Skin = 0.3
|
||||
}
|
||||
if req.GenMode == "" {
|
||||
if req.Prompt != "" {
|
||||
req.GenMode = jimeng.GenModeCreative
|
||||
} else {
|
||||
req.GenMode = jimeng.GenModeReference
|
||||
switch req.TaskType {
|
||||
case "text_to_image":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToImage)
|
||||
taskType = model.JMTaskTypeTextToImage
|
||||
reqKey = jimeng.ReqKeyTextToImage
|
||||
modelName = "即梦文生图"
|
||||
if req.Scale == 0 {
|
||||
req.Scale = 2.5
|
||||
}
|
||||
if req.Width == 0 {
|
||||
req.Width = 1328
|
||||
}
|
||||
if req.Height == 0 {
|
||||
req.Height = 1328
|
||||
}
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
params = map[string]interface{}{
|
||||
"seed": req.Seed,
|
||||
"scale": req.Scale,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
"use_pre_llm": req.UsePreLLM,
|
||||
}
|
||||
case "image_to_image":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToImage)
|
||||
taskType = model.JMTaskTypeImageToImage
|
||||
reqKey = jimeng.ReqKeyImageToImagePortrait
|
||||
modelName = "即梦图生图"
|
||||
if req.Width == 0 {
|
||||
req.Width = 1328
|
||||
}
|
||||
if req.Height == 0 {
|
||||
req.Height = 1328
|
||||
}
|
||||
if req.Gpen == 0 {
|
||||
req.Gpen = 0.4
|
||||
}
|
||||
if req.Skin == 0 {
|
||||
req.Skin = 0.3
|
||||
}
|
||||
if req.GenMode == "" {
|
||||
if req.Prompt != "" {
|
||||
req.GenMode = jimeng.GenModeCreative
|
||||
} else {
|
||||
req.GenMode = jimeng.GenModeReference
|
||||
}
|
||||
}
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
}
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
if req.Prompt == "" {
|
||||
req.Prompt = "演唱会现场的合照,闪光灯拍摄"
|
||||
}
|
||||
|
||||
// 构建任务参数
|
||||
params := map[string]interface{}{
|
||||
"image_input": req.ImageInput,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
"gpen": req.Gpen,
|
||||
"skin": req.Skin,
|
||||
"skin_unifi": req.SkinUnifi,
|
||||
"gen_mode": req.GenMode,
|
||||
"seed": req.Seed,
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
taskReq := &jimeng.CreateTaskRequest{
|
||||
Type: model.JMTaskTypeImageToImage,
|
||||
Prompt: req.Prompt,
|
||||
Params: params,
|
||||
ReqKey: jimeng.ReqKeyImageToImagePortrait,
|
||||
Power: powerCost,
|
||||
}
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng image to image portrait task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
params = map[string]interface{}{
|
||||
"image_input": req.ImageInput,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
"gpen": req.Gpen,
|
||||
"skin": req.Skin,
|
||||
"skin_unifi": req.SkinUnifi,
|
||||
"gen_mode": req.GenMode,
|
||||
"seed": req.Seed,
|
||||
}
|
||||
case "image_edit":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEdit)
|
||||
taskType = model.JMTaskTypeImageEdit
|
||||
reqKey = jimeng.ReqKeyImageEdit
|
||||
modelName = "即梦图像编辑"
|
||||
if req.Scale == 0 {
|
||||
req.Scale = 0.5
|
||||
}
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
params = map[string]interface{}{
|
||||
"seed": req.Seed,
|
||||
"scale": req.Scale,
|
||||
}
|
||||
if len(req.ImageUrls) > 0 {
|
||||
params["image_urls"] = req.ImageUrls
|
||||
}
|
||||
if len(req.BinaryDataBase64) > 0 {
|
||||
params["binary_data_base64"] = req.BinaryDataBase64
|
||||
}
|
||||
case "image_effects":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEffects)
|
||||
taskType = model.JMTaskTypeImageEffects
|
||||
reqKey = jimeng.ReqKeyImageEffects
|
||||
modelName = "即梦图像特效"
|
||||
if req.Width == 0 {
|
||||
req.Width = 1328
|
||||
}
|
||||
if req.Height == 0 {
|
||||
req.Height = 1328
|
||||
}
|
||||
params = map[string]interface{}{
|
||||
"image_input1": req.ImageInput,
|
||||
"template_id": req.TemplateId,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
}
|
||||
case "text_to_video":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToVideo)
|
||||
taskType = model.JMTaskTypeTextToVideo
|
||||
reqKey = jimeng.ReqKeyTextToVideo
|
||||
modelName = "即梦文生视频"
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
if req.AspectRatio == "" {
|
||||
req.AspectRatio = jimeng.AspectRatio16_9
|
||||
}
|
||||
params = map[string]interface{}{
|
||||
"seed": req.Seed,
|
||||
"aspect_ratio": req.AspectRatio,
|
||||
}
|
||||
case "image_to_video":
|
||||
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToVideo)
|
||||
taskType = model.JMTaskTypeImageToVideo
|
||||
reqKey = jimeng.ReqKeyImageToVideo
|
||||
modelName = "即梦图生视频"
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
params = map[string]interface{}{
|
||||
"seed": req.Seed,
|
||||
"aspect_ratio": req.AspectRatio,
|
||||
}
|
||||
if len(req.ImageUrls) > 0 {
|
||||
params["image_urls"] = req.ImageUrls
|
||||
}
|
||||
if len(req.BinaryDataBase64) > 0 {
|
||||
params["binary_data_base64"] = req.BinaryDataBase64
|
||||
}
|
||||
default:
|
||||
resp.ERROR(c, "不支持的任务类型")
|
||||
return
|
||||
}
|
||||
|
||||
// 扣除用户算力
|
||||
h.subUserPower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "即梦图生图",
|
||||
Remark: fmt.Sprintf("任务ID:%d", job.Id),
|
||||
})
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// ImageEdit 图像编辑
|
||||
func (h *JimengHandler) ImageEdit(c *gin.Context) {
|
||||
var req struct {
|
||||
ImageUrls []string `json:"image_urls"`
|
||||
BinaryDataBase64 []string `json:"binary_data_base64"`
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
Seed int64 `json:"seed"`
|
||||
Scale float64 `json:"scale"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.ImageUrls) == 0 && len(req.BinaryDataBase64) == 0 {
|
||||
resp.ERROR(c, "请提供图片URL或Base64数据")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置中的算力消耗
|
||||
powerCost := h.getPowerFromConfig(model.JMTaskTypeImageEdit)
|
||||
|
||||
// 检查用户算力
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认参数
|
||||
if req.Scale == 0 {
|
||||
req.Scale = 0.5
|
||||
}
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
|
||||
// 构建任务参数
|
||||
params := map[string]interface{}{
|
||||
"seed": req.Seed,
|
||||
"scale": req.Scale,
|
||||
}
|
||||
if len(req.ImageUrls) > 0 {
|
||||
params["image_urls"] = req.ImageUrls
|
||||
}
|
||||
if len(req.BinaryDataBase64) > 0 {
|
||||
params["binary_data_base64"] = req.BinaryDataBase64
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
taskReq := &jimeng.CreateTaskRequest{
|
||||
Type: model.JMTaskTypeImageEdit,
|
||||
Type: taskType,
|
||||
Prompt: req.Prompt,
|
||||
Params: params,
|
||||
ReqKey: jimeng.ReqKeyImageEdit,
|
||||
ReqKey: reqKey,
|
||||
Power: powerCost,
|
||||
}
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng image edit task failed: %v", err)
|
||||
logger.Errorf("create jimeng task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 扣除用户算力
|
||||
h.subUserPower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "即梦图像编辑",
|
||||
Remark: fmt.Sprintf("任务ID:%d", job.Id),
|
||||
})
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// ImageEffects 图像特效
|
||||
func (h *JimengHandler) ImageEffects(c *gin.Context) {
|
||||
var req struct {
|
||||
ImageInput1 string `json:"image_input1" binding:"required"`
|
||||
TemplateId string `json:"template_id" binding:"required"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置中的算力消耗
|
||||
powerCost := h.getPowerFromConfig(model.JMTaskTypeImageEffects)
|
||||
|
||||
// 检查用户算力
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认参数
|
||||
if req.Width == 0 {
|
||||
req.Width = 1328
|
||||
}
|
||||
if req.Height == 0 {
|
||||
req.Height = 1328
|
||||
}
|
||||
|
||||
// 构建任务参数
|
||||
params := map[string]interface{}{
|
||||
"image_input1": req.ImageInput1,
|
||||
"template_id": req.TemplateId,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
taskReq := &jimeng.CreateTaskRequest{
|
||||
Type: model.JMTaskTypeImageEffects,
|
||||
Prompt: "",
|
||||
Params: params,
|
||||
ReqKey: jimeng.ReqKeyImageEffects,
|
||||
Power: powerCost,
|
||||
}
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng image effects task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 扣除用户算力
|
||||
h.subUserPower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "即梦图像特效",
|
||||
Remark: fmt.Sprintf("任务ID:%d", job.Id),
|
||||
})
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// TextToVideo 文生视频
|
||||
func (h *JimengHandler) TextToVideo(c *gin.Context) {
|
||||
var req struct {
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
Seed int64 `json:"seed"`
|
||||
AspectRatio string `json:"aspect_ratio"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置中的算力消耗
|
||||
powerCost := h.getPowerFromConfig(model.JMTaskTypeTextToVideo)
|
||||
|
||||
// 检查用户算力
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认参数
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
if req.AspectRatio == "" {
|
||||
req.AspectRatio = jimeng.AspectRatio16_9
|
||||
}
|
||||
|
||||
// 构建任务参数
|
||||
params := map[string]interface{}{
|
||||
"seed": req.Seed,
|
||||
"aspect_ratio": req.AspectRatio,
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
taskReq := &jimeng.CreateTaskRequest{
|
||||
Type: model.JMTaskTypeTextToVideo,
|
||||
Prompt: req.Prompt,
|
||||
Params: params,
|
||||
ReqKey: jimeng.ReqKeyTextToVideo,
|
||||
Power: powerCost,
|
||||
}
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng text to video task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 扣除用户算力
|
||||
h.subUserPower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "即梦文生视频",
|
||||
Remark: fmt.Sprintf("任务ID:%d", job.Id),
|
||||
})
|
||||
|
||||
resp.SUCCESS(c, job)
|
||||
}
|
||||
|
||||
// ImageToVideo 图生视频
|
||||
func (h *JimengHandler) ImageToVideo(c *gin.Context) {
|
||||
var req struct {
|
||||
ImageUrls []string `json:"image_urls"`
|
||||
BinaryDataBase64 []string `json:"binary_data_base64"`
|
||||
Prompt string `json:"prompt"`
|
||||
Seed int64 `json:"seed"`
|
||||
AspectRatio string `json:"aspect_ratio" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
resp.ERROR(c, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.ImageUrls) == 0 && len(req.BinaryDataBase64) == 0 {
|
||||
resp.ERROR(c, "请提供图片URL或Base64数据")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置中的算力消耗
|
||||
powerCost := h.getPowerFromConfig(model.JMTaskTypeImageToVideo)
|
||||
|
||||
// 检查用户算力
|
||||
if user.Power < powerCost {
|
||||
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认参数
|
||||
if req.Seed == 0 {
|
||||
req.Seed = -1
|
||||
}
|
||||
|
||||
// 构建任务参数
|
||||
params := map[string]interface{}{
|
||||
"seed": req.Seed,
|
||||
"aspect_ratio": req.AspectRatio,
|
||||
}
|
||||
if len(req.ImageUrls) > 0 {
|
||||
params["image_urls"] = req.ImageUrls
|
||||
}
|
||||
if len(req.BinaryDataBase64) > 0 {
|
||||
params["binary_data_base64"] = req.BinaryDataBase64
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
taskReq := &jimeng.CreateTaskRequest{
|
||||
Type: model.JMTaskTypeImageToVideo,
|
||||
Prompt: req.Prompt,
|
||||
Params: params,
|
||||
ReqKey: jimeng.ReqKeyImageToVideo,
|
||||
Power: powerCost,
|
||||
}
|
||||
|
||||
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||
if err != nil {
|
||||
logger.Errorf("create jimeng image to video task failed: %v", err)
|
||||
resp.ERROR(c, "创建任务失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 扣除用户算力
|
||||
h.subUserPower(user.Id, powerCost, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: "即梦图生视频",
|
||||
Model: modelName,
|
||||
Remark: fmt.Sprintf("任务ID:%d", job.Id),
|
||||
})
|
||||
|
||||
@@ -551,24 +277,6 @@ func (h *JimengHandler) Jobs(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// PendingCount 获取未完成任务数量
|
||||
func (h *JimengHandler) PendingCount(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
if err != nil {
|
||||
resp.NotAuth(c)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := h.jimengService.GetPendingTaskCount(user.Id)
|
||||
if err != nil {
|
||||
logger.Errorf("get pending task count failed: %v", err)
|
||||
resp.ERROR(c, "获取待处理任务数量失败")
|
||||
return
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, gin.H{"count": count})
|
||||
}
|
||||
|
||||
// Remove 删除任务
|
||||
func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
user, err := h.GetLoginUser(c)
|
||||
@@ -583,6 +291,21 @@ func (h *JimengHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取任务,判断状态
|
||||
job, err := h.jimengService.GetJob(uint(jobId))
|
||||
if err != nil {
|
||||
resp.ERROR(c, "任务不存在")
|
||||
return
|
||||
}
|
||||
if job.UserId != user.Id {
|
||||
resp.ERROR(c, "无权限操作")
|
||||
return
|
||||
}
|
||||
if job.Status != model.JMTaskStatusFailed {
|
||||
resp.ERROR(c, "只有失败的任务才能删除")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.jimengService.DeleteJob(uint(jobId), user.Id); err != nil {
|
||||
logger.Errorf("delete jimeng job failed: %v", err)
|
||||
resp.ERROR(c, "删除任务失败")
|
||||
@@ -709,3 +432,20 @@ func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
|
||||
return 10
|
||||
}
|
||||
}
|
||||
|
||||
// GetPowerConfig 获取即梦各任务类型算力消耗配置
|
||||
func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
|
||||
config, err := h.jimengService.GetConfig()
|
||||
if err != nil || config == nil {
|
||||
resp.ERROR(c, "获取算力配置失败")
|
||||
return
|
||||
}
|
||||
resp.SUCCESS(c, gin.H{
|
||||
"text_to_image": config.Power.TextToImage,
|
||||
"image_to_image": config.Power.ImageToImage,
|
||||
"image_edit": config.Power.ImageEdit,
|
||||
"image_effects": config.Power.ImageEffects,
|
||||
"text_to_video": config.Power.TextToVideo,
|
||||
"image_to_video": config.Power.ImageToVideo,
|
||||
})
|
||||
}
|
||||
|
||||
13
api/main.go
13
api/main.go
@@ -208,21 +208,10 @@ func main() {
|
||||
}),
|
||||
|
||||
// 即梦AI 服务
|
||||
fx.Provide(func(config *types.AppConfig) *jimeng.Client {
|
||||
// 使用默认配置初始化客户端,后续会从数据库加载
|
||||
return jimeng.NewClient("", "")
|
||||
}),
|
||||
fx.Provide(jimeng.NewService),
|
||||
fx.Invoke(func(service *jimeng.Service) {
|
||||
// 从数据库加载配置
|
||||
err := service.LoadConfigFromDB()
|
||||
if err != nil {
|
||||
logger.Errorf("加载即梦AI配置失败: %v", err)
|
||||
}
|
||||
}),
|
||||
fx.Provide(jimeng.NewConsumer),
|
||||
fx.Invoke(func(consumer *jimeng.Consumer) {
|
||||
consumer.Start()
|
||||
//consumer.Start()
|
||||
go consumer.MonitorQueue()
|
||||
}),
|
||||
fx.Provide(service.NewUserService),
|
||||
|
||||
@@ -8,11 +8,8 @@ import (
|
||||
|
||||
"github.com/volcengine/volc-sdk-golang/base"
|
||||
"github.com/volcengine/volc-sdk-golang/service/visual"
|
||||
"geekai/logger"
|
||||
)
|
||||
|
||||
var clientLogger = logger.GetLogger()
|
||||
|
||||
// Client 即梦API客户端
|
||||
type Client struct {
|
||||
visual *visual.Visual
|
||||
@@ -80,7 +77,7 @@ func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error)
|
||||
return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
clientLogger.Infof("Jimeng SubmitTask Response: %s", string(respBody))
|
||||
looger.Infof("Jimeng SubmitTask Response: %s", string(respBody))
|
||||
|
||||
// 解析响应
|
||||
var result SubmitTaskResponse
|
||||
@@ -105,7 +102,7 @@ func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
|
||||
return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
clientLogger.Infof("Jimeng QueryTask Response: %s", string(respBody))
|
||||
looger.Infof("Jimeng QueryTask Response: %s", string(respBody))
|
||||
|
||||
// 解析响应
|
||||
var result QueryTaskResponse
|
||||
@@ -130,7 +127,7 @@ func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*QueryTaskResponse, err
|
||||
return nil, fmt.Errorf("submit sync task failed (status: %d): %w", statusCode, err)
|
||||
}
|
||||
|
||||
clientLogger.Infof("Jimeng SubmitSyncTask Response: %s", string(respBody))
|
||||
looger.Infof("Jimeng SubmitSyncTask Response: %s", string(respBody))
|
||||
|
||||
// 解析响应,同步任务直接返回结果
|
||||
var result QueryTaskResponse
|
||||
@@ -139,4 +136,4 @@ func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*QueryTaskResponse, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func (c *Consumer) consume() {
|
||||
// processTask 处理任务
|
||||
func (c *Consumer) processTask() {
|
||||
// 从队列中获取任务
|
||||
var task map[string]interface{}
|
||||
var task map[string]any
|
||||
if err := c.service.taskQueue.LPop(&task); err != nil {
|
||||
// 队列为空,等待1秒后重试
|
||||
time.Sleep(time.Second)
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"geekai/logger"
|
||||
logger2 "geekai/logger"
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
var serviceLogger = logger.GetLogger()
|
||||
var looger = logger2.GetLogger()
|
||||
|
||||
// Service 即梦服务
|
||||
type Service struct {
|
||||
@@ -29,8 +30,16 @@ type Service struct {
|
||||
}
|
||||
|
||||
// NewService 创建即梦服务
|
||||
func NewService(db *gorm.DB, redisCli *redis.Client, client *Client) *Service {
|
||||
func NewService(db *gorm.DB, redisCli *redis.Client) *Service {
|
||||
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
|
||||
// 从数据库加载配置
|
||||
var config model.Config
|
||||
db.Where("name = ?", "Jimeng").First(&config)
|
||||
var jimengConfig types.JimengConfig
|
||||
if config.Id > 0 {
|
||||
_ = utils.JsonDecode(config.Value, &jimengConfig)
|
||||
}
|
||||
client := NewClient(jimengConfig.AccessKey, jimengConfig.SecretKey)
|
||||
return &Service{
|
||||
db: db,
|
||||
redis: redisCli,
|
||||
@@ -99,7 +108,7 @@ func (s *Service) ProcessTask(jobId uint) error {
|
||||
case model.JMTaskTypeTextToImage:
|
||||
return s.processTextToImage(&job)
|
||||
case model.JMTaskTypeImageToImage:
|
||||
return s.processImageToImagePortrait(&job)
|
||||
return s.processImageToImage(&job)
|
||||
case model.JMTaskTypeImageEdit:
|
||||
return s.processImageEdit(&job)
|
||||
case model.JMTaskTypeImageEffects:
|
||||
@@ -171,15 +180,15 @@ func (s *Service) processTextToImage(job *model.JimengJob) error {
|
||||
"raw_data": string(rawData),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
looger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
return s.pollTaskStatus(job.Id, resp.Data.TaskId, job.ReqKey)
|
||||
}
|
||||
|
||||
// processImageToImagePortrait 处理图生图人像写真任务
|
||||
func (s *Service) processImageToImagePortrait(job *model.JimengJob) error {
|
||||
// processImageToImage 处理图生图任务
|
||||
func (s *Service) processImageToImage(job *model.JimengJob) error {
|
||||
// 解析任务参数
|
||||
var params map[string]any
|
||||
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
||||
@@ -249,7 +258,7 @@ func (s *Service) processImageToImagePortrait(job *model.JimengJob) error {
|
||||
"raw_data": string(rawData),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
looger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
@@ -315,7 +324,7 @@ func (s *Service) processImageEdit(job *model.JimengJob) error {
|
||||
"raw_data": string(rawData),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
looger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
@@ -370,7 +379,7 @@ func (s *Service) processImageEffects(job *model.JimengJob) error {
|
||||
"raw_data": string(rawData),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
looger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
@@ -418,7 +427,7 @@ func (s *Service) processTextToVideo(job *model.JimengJob) error {
|
||||
"raw_data": string(rawData),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
looger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
@@ -482,7 +491,7 @@ func (s *Service) processImageToVideo(job *model.JimengJob) error {
|
||||
"raw_data": string(rawData),
|
||||
"updated_at": time.Now(),
|
||||
}).Error; err != nil {
|
||||
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
looger.Errorf("update jimeng job task_id failed: %v", err)
|
||||
}
|
||||
|
||||
// 开始轮询任务状态
|
||||
@@ -505,7 +514,7 @@ func (s *Service) pollTaskStatus(jobId uint, taskId, reqKey string) error {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
serviceLogger.Errorf("query jimeng task status failed: %v", err)
|
||||
looger.Errorf("query jimeng task status failed: %v", err)
|
||||
retryCount++
|
||||
continue
|
||||
}
|
||||
@@ -555,7 +564,7 @@ func (s *Service) pollTaskStatus(jobId uint, taskId, reqKey string) error {
|
||||
return s.handleTaskError(jobId, fmt.Sprintf("task not found or expired: %s", resp.Data.Status))
|
||||
|
||||
default:
|
||||
serviceLogger.Warnf("unknown task status: %s", resp.Data.Status)
|
||||
looger.Warnf("unknown task status: %s", resp.Data.Status)
|
||||
}
|
||||
|
||||
retryCount++
|
||||
@@ -587,7 +596,7 @@ func (s *Service) UpdateJobProgress(jobId uint, progress int) error {
|
||||
|
||||
// handleTaskError 处理任务错误
|
||||
func (s *Service) handleTaskError(jobId uint, errMsg string) error {
|
||||
serviceLogger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
||||
looger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
||||
return s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, errMsg)
|
||||
}
|
||||
|
||||
@@ -635,13 +644,12 @@ func (s *Service) DeleteJob(jobId uint, userId uint) error {
|
||||
}
|
||||
|
||||
// PushTaskToQueue 推送任务到队列
|
||||
func (s *Service) PushTaskToQueue(task map[string]interface{}) error {
|
||||
func (s *Service) PushTaskToQueue(task map[string]any) error {
|
||||
return s.taskQueue.RPush(task)
|
||||
}
|
||||
|
||||
// TestConnection 测试即梦AI连接
|
||||
func (s *Service) TestConnection(accessKey, secretKey string) error {
|
||||
// 创建临时客户端进行测试
|
||||
// testConnection 测试即梦AI连接
|
||||
func (s *Service) testConnection(accessKey, secretKey string) error {
|
||||
testClient := NewClient(accessKey, secretKey)
|
||||
|
||||
// 使用一个简单的查询任务来测试连接
|
||||
@@ -655,13 +663,12 @@ func (s *Service) TestConnection(accessKey, secretKey string) error {
|
||||
// 即使任务不存在,只要不是认证错误就说明连接正常
|
||||
if err != nil {
|
||||
// 检查是否是认证错误
|
||||
if err.Error() == "unauthorized" || err.Error() == "access denied" {
|
||||
if strings.Contains(err.Error(), "InvalidAccessKey") {
|
||||
return fmt.Errorf("认证失败,请检查AccessKey和SecretKey是否正确")
|
||||
}
|
||||
// 其他错误(如任务不存在)说明连接正常
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -671,9 +678,9 @@ func (s *Service) UpdateClientConfig(accessKey, secretKey string) error {
|
||||
newClient := NewClient(accessKey, secretKey)
|
||||
|
||||
// 测试新客户端是否可用
|
||||
err := s.TestConnection(accessKey, secretKey)
|
||||
err := s.testConnection(accessKey, secretKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("新配置测试失败: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新客户端
|
||||
@@ -709,18 +716,3 @@ func (s *Service) GetConfig() (*types.JimengConfig, error) {
|
||||
|
||||
return &jimengConfig, nil
|
||||
}
|
||||
|
||||
// LoadConfigFromDB 从数据库加载配置并更新客户端
|
||||
func (s *Service) LoadConfigFromDB() error {
|
||||
config, err := s.GetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果配置中有AccessKey和SecretKey,则更新客户端
|
||||
if config.AccessKey != "" && config.SecretKey != "" {
|
||||
return s.UpdateClientConfig(config.AccessKey, config.SecretKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ html, body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
--primary-color: #21aa93
|
||||
// --primary-color: #21aa93
|
||||
|
||||
h1 { font-size: 2em; } /* 通常是 2em */
|
||||
h2 { font-size: 1.5em; } /* 通常是 1.5em */
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
color: #333;
|
||||
background: var(--card-bg);
|
||||
box-shadow: var(--card-shadow, 0 8px 24px rgba(0,0,0,0.12));
|
||||
color: var(--text-theme-color);
|
||||
font-size: 14px;
|
||||
overflow: auto;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
color: var(--text-theme-color);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--text-theme-color);
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
color: #5865f2;
|
||||
color: var(--primary-color, #5865f2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,24 +52,36 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 15px 10px;
|
||||
border: 2px solid #f0f0f0;
|
||||
border: 2px solid var(--border-color, #f0f0f0);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: #fafafa;
|
||||
background: var(--card-bg-secondary, #fafafa);
|
||||
/* 暗色主题支持 */
|
||||
[data-theme="dark"] & {
|
||||
background: var(--card-bg-secondary-dark, #23242a);
|
||||
border-color: var(--border-color-dark, #33343a);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #5865f2;
|
||||
background: #f8f9ff;
|
||||
border-color: var(--primary-color, #5865f2);
|
||||
background: var(--card-bg-hover, #f8f9ff);
|
||||
[data-theme="dark"] & {
|
||||
background: var(--card-bg-hover-dark, #2a2b31);
|
||||
}
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #5865f2;
|
||||
background: linear-gradient(135deg, #5865f2 0%, #7289da 100%);
|
||||
color: white;
|
||||
border-color: var(--primary-color, #5865f2);
|
||||
background: var(--primary-gradient, linear-gradient(135deg, #5865f2 0%, #7289da 100%));
|
||||
color: var(--primary-text-on-primary, #fff);
|
||||
[data-theme="dark"] & {
|
||||
background: var(--primary-gradient-dark, linear-gradient(135deg, #23242a 0%, #2a2b31 100%));
|
||||
color: var(--primary-text-on-primary-dark, #fff);
|
||||
}
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(88, 101, 242, 0.3);
|
||||
box-shadow: var(--primary-shadow, 0 4px 12px rgba(88,101,242,0.3));
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
@@ -95,11 +107,11 @@
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--text-theme-color);
|
||||
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
color: #5865f2;
|
||||
color: var(--primary-color, #5865f2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +119,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 5px 15px;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 10px;
|
||||
background: #f9f9f9;
|
||||
background: var(--card-bg-secondary, #f9f9f9);
|
||||
[data-theme="dark"] & {
|
||||
background: var(--card-bg-secondary-dark, #23242a);
|
||||
border-color: var(--border-color-dark, #33343a);
|
||||
}
|
||||
|
||||
.switch-info {
|
||||
flex: 1;
|
||||
@@ -118,13 +134,13 @@
|
||||
.switch-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--text-theme-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.switch-desc {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-sub-color, #666);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +161,7 @@
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--text-theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +173,7 @@
|
||||
.label {
|
||||
margin-right: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--text-theme-color);
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
@@ -165,9 +181,9 @@
|
||||
.text-info {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: #f0f8ff;
|
||||
background: var(--info-bg, #f0f8ff);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #5865f2;
|
||||
border-left: 4px solid var(--primary-color, #5865f2);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
@@ -206,80 +222,87 @@
|
||||
}
|
||||
|
||||
.task-list {
|
||||
.list-box {
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.task-left {
|
||||
margin-right: 20px;
|
||||
|
||||
.task-preview {
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
.task-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.task-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--card-shadow, 0 2px 8px rgba(0,0,0,0.1));
|
||||
overflow: hidden;
|
||||
min-height: 420px;
|
||||
height: 100%;
|
||||
transition: box-shadow 0.2s;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 24px rgba(88,101,242,0.12);
|
||||
}
|
||||
.task-left {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
.task-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 1.2/1;
|
||||
min-height: 220px;
|
||||
max-height: 320px;
|
||||
background: var(--card-bg-secondary, #f0f0f0);
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.preview-image, .preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.preview-image, .preview-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
color: var(--text-disabled-color, #999);
|
||||
font-size: 16px;
|
||||
.el-icon, .iconfont {
|
||||
font-size: 32px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-center {
|
||||
flex: 1;
|
||||
|
||||
.task-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.task-prompt {
|
||||
font-size: 14px;
|
||||
color: var(--text-theme-color);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.task-center {
|
||||
flex: none;
|
||||
padding: 18px 18px 8px 18px;
|
||||
.task-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.task-right {
|
||||
.task-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.task-prompt {
|
||||
font-size: 14px;
|
||||
color: var(--text-theme-color);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
.task-meta {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 12px;
|
||||
color: var(--text-disabled-color, #999);
|
||||
}
|
||||
}
|
||||
.task-right {
|
||||
flex: none;
|
||||
padding: 0 18px 16px 18px;
|
||||
.task-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,4 +331,21 @@
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.task-list .task-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.task-list .task-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.task-list .task-item {
|
||||
min-height: 320px;
|
||||
.task-left .task-preview {
|
||||
min-height: 160px;
|
||||
max-height: 220px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,40 +7,42 @@
|
||||
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg, substr, dateFormat } from '@/utils/libs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { replaceImg, substr } from '@/utils/libs'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { computed, nextTick, reactive, ref } from 'vue'
|
||||
|
||||
export const useJimengStore = defineStore('jimeng', () => {
|
||||
// 当前激活的功能分类和具体功能
|
||||
const activeCategory = ref('image_generation')
|
||||
const activeFunction = ref('text_to_image')
|
||||
const useImageInput = ref(false)
|
||||
|
||||
|
||||
// 新增:全局提示词
|
||||
const currentPrompt = ref('')
|
||||
|
||||
// 共同状态
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(false)
|
||||
const pullHandler = ref(null)
|
||||
const taskFilter = ref('all')
|
||||
const currentList = ref([])
|
||||
|
||||
const isOver = ref(false)
|
||||
|
||||
// 用户信息
|
||||
const isLogin = ref(false)
|
||||
const userPower = ref(100)
|
||||
|
||||
|
||||
// 视频预览
|
||||
const showDialog = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
|
||||
|
||||
// 功能分类配置
|
||||
const categories = [
|
||||
{ key: 'image_generation', name: '图片生成' },
|
||||
@@ -48,29 +50,83 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
{ key: 'image_effects', name: '图像特效' },
|
||||
{ key: 'video_generation', name: '视频生成' },
|
||||
]
|
||||
|
||||
|
||||
// 新增:动态获取算力消耗配置
|
||||
const powerConfig = reactive({})
|
||||
|
||||
// 功能配置
|
||||
const functions = [
|
||||
{ key: 'text_to_image', name: '文生图', category: 'image_generation', needsPrompt: true, needsImage: false, power: 20 },
|
||||
{ key: 'image_to_image_portrait', name: '图生图', category: 'image_generation', needsPrompt: true, needsImage: true, power: 30 },
|
||||
{ key: 'image_edit', name: '图像编辑', category: 'image_editing', needsPrompt: true, needsImage: true, multiple: true, power: 25 },
|
||||
{ key: 'image_effects', name: '图像特效', category: 'image_effects', needsPrompt: false, needsImage: true, power: 15 },
|
||||
{ key: 'text_to_video', name: '文生视频', category: 'video_generation', needsPrompt: true, needsImage: false, power: 100 },
|
||||
{ key: 'image_to_video', name: '图生视频', category: 'video_generation', needsPrompt: true, needsImage: true, multiple: true, power: 120 },
|
||||
]
|
||||
|
||||
const functions = reactive([
|
||||
{
|
||||
key: 'text_to_image',
|
||||
name: '文生图',
|
||||
category: 'image_generation',
|
||||
needsPrompt: true,
|
||||
needsImage: false,
|
||||
power: 20,
|
||||
},
|
||||
{
|
||||
key: 'image_to_image',
|
||||
name: '图生图',
|
||||
category: 'image_generation',
|
||||
needsPrompt: true,
|
||||
needsImage: true,
|
||||
power: 30,
|
||||
},
|
||||
{
|
||||
key: 'image_edit',
|
||||
name: '图像编辑',
|
||||
category: 'image_editing',
|
||||
needsPrompt: true,
|
||||
needsImage: true,
|
||||
multiple: true,
|
||||
power: 25,
|
||||
},
|
||||
{
|
||||
key: 'image_effects',
|
||||
name: '图像特效',
|
||||
category: 'image_effects',
|
||||
needsPrompt: false,
|
||||
needsImage: true,
|
||||
power: 15,
|
||||
},
|
||||
{
|
||||
key: 'text_to_video',
|
||||
name: '文生视频',
|
||||
category: 'video_generation',
|
||||
needsPrompt: true,
|
||||
needsImage: false,
|
||||
power: 100,
|
||||
},
|
||||
{
|
||||
key: 'image_to_video',
|
||||
name: '图生视频',
|
||||
category: 'video_generation',
|
||||
needsPrompt: true,
|
||||
needsImage: true,
|
||||
multiple: true,
|
||||
power: 120,
|
||||
},
|
||||
])
|
||||
|
||||
// 动态设置算力消耗
|
||||
const setFunctionPowers = (config) => {
|
||||
functions.forEach((f) => {
|
||||
if (config[f.key] !== undefined) {
|
||||
f.power = config[f.key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 各功能的参数
|
||||
const textToImageParams = reactive({
|
||||
prompt: '',
|
||||
size: '1328x1328',
|
||||
scale: 2.5,
|
||||
seed: -1,
|
||||
use_pre_llm: false,
|
||||
use_pre_llm: true,
|
||||
})
|
||||
|
||||
|
||||
const imageToImageParams = reactive({
|
||||
image_input: '',
|
||||
prompt: '演唱会现场的合照,闪光灯拍摄',
|
||||
size: '1328x1328',
|
||||
gpen: 0.4,
|
||||
skin: 0.3,
|
||||
@@ -78,74 +134,70 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
gen_mode: 'creative',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
|
||||
const imageEditParams = reactive({
|
||||
image_urls: [],
|
||||
prompt: '',
|
||||
scale: 0.5,
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
|
||||
const imageEffectsParams = reactive({
|
||||
image_input1: '',
|
||||
template_id: '',
|
||||
size: '1328x1328',
|
||||
})
|
||||
|
||||
|
||||
const textToVideoParams = reactive({
|
||||
prompt: '',
|
||||
aspect_ratio: '16:9',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
|
||||
const imageToVideoParams = reactive({
|
||||
image_urls: [],
|
||||
prompt: '',
|
||||
aspect_ratio: '16:9',
|
||||
seed: -1,
|
||||
})
|
||||
|
||||
|
||||
// 计算属性
|
||||
const currentFunction = computed(() => {
|
||||
return functions.find(f => f.key === activeFunction.value) || functions[0]
|
||||
return functions.find((f) => f.key === activeFunction.value) || functions[0]
|
||||
})
|
||||
|
||||
|
||||
const currentFunctions = computed(() => {
|
||||
return functions.filter(f => f.category === activeCategory.value)
|
||||
return functions.filter((f) => f.category === activeCategory.value)
|
||||
})
|
||||
|
||||
|
||||
const needsPrompt = computed(() => currentFunction.value.needsPrompt)
|
||||
const needsImage = computed(() => currentFunction.value.needsImage)
|
||||
const needsMultipleImages = computed(() => currentFunction.value.multiple)
|
||||
const currentPowerCost = computed(() => currentFunction.value.power)
|
||||
|
||||
|
||||
// 初始化方法
|
||||
const init = async () => {
|
||||
try {
|
||||
// 获取算力消耗配置
|
||||
const powerRes = await httpGet('/api/jimeng/power-config')
|
||||
if (powerRes.data) {
|
||||
Object.assign(powerConfig, powerRes.data)
|
||||
setFunctionPowers(powerRes.data)
|
||||
}
|
||||
const user = await checkSession()
|
||||
isLogin.value = true
|
||||
userPower.value = user.power
|
||||
|
||||
// 获取任务列表
|
||||
await fetchData(1)
|
||||
|
||||
// 检查是否需要开始轮询
|
||||
const pendingCount = await getPendingCount()
|
||||
if (pendingCount > 0) {
|
||||
startPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 切换功能分类
|
||||
const switchCategory = (category) => {
|
||||
activeCategory.value = category
|
||||
const categoryFunctions = functions.filter(f => f.category === category)
|
||||
const categoryFunctions = functions.filter((f) => f.category === category)
|
||||
if (categoryFunctions.length > 0) {
|
||||
if (category === 'image_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_image_portrait' : 'text_to_image'
|
||||
activeFunction.value = useImageInput.value ? 'image_to_image' : 'text_to_image'
|
||||
} else if (category === 'video_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video'
|
||||
} else {
|
||||
@@ -153,91 +205,106 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 切换输入模式
|
||||
const switchInputMode = () => {
|
||||
if (activeCategory.value === 'image_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_image_portrait' : 'text_to_image'
|
||||
activeFunction.value = useImageInput.value ? 'image_to_image' : 'text_to_image'
|
||||
} else if (activeCategory.value === 'video_generation') {
|
||||
activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 切换功能
|
||||
const switchFunction = (functionKey) => {
|
||||
activeFunction.value = functionKey
|
||||
}
|
||||
|
||||
|
||||
// 获取当前算力消耗
|
||||
const getCurrentPowerCost = () => {
|
||||
return currentFunction.value.power
|
||||
}
|
||||
|
||||
|
||||
// 获取功能名称
|
||||
const getFunctionName = (type) => {
|
||||
const func = functions.find(f => f.key === type)
|
||||
const func = functions.find((f) => f.key === type)
|
||||
return func ? func.name : type
|
||||
}
|
||||
|
||||
|
||||
// 获取任务状态文本
|
||||
const getTaskStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'pending': '等待中',
|
||||
'processing': '处理中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败'
|
||||
in_queue: '排队中',
|
||||
generating: '处理中',
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
canceled: '已取消',
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
'pending': 'info',
|
||||
'processing': 'warning',
|
||||
'completed': 'success',
|
||||
'failed': 'danger'
|
||||
pending: 'info',
|
||||
processing: 'warning',
|
||||
completed: 'success',
|
||||
failed: 'danger',
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
|
||||
// 切换任务筛选
|
||||
const switchTaskFilter = (filter) => {
|
||||
taskFilter.value = filter
|
||||
updateCurrentList()
|
||||
}
|
||||
|
||||
|
||||
// 更新当前列表
|
||||
const updateCurrentList = () => {
|
||||
if (taskFilter.value === 'all') {
|
||||
currentList.value = list.value
|
||||
} else if (taskFilter.value === 'image') {
|
||||
currentList.value = list.value.filter(item =>
|
||||
['text_to_image', 'image_to_image_portrait', 'image_edit', 'image_effects'].includes(item.type)
|
||||
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 =>
|
||||
currentList.value = list.value.filter((item) =>
|
||||
['text_to_video', 'image_to_video'].includes(item.type)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 轮询定时器
|
||||
let pollHandler = null
|
||||
|
||||
// 获取任务列表
|
||||
const fetchData = async (pageNum = 1) => {
|
||||
try {
|
||||
loading.value = true
|
||||
page.value = pageNum
|
||||
|
||||
|
||||
const response = await httpGet('/api/jimeng/jobs', {
|
||||
page: pageNum,
|
||||
page_size: pageSize.value
|
||||
page_size: pageSize.value,
|
||||
})
|
||||
|
||||
|
||||
if (response.data) {
|
||||
list.value = response.data.jobs || []
|
||||
total.value = response.data.total || 0
|
||||
noData.value = list.value.length === 0
|
||||
updateCurrentList()
|
||||
// 判断是否有未完成任务
|
||||
const hasPending = list.value.some(
|
||||
(item) => item.status === 'in_queue' || item.status === 'processing'
|
||||
)
|
||||
if (hasPending) {
|
||||
startPolling()
|
||||
} else {
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务列表失败:', error)
|
||||
@@ -246,42 +313,53 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 简单轮询逻辑
|
||||
const startPolling = () => {
|
||||
if (pollHandler) return
|
||||
pollHandler = setInterval(() => {
|
||||
fetchData(page.value)
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollHandler) {
|
||||
clearInterval(pollHandler)
|
||||
pollHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
// 提交任务
|
||||
const submitTask = async () => {
|
||||
if (!isLogin.value) {
|
||||
showMessageError('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
if (userPower.value < currentPowerCost.value) {
|
||||
showMessageError('算力不足')
|
||||
return
|
||||
}
|
||||
|
||||
// 新增:除图像特效外,其他任务类型必须有提示词
|
||||
if (activeFunction.value !== 'image_effects' && !currentPrompt.value) {
|
||||
showMessageError('提示词不能为空')
|
||||
return
|
||||
}
|
||||
try {
|
||||
submitting.value = true
|
||||
let apiUrl = ''
|
||||
let requestData = {}
|
||||
|
||||
let requestData = { task_type: activeFunction.value, prompt: currentPrompt.value }
|
||||
switch (activeFunction.value) {
|
||||
case 'text_to_image':
|
||||
apiUrl = '/api/jimeng/text-to-image'
|
||||
requestData = {
|
||||
prompt: textToImageParams.prompt,
|
||||
Object.assign(requestData, {
|
||||
width: parseInt(textToImageParams.size.split('x')[0]),
|
||||
height: parseInt(textToImageParams.size.split('x')[1]),
|
||||
scale: textToImageParams.scale,
|
||||
seed: textToImageParams.seed,
|
||||
use_pre_llm: textToImageParams.use_pre_llm,
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'image_to_image_portrait':
|
||||
apiUrl = '/api/jimeng/image-to-image-portrait'
|
||||
requestData = {
|
||||
case 'image_to_image':
|
||||
Object.assign(requestData, {
|
||||
image_input: imageToImageParams.image_input,
|
||||
prompt: imageToImageParams.prompt,
|
||||
width: parseInt(imageToImageParams.size.split('x')[0]),
|
||||
height: parseInt(imageToImageParams.size.split('x')[1]),
|
||||
gpen: imageToImageParams.gpen,
|
||||
@@ -289,57 +367,41 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
skin_unifi: imageToImageParams.skin_unifi,
|
||||
gen_mode: imageToImageParams.gen_mode,
|
||||
seed: imageToImageParams.seed,
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'image_edit':
|
||||
apiUrl = '/api/jimeng/image-edit'
|
||||
requestData = {
|
||||
Object.assign(requestData, {
|
||||
image_urls: imageEditParams.image_urls,
|
||||
prompt: imageEditParams.prompt,
|
||||
scale: imageEditParams.scale,
|
||||
seed: imageEditParams.seed,
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'image_effects':
|
||||
apiUrl = '/api/jimeng/image-effects'
|
||||
requestData = {
|
||||
image_input1: imageEffectsParams.image_input1,
|
||||
Object.assign(requestData, {
|
||||
image_input: imageEffectsParams.image_input1,
|
||||
template_id: imageEffectsParams.template_id,
|
||||
width: parseInt(imageEffectsParams.size.split('x')[0]),
|
||||
height: parseInt(imageEffectsParams.size.split('x')[1]),
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'text_to_video':
|
||||
apiUrl = '/api/jimeng/text-to-video'
|
||||
requestData = {
|
||||
prompt: textToVideoParams.prompt,
|
||||
Object.assign(requestData, {
|
||||
aspect_ratio: textToVideoParams.aspect_ratio,
|
||||
seed: textToVideoParams.seed,
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case 'image_to_video':
|
||||
apiUrl = '/api/jimeng/image-to-video'
|
||||
requestData = {
|
||||
Object.assign(requestData, {
|
||||
image_urls: imageToVideoParams.image_urls,
|
||||
prompt: imageToVideoParams.prompt,
|
||||
aspect_ratio: imageToVideoParams.aspect_ratio,
|
||||
seed: imageToVideoParams.seed,
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
const response = await httpPost(apiUrl, requestData)
|
||||
|
||||
const response = await httpPost('/api/jimeng/task', requestData)
|
||||
if (response.data) {
|
||||
showMessageOK('任务提交成功')
|
||||
// 重新获取任务列表
|
||||
await fetchData(1)
|
||||
// 开始轮询
|
||||
startPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交任务失败:', error)
|
||||
@@ -348,42 +410,7 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取待处理任务数量
|
||||
const getPendingCount = async () => {
|
||||
try {
|
||||
const response = await httpGet('/api/jimeng/pending-count')
|
||||
return response.data?.count || 0
|
||||
} catch (error) {
|
||||
console.error('获取待处理任务数量失败:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 开始轮询
|
||||
const startPolling = () => {
|
||||
if (taskPulling.value) return
|
||||
|
||||
taskPulling.value = true
|
||||
pullHandler.value = setInterval(async () => {
|
||||
const pendingCount = await getPendingCount()
|
||||
if (pendingCount > 0) {
|
||||
await fetchData(page.value)
|
||||
} else {
|
||||
stopPolling()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 停止轮询
|
||||
const stopPolling = () => {
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value)
|
||||
pullHandler.value = null
|
||||
}
|
||||
taskPulling.value = false
|
||||
}
|
||||
|
||||
|
||||
// 重试任务
|
||||
const retryTask = async (taskId) => {
|
||||
try {
|
||||
@@ -391,14 +418,13 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
if (response.data) {
|
||||
showMessageOK('重试任务已提交')
|
||||
await fetchData(page.value)
|
||||
startPolling()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重试任务失败:', error)
|
||||
showMessageError(error.message || '重试任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除任务
|
||||
const removeJob = async (item) => {
|
||||
try {
|
||||
@@ -407,7 +433,7 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
|
||||
|
||||
const response = await httpGet('/api/jimeng/remove', { id: item.id })
|
||||
if (response.data) {
|
||||
showMessageOK('删除成功')
|
||||
@@ -420,13 +446,13 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 播放视频
|
||||
const playVideo = (item) => {
|
||||
currentVideoUrl.value = item.video_url
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (item) => {
|
||||
const url = item.video_url || item.img_url
|
||||
@@ -437,12 +463,68 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
link.click()
|
||||
}
|
||||
}
|
||||
|
||||
// 清理
|
||||
|
||||
// 画同款功能
|
||||
const drawSame = (item) => {
|
||||
// 联动功能开关
|
||||
if (item.type === 'text_to_image' || item.type === 'image_to_image') {
|
||||
activeCategory.value = 'image_generation'
|
||||
useImageInput.value = item.type === 'image_to_image'
|
||||
} else if (item.type === 'text_to_video' || item.type === 'image_to_video') {
|
||||
activeCategory.value = 'video_generation'
|
||||
useImageInput.value = item.type === 'image_to_video'
|
||||
} else if (item.type === 'image_edit') {
|
||||
activeCategory.value = 'image_editing'
|
||||
} else if (item.type === 'image_effects') {
|
||||
activeCategory.value = 'image_effects'
|
||||
}
|
||||
switchFunction(item.type)
|
||||
nextTick(() => {
|
||||
currentPrompt.value = item.prompt
|
||||
})
|
||||
if (item.type === 'text_to_image') {
|
||||
if (item.width && item.height) {
|
||||
textToImageParams.size = `${item.width}x${item.height}`
|
||||
}
|
||||
if (item.scale) textToImageParams.scale = item.scale
|
||||
if (item.seed) textToImageParams.seed = item.seed
|
||||
if (item.use_pre_llm !== undefined) textToImageParams.use_pre_llm = item.use_pre_llm
|
||||
} else if (item.type === 'image_to_image') {
|
||||
if (item.image_input) imageToImageParams.image_input = item.image_input
|
||||
if (item.width && item.height) {
|
||||
imageToImageParams.size = `${item.width}x${item.height}`
|
||||
}
|
||||
if (item.gpen) imageToImageParams.gpen = item.gpen
|
||||
if (item.skin) imageToImageParams.skin = item.skin
|
||||
if (item.skin_unifi) imageToImageParams.skin_unifi = item.skin_unifi
|
||||
if (item.gen_mode) imageToImageParams.gen_mode = item.gen_mode
|
||||
if (item.seed) imageToImageParams.seed = item.seed
|
||||
} else if (item.type === 'image_edit') {
|
||||
if (item.image_urls) imageEditParams.image_urls = item.image_urls
|
||||
if (item.scale) imageEditParams.scale = item.scale
|
||||
if (item.seed) imageEditParams.seed = item.seed
|
||||
} else if (item.type === 'image_effects') {
|
||||
if (item.image_input1) imageEffectsParams.image_input1 = item.image_input1
|
||||
if (item.template_id) imageEffectsParams.template_id = item.template_id
|
||||
if (item.width && item.height) {
|
||||
imageEffectsParams.size = `${item.width}x${item.height}`
|
||||
}
|
||||
} else if (item.type === 'text_to_video') {
|
||||
if (item.aspect_ratio) textToVideoParams.aspect_ratio = item.aspect_ratio
|
||||
if (item.seed) textToVideoParams.seed = item.seed
|
||||
} else if (item.type === 'image_to_video') {
|
||||
if (item.image_urls) imageToVideoParams.image_urls = item.image_urls
|
||||
if (item.aspect_ratio) imageToVideoParams.aspect_ratio = item.aspect_ratio
|
||||
if (item.seed) imageToVideoParams.seed = item.seed
|
||||
}
|
||||
showMessageOK('已填入全部参数,可直接生成同款')
|
||||
}
|
||||
|
||||
// 页面卸载时清理轮询
|
||||
const cleanup = () => {
|
||||
stopPolling()
|
||||
}
|
||||
|
||||
|
||||
// 返回所有状态和方法
|
||||
return {
|
||||
// 状态
|
||||
@@ -463,27 +545,28 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
showDialog,
|
||||
currentVideoUrl,
|
||||
nodata,
|
||||
|
||||
|
||||
// 配置
|
||||
categories,
|
||||
functions,
|
||||
currentFunctions,
|
||||
|
||||
|
||||
// 参数
|
||||
currentPrompt,
|
||||
textToImageParams,
|
||||
imageToImageParams,
|
||||
imageEditParams,
|
||||
imageEffectsParams,
|
||||
textToVideoParams,
|
||||
imageToVideoParams,
|
||||
|
||||
|
||||
// 计算属性
|
||||
currentFunction,
|
||||
needsPrompt,
|
||||
needsImage,
|
||||
needsMultipleImages,
|
||||
currentPowerCost,
|
||||
|
||||
|
||||
// 方法
|
||||
init,
|
||||
switchCategory,
|
||||
@@ -497,17 +580,33 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
updateCurrentList,
|
||||
fetchData,
|
||||
submitTask,
|
||||
getPendingCount,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
retryTask,
|
||||
removeJob,
|
||||
playVideo,
|
||||
downloadFile,
|
||||
cleanup,
|
||||
|
||||
drawSame,
|
||||
|
||||
// 工具函数
|
||||
substr,
|
||||
replaceImg,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const imageSizeOptions = [
|
||||
{ label: '1:1 (1328x1328)', value: '1328x1328' },
|
||||
{ label: '3:2 (1584x1056)', value: '1584x1056' },
|
||||
{ label: '2:3 (1056x1584)', value: '1056x1584' },
|
||||
{ label: '4:3 (1472x1104)', value: '1472x1104' },
|
||||
{ label: '3:4 (1104x1472)', value: '1104x1472' },
|
||||
{ label: '16:9 (1664x936)', value: '1664x936' },
|
||||
{ label: '9:16 (936x1664)', value: '936x1664' },
|
||||
{ label: '21:9 (2016x864)', value: '2016x864' },
|
||||
{ label: '9:21 (864x2016)', value: '864x2016' },
|
||||
]
|
||||
|
||||
export const videoAspectRatioOptions = [
|
||||
{ label: '1:1 (正方形)', value: '1:1' },
|
||||
{ label: '16:9 (横版)', value: '16:9' },
|
||||
{ label: '9:16 (竖版)', value: '9:16' },
|
||||
]
|
||||
|
||||
@@ -33,21 +33,10 @@
|
||||
<div class="switch-container">
|
||||
<div class="switch-info">
|
||||
<div class="switch-title">
|
||||
{{
|
||||
store.useImageInput
|
||||
? store.activeCategory === 'image_generation'
|
||||
? '图生图'
|
||||
: '图生视频'
|
||||
: store.activeCategory === 'image_generation'
|
||||
? '文生图'
|
||||
: '文生视频'
|
||||
}}
|
||||
</div>
|
||||
<div class="switch-desc">
|
||||
{{ store.useImageInput ? '使用图片作为输入' : '使用文字作为输入' }}
|
||||
{{ store.activeCategory === 'image_generation' ? '图生图人像写真' : '图生视频' }}
|
||||
</div>
|
||||
</div>
|
||||
<el-switch v-model="store.useImageInput" @change="store.switchInputMode" size="large" />
|
||||
<el-switch v-model="store.useImageInput" @change="store.switchInputMode" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,13 +46,10 @@
|
||||
<div v-if="store.activeFunction === 'text_to_image'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">提示词:</span>
|
||||
<el-tooltip content="输入你想要的图片内容描述" placement="right">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.textToImageParams.prompt"
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="请输入图片描述,越详细越好"
|
||||
@@ -77,36 +63,34 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.textToImageParams.size" placeholder="选择尺寸">
|
||||
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||
<el-option
|
||||
v-for="opt in imageSizeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">创意度:</span>
|
||||
<el-slider v-model="store.textToImageParams.scale" :min="1" :max="10" :step="0.5" />
|
||||
<div class="param-line">
|
||||
<span class="label"
|
||||
>创意度
|
||||
<el-tooltip content="创意度越高,影响文本描述的程度越高" placement="top">
|
||||
<i class="iconfont icon-info cursor-pointer ml-1"></i> </el-tooltip
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number
|
||||
v-model="store.textToImageParams.seed"
|
||||
:min="-1"
|
||||
:max="999999"
|
||||
size="small"
|
||||
/>
|
||||
<el-slider v-model="store.textToImageParams.scale" :min="1" :max="10" :step="0.5" />
|
||||
</div>
|
||||
|
||||
<div class="item-group flex justify-between">
|
||||
<span class="label">智能优化提示词</span>
|
||||
<el-switch v-model="store.textToImageParams.use_pre_llm" size="small" />
|
||||
<el-switch v-model="store.textToImageParams.use_pre_llm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生图 -->
|
||||
<div v-if="store.activeFunction === 'image_to_image_portrait'" class="function-panel">
|
||||
<div v-if="store.activeFunction === 'image_to_image'" class="function-panel">
|
||||
<div class="param-line pt">
|
||||
<span class="label">上传图片:</span>
|
||||
</div>
|
||||
@@ -119,7 +103,7 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.imageToImageParams.prompt"
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的图片效果"
|
||||
@@ -133,32 +117,14 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageToImageParams.size" placeholder="选择尺寸">
|
||||
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||
<el-option
|
||||
v-for="opt in imageSizeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">GPEN强度:</span>
|
||||
<el-slider v-model="store.imageToImageParams.gpen" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">肌肤质感:</span>
|
||||
<el-slider v-model="store.imageToImageParams.skin" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number
|
||||
v-model="store.imageToImageParams.seed"
|
||||
:min="-1"
|
||||
:max="999999"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像编辑 -->
|
||||
@@ -175,7 +141,7 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.imageEditParams.prompt"
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的编辑效果"
|
||||
@@ -188,16 +154,6 @@
|
||||
<span class="label">编辑强度:</span>
|
||||
<el-slider v-model="store.imageEditParams.scale" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number
|
||||
v-model="store.imageEditParams.seed"
|
||||
:min="-1"
|
||||
:max="999999"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像特效 -->
|
||||
@@ -225,10 +181,12 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageEffectsParams.size" placeholder="选择尺寸">
|
||||
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||
<el-option
|
||||
v-for="opt in imageSizeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,7 +198,7 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.textToVideoParams.prompt"
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的视频内容"
|
||||
@@ -254,21 +212,14 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.textToVideoParams.aspect_ratio" placeholder="选择比例">
|
||||
<el-option label="16:9 (横版)" value="16:9" />
|
||||
<el-option label="9:16 (竖版)" value="9:16" />
|
||||
<el-option label="1:1 (正方形)" value="1:1" />
|
||||
<el-option
|
||||
v-for="opt in videoAspectRatioOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number
|
||||
v-model="store.textToVideoParams.seed"
|
||||
:min="-1"
|
||||
:max="999999"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生视频 -->
|
||||
@@ -285,7 +236,7 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="store.imageToVideoParams.prompt"
|
||||
v-model="store.currentPrompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||
placeholder="描述你想要的视频效果"
|
||||
@@ -299,31 +250,18 @@
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="store.imageToVideoParams.aspect_ratio" placeholder="选择比例">
|
||||
<el-option label="16:9 (横版)" value="16:9" />
|
||||
<el-option label="9:16 (竖版)" value="9:16" />
|
||||
<el-option label="1:1 (正方形)" value="1:1" />
|
||||
<el-option
|
||||
v-for="opt in videoAspectRatioOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<span class="label">种子值:</span>
|
||||
<el-input-number
|
||||
v-model="store.imageToVideoParams.seed"
|
||||
:min="-1"
|
||||
:max="999999"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 算力显示 -->
|
||||
<div class="text-info">
|
||||
<el-tag type="primary">当前算力: {{ store.userPower }}</el-tag>
|
||||
<el-tag type="warning">消耗: {{ store.currentPowerCost }}</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div class="submit-btn">
|
||||
<div class="submit-btn flex justify-center pt-4">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="store.submitTask"
|
||||
@@ -369,91 +307,118 @@
|
||||
</div>
|
||||
|
||||
<div class="task-list">
|
||||
<div class="list-box" v-if="!store.noData">
|
||||
<div v-for="item in store.currentList" :key="item.id" class="task-item">
|
||||
<div class="task-left">
|
||||
<div class="task-preview">
|
||||
<el-image
|
||||
v-if="item.img_url"
|
||||
:src="item.img_url"
|
||||
fit="cover"
|
||||
class="preview-image"
|
||||
/>
|
||||
<video
|
||||
v-else-if="item.video_url"
|
||||
:src="item.video_url"
|
||||
class="preview-video"
|
||||
preload="metadata"
|
||||
/>
|
||||
<div v-else class="preview-placeholder">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>{{ store.getTaskStatusText(item.status) }}</span>
|
||||
<Waterfall
|
||||
:list="store.currentList"
|
||||
v-bind="waterfallOptions"
|
||||
:is-loading="store.loading"
|
||||
:is-over="store.currentList.length >= store.total"
|
||||
@afterRender="onWaterfallAfterRender"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="task-item">
|
||||
<!-- 保持原有内容 -->
|
||||
<div class="task-left">
|
||||
<div class="task-preview">
|
||||
<el-image
|
||||
v-if="item.img_url"
|
||||
:src="item.img_url"
|
||||
fit="cover"
|
||||
class="preview-image"
|
||||
/>
|
||||
<video
|
||||
v-else-if="item.video_url"
|
||||
:src="item.video_url"
|
||||
class="preview-video"
|
||||
preload="metadata"
|
||||
/>
|
||||
<div v-else class="preview-placeholder">
|
||||
<i class="iconfont icon-dalle text-2xl" v-if="item.type.includes('image')"></i>
|
||||
<i
|
||||
class="iconfont icon-video text-2xl"
|
||||
v-else-if="item.type.includes('video')"
|
||||
></i>
|
||||
<span>{{ store.getTaskStatusText(item.status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-center">
|
||||
<div class="task-info flex justify-between">
|
||||
<div class="flex gap-2">
|
||||
<el-tag size="small" :type="store.getStatusType(item.status)">
|
||||
{{ store.getTaskStatusText(item.status) }}
|
||||
</el-tag>
|
||||
<el-tag size="small">{{ store.getFunctionName(item.type) }}</el-tag>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span>
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<i
|
||||
class="iconfont icon-copy cursor-pointer"
|
||||
@click="copyPrompt(item.prompt)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<span class="ml-1">
|
||||
<el-tooltip content="画同款" placement="top">
|
||||
<i
|
||||
class="iconfont icon-image-list cursor-pointer"
|
||||
@click="store.drawSame(item)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</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 class="task-right">
|
||||
<div class="task-actions">
|
||||
<el-button
|
||||
v-if="item.status === 'failed'"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="store.retryTask(item.id)"
|
||||
>
|
||||
重试
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.video_url || item.img_url"
|
||||
type="default"
|
||||
size="small"
|
||||
@click="store.downloadFile(item)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.video_url"
|
||||
type="default"
|
||||
size="small"
|
||||
@click="store.playVideo(item)"
|
||||
>
|
||||
播放
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
v-if="item.status === 'failed'"
|
||||
size="small"
|
||||
@click="store.removeJob(item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-center">
|
||||
<div class="task-info">
|
||||
<el-tag size="small" :type="store.getStatusType(item.status)">
|
||||
{{ store.getTaskStatusText(item.status) }}
|
||||
</el-tag>
|
||||
<el-tag size="small">{{ store.getFunctionName(item.type) }}</el-tag>
|
||||
</div>
|
||||
<div class="task-prompt">
|
||||
{{ store.substr(item.prompt, 200) }}
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>{{ dateFormat(item.created_at) }}</span>
|
||||
<span v-if="item.power">{{ item.power }}算力</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-right">
|
||||
<div class="task-actions">
|
||||
<el-button
|
||||
v-if="item.status === 'failed'"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="store.retryTask(item.id)"
|
||||
>
|
||||
重试
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.video_url || item.img_url"
|
||||
type="default"
|
||||
size="small"
|
||||
@click="store.downloadFile(item)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.video_url"
|
||||
type="default"
|
||||
size="small"
|
||||
@click="store.playVideo(item)"
|
||||
>
|
||||
播放
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="store.removeJob(item)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else :image="store.nodata" description="暂无任务,快去创建吧!" />
|
||||
|
||||
<div class="pagination" v-if="store.total > store.pageSize">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, prev, pager, next"
|
||||
:current-page="store.page"
|
||||
:page-size="store.pageSize"
|
||||
:total="store.total"
|
||||
@current-change="store.fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Waterfall>
|
||||
<el-empty v-if="store.noData" :image="store.nodata" description="暂无任务,快去创建吧!" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -469,12 +434,17 @@
|
||||
<script setup>
|
||||
import '@/assets/css/jimeng.styl'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
import { useJimengStore } from '@/store/jimeng'
|
||||
import { imageSizeOptions, useJimengStore, videoAspectRatioOptions } from '@/store/jimeng'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { dateFormat } from '@/utils/libs'
|
||||
import { InfoFilled, Picture, Switch } from '@element-plus/icons-vue'
|
||||
import { Switch } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
|
||||
const store = useJimengStore()
|
||||
const sharedStore = useSharedStore()
|
||||
const waterfallOptions = sharedStore.waterfallOptions
|
||||
|
||||
// 获取分类图标
|
||||
const getCategoryIcon = (category) => {
|
||||
@@ -487,6 +457,8 @@ const getCategoryIcon = (category) => {
|
||||
return iconMap[category] || 'iconfont icon-image'
|
||||
}
|
||||
|
||||
const store = useJimengStore()
|
||||
|
||||
onMounted(() => {
|
||||
store.init()
|
||||
})
|
||||
@@ -494,4 +466,43 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
store.cleanup()
|
||||
})
|
||||
|
||||
// 自动加载下一页逻辑
|
||||
function onWaterfallAfterRender() {
|
||||
if (!store.loading && store.currentList.length < store.total) {
|
||||
store.fetchData(store.page + 1)
|
||||
}
|
||||
}
|
||||
|
||||
function copyPrompt(prompt) {
|
||||
navigator.clipboard
|
||||
.writeText(prompt)
|
||||
.then(() => {
|
||||
ElMessage.success('提示词已复制')
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('复制失败')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.task-list {
|
||||
.task-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.task-list .task-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.task-list .task-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.text_to_image"
|
||||
:min="1"
|
||||
:max="100"
|
||||
placeholder="请输入文生图算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -78,7 +77,6 @@
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.image_to_image"
|
||||
:min="1"
|
||||
:max="100"
|
||||
placeholder="请输入图生图算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -102,7 +100,6 @@
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.image_edit"
|
||||
:min="1"
|
||||
:max="100"
|
||||
placeholder="请输入图片编辑算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -126,7 +123,6 @@
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.image_effects"
|
||||
:min="1"
|
||||
:max="100"
|
||||
placeholder="请输入图片特效算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -150,7 +146,6 @@
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.text_to_video"
|
||||
:min="1"
|
||||
:max="100"
|
||||
placeholder="请输入文生视频算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -174,7 +169,6 @@
|
||||
<el-input-number
|
||||
v-model="jimengConfig.power.image_to_video"
|
||||
:min="1"
|
||||
:max="100"
|
||||
placeholder="请输入图生视频算力消耗"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -243,16 +237,10 @@ const saveConfig = async () => {
|
||||
try {
|
||||
await configFormRef.value.validate()
|
||||
saving.value = true
|
||||
|
||||
await httpPost('/api/admin/jimeng/config', {
|
||||
config: jimengConfig.value,
|
||||
})
|
||||
|
||||
await httpPost('/api/admin/jimeng/config/update', jimengConfig.value)
|
||||
ElMessage.success('配置保存成功!')
|
||||
} catch (e) {
|
||||
if (e.message) {
|
||||
ElMessage.error('保存失败:' + e.message)
|
||||
}
|
||||
ElMessage.error(e.message)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user