feat: support custom menu
@ -217,6 +217,7 @@ func needLogin(c *gin.Context) bool {
|
|||||||
c.Request.URL.Path == "/api/sd/client" ||
|
c.Request.URL.Path == "/api/sd/client" ||
|
||||||
c.Request.URL.Path == "/api/config/get" ||
|
c.Request.URL.Path == "/api/config/get" ||
|
||||||
c.Request.URL.Path == "/api/product/list" ||
|
c.Request.URL.Path == "/api/product/list" ||
|
||||||
|
c.Request.URL.Path == "/api/menu/list" ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||||
|
@ -144,11 +144,11 @@ type SystemConfig struct {
|
|||||||
PowerPrice float64 `json:"power_price,omitempty"` // 算力单价
|
PowerPrice float64 `json:"power_price,omitempty"` // 算力单价
|
||||||
|
|
||||||
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
|
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
|
||||||
VipInfoText string `json:"vip_info_text"` // 会员页面充值说明
|
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
|
||||||
DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型
|
DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型
|
||||||
|
|
||||||
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
|
||||||
MjActionPower int `json:"mj_action_power"` // MJ 操作(放大,变换)消耗算力
|
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
|
||||||
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
|
||||||
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
|
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
|
||||||
|
|
||||||
|
121
api/handler/admin/menu_handler.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/handler"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MenuHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||||
|
return &MenuHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MenuHandler) Save(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
SortNum int `json:"sort_num"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.DB.Save(&model.Menu{
|
||||||
|
Id: data.Id,
|
||||||
|
Name: data.Name,
|
||||||
|
Icon: data.Icon,
|
||||||
|
URL: data.URL,
|
||||||
|
SortNum: data.SortNum,
|
||||||
|
Enabled: data.Enabled,
|
||||||
|
})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 数据列表
|
||||||
|
func (h *MenuHandler) List(c *gin.Context) {
|
||||||
|
var items []model.Menu
|
||||||
|
var list = make([]vo.Menu, 0)
|
||||||
|
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var product vo.Menu
|
||||||
|
err := utils.CopyObject(item, &product)
|
||||||
|
if err == nil {
|
||||||
|
list = append(list, product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MenuHandler) Enable(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MenuHandler) Sort(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Ids []uint `json:"ids"`
|
||||||
|
Sorts []int `json:"sorts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, id := range data.Ids {
|
||||||
|
res := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index])
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MenuHandler) Remove(c *gin.Context) {
|
||||||
|
id := h.GetInt(c, "id", 0)
|
||||||
|
|
||||||
|
if id > 0 {
|
||||||
|
res := h.DB.Where("id", id).Delete(&model.Menu{})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
@ -65,21 +65,11 @@ func (h *ProductHandler) Save(c *gin.Context) {
|
|||||||
resp.SUCCESS(c, itemVo)
|
resp.SUCCESS(c, itemVo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List 模型列表
|
// List 数据列表
|
||||||
func (h *ProductHandler) List(c *gin.Context) {
|
func (h *ProductHandler) List(c *gin.Context) {
|
||||||
if err := utils.CheckPermission(c, h.DB); err != nil {
|
|
||||||
resp.NotPermission(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session := h.DB.Session(&gorm.Session{})
|
|
||||||
enable := h.GetBool(c, "enable")
|
|
||||||
if enable {
|
|
||||||
session = session.Where("enabled", enable)
|
|
||||||
}
|
|
||||||
var items []model.Product
|
var items []model.Product
|
||||||
var list = make([]vo.Product, 0)
|
var list = make([]vo.Product, 0)
|
||||||
res := session.Order("sort_num ASC").Find(&items)
|
res := h.DB.Order("sort_num ASC").Find(&items)
|
||||||
if res.Error == nil {
|
if res.Error == nil {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
var product vo.Product
|
var product vo.Product
|
||||||
@ -128,7 +118,7 @@ func (h *ProductHandler) Sort(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for index, id := range data.Ids {
|
for index, id := range data.Ids {
|
||||||
res := h.DB.Model(&model.Product{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
res := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index])
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
return
|
return
|
||||||
@ -142,7 +132,7 @@ func (h *ProductHandler) Remove(c *gin.Context) {
|
|||||||
id := h.GetInt(c, "id", 0)
|
id := h.GetInt(c, "id", 0)
|
||||||
|
|
||||||
if id > 0 {
|
if id > 0 {
|
||||||
res := h.DB.Where("id = ?", id).Delete(&model.Product{})
|
res := h.DB.Where("id", id).Delete(&model.Product{})
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
return
|
return
|
||||||
|
@ -25,9 +25,10 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
|||||||
all := h.GetBool(c, "all")
|
all := h.GetBool(c, "all")
|
||||||
userId := h.GetLoginUserId(c)
|
userId := h.GetLoginUserId(c)
|
||||||
var roles []model.ChatRole
|
var roles []model.ChatRole
|
||||||
|
var roleVos = make([]vo.ChatRole, 0)
|
||||||
res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles)
|
res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "No roles found,"+res.Error.Error())
|
resp.SUCCESS(c, roleVos)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +56,7 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
|||||||
resp.ERROR(c, "角色解析失败!")
|
resp.ERROR(c, "角色解析失败!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 转成 vo
|
|
||||||
var roleVos = make([]vo.ChatRole, 0)
|
|
||||||
for _, r := range roles {
|
for _, r := range roles {
|
||||||
if !utils.ContainsStr(roleKeys, r.Key) {
|
if !utils.ContainsStr(roleKeys, r.Key) {
|
||||||
continue
|
continue
|
||||||
|
36
api/handler/menu_handler.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MenuHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
|
||||||
|
return &MenuHandler{BaseHandler: BaseHandler{App: app, DB: db}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 数据列表
|
||||||
|
func (h *MenuHandler) List(c *gin.Context) {
|
||||||
|
var items []model.Menu
|
||||||
|
var list = make([]vo.Menu, 0)
|
||||||
|
res := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var product vo.Menu
|
||||||
|
err := utils.CopyObject(item, &product)
|
||||||
|
if err == nil {
|
||||||
|
list = append(list, product)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, list)
|
||||||
|
}
|
@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"chatplus/core"
|
"chatplus/core"
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
|
"chatplus/service"
|
||||||
"chatplus/service/oss"
|
"chatplus/service/oss"
|
||||||
"chatplus/service/sd"
|
"chatplus/service/sd"
|
||||||
"chatplus/store/model"
|
"chatplus/store/model"
|
||||||
@ -26,12 +27,14 @@ type SdJobHandler struct {
|
|||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
pool *sd.ServicePool
|
pool *sd.ServicePool
|
||||||
uploader *oss.UploaderManager
|
uploader *oss.UploaderManager
|
||||||
|
snowflake *service.Snowflake
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager) *SdJobHandler {
|
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager, snowflake *service.Snowflake) *SdJobHandler {
|
||||||
return &SdJobHandler{
|
return &SdJobHandler{
|
||||||
pool: pool,
|
pool: pool,
|
||||||
uploader: manager,
|
uploader: manager,
|
||||||
|
snowflake: snowflake,
|
||||||
BaseHandler: BaseHandler{
|
BaseHandler: BaseHandler{
|
||||||
App: app,
|
App: app,
|
||||||
DB: db,
|
DB: db,
|
||||||
@ -116,8 +119,13 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
idValue, _ := c.Get(types.LoginUserID)
|
idValue, _ := c.Get(types.LoginUserID)
|
||||||
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||||
|
taskId, err := h.snowflake.Next(true)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "error with generate task id: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
params := types.SdTaskParams{
|
params := types.SdTaskParams{
|
||||||
TaskId: fmt.Sprintf("task(%s)", utils.RandString(15)),
|
TaskId: taskId,
|
||||||
Prompt: data.Prompt,
|
Prompt: data.Prompt,
|
||||||
NegativePrompt: data.NegativePrompt,
|
NegativePrompt: data.NegativePrompt,
|
||||||
Steps: data.Steps,
|
Steps: data.Steps,
|
||||||
|
14
api/main.go
@ -417,6 +417,20 @@ func main() {
|
|||||||
group := s.Engine.Group("/api/admin/powerLog/")
|
group := s.Engine.Group("/api/admin/powerLog/")
|
||||||
group.POST("list", h.List)
|
group.POST("list", h.List)
|
||||||
}),
|
}),
|
||||||
|
fx.Provide(admin.NewMenuHandler),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) {
|
||||||
|
group := s.Engine.Group("/api/admin/menu/")
|
||||||
|
group.POST("save", h.Save)
|
||||||
|
group.GET("list", h.List)
|
||||||
|
group.POST("enable", h.Enable)
|
||||||
|
group.POST("sort", h.Sort)
|
||||||
|
group.GET("remove", h.Remove)
|
||||||
|
}),
|
||||||
|
fx.Provide(handler.NewMenuHandler),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) {
|
||||||
|
group := s.Engine.Group("/api/menu/")
|
||||||
|
group.GET("list", h.List)
|
||||||
|
}),
|
||||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||||
err := s.Run(db)
|
err := s.Run(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -181,6 +181,7 @@ func (p *ServicePool) SyncTaskProgress() {
|
|||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if servicePlus := p.getService(job.ChannelId); servicePlus != nil {
|
if servicePlus := p.getService(job.ChannelId); servicePlus != nil {
|
||||||
|
@ -77,6 +77,13 @@ func (s *Service) Run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var job model.MidJourneyJob
|
||||||
|
tx := s.db.Where("id = ?", task.Id).First(&job)
|
||||||
|
if tx.Error != nil {
|
||||||
|
logger.Error("任务不存在,任务ID:", task.TaskId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
logger.Infof("%s handle a new MidJourney task: %+v", s.Name, task)
|
logger.Infof("%s handle a new MidJourney task: %+v", s.Name, task)
|
||||||
var res ImageRes
|
var res ImageRes
|
||||||
switch task.Type {
|
switch task.Type {
|
||||||
@ -97,8 +104,6 @@ func (s *Service) Run() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
var job model.MidJourneyJob
|
|
||||||
s.db.Where("id = ?", task.Id).First(&job)
|
|
||||||
if err != nil || (res.Code != 1 && res.Code != 22) {
|
if err != nil || (res.Code != 1 && res.Code != 22) {
|
||||||
errMsg := fmt.Sprintf("%v,%s", err, res.Description)
|
errMsg := fmt.Sprintf("%v,%s", err, res.Description)
|
||||||
logger.Error("绘画任务执行失败:", errMsg)
|
logger.Error("绘画任务执行失败:", errMsg)
|
||||||
@ -127,14 +132,17 @@ func (s *Service) canHandleTask() bool {
|
|||||||
return handledNum < s.maxHandleTaskNum
|
return handledNum < s.maxHandleTaskNum
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the expired tasks
|
// remove the timeout tasks
|
||||||
func (s *Service) checkTasks() {
|
func (s *Service) checkTasks() {
|
||||||
for k, t := range s.taskStartTimes {
|
for k, t := range s.taskStartTimes {
|
||||||
if time.Now().Unix()-t.Unix() > s.taskTimeout {
|
if time.Now().Unix()-t.Unix() > s.taskTimeout {
|
||||||
delete(s.taskStartTimes, k)
|
delete(s.taskStartTimes, k)
|
||||||
atomic.AddInt32(&s.HandledTaskNum, -1)
|
atomic.AddInt32(&s.HandledTaskNum, -1)
|
||||||
// delete task from database
|
|
||||||
s.db.Delete(&model.MidJourneyJob{Id: uint(k)}, "progress < 100")
|
s.db.Model(&model.MidJourneyJob{Id: uint(k)}).UpdateColumns(map[string]interface{}{
|
||||||
|
"progress": -1,
|
||||||
|
"err_msg": "任务超时",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,7 @@ func (s *Service) Txt2Img(task types.SdTask) error {
|
|||||||
Width: task.Params.Width,
|
Width: task.Params.Width,
|
||||||
Height: task.Params.Height,
|
Height: task.Params.Height,
|
||||||
SamplerName: task.Params.Sampler,
|
SamplerName: task.Params.Sampler,
|
||||||
|
ForceTaskId: task.Params.TaskId,
|
||||||
}
|
}
|
||||||
if task.Params.Seed > 0 {
|
if task.Params.Seed > 0 {
|
||||||
body.Seed = task.Params.Seed
|
body.Seed = task.Params.Seed
|
||||||
|
11
api/store/model/menu.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Menu 系统菜单
|
||||||
|
type Menu struct {
|
||||||
|
Id uint `gorm:"primarykey;column:id"`
|
||||||
|
Name string // 菜单名称
|
||||||
|
Icon string // 菜单图标
|
||||||
|
URL string // 菜单跳转地址
|
||||||
|
SortNum int // 排序
|
||||||
|
Enabled bool // 启用状态
|
||||||
|
}
|
11
api/store/vo/menu.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
// Menu 系统菜单
|
||||||
|
type Menu struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
SortNum int `json:"sort_num"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
19
database/update-v4.0.2.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- 菜单表
|
||||||
|
CREATE TABLE `chatgpt_plus`.`chatgpt_menus` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT ,
|
||||||
|
`name` VARCHAR(30) NOT NULL COMMENT '菜单名称' ,
|
||||||
|
`icon` VARCHAR(150) NOT NULL COMMENT '菜单图标' ,
|
||||||
|
`url` VARCHAR(100) NOT NULL COMMENT '地址' ,
|
||||||
|
`sort_num` SMALLINT(3) NOT NULL COMMENT '排序' ,
|
||||||
|
`enabled` TINYINT(1) NOT NULL COMMENT '是否启用' ,
|
||||||
|
PRIMARY KEY (`id`)) ENGINE = InnoDB COMMENT = '前端菜单表';
|
||||||
|
|
||||||
|
INSERT INTO `chatgpt_menus` (`id`, `name`, `icon`, `url`, `sort_num`, `enabled`) VALUES
|
||||||
|
(1, '对话聊天', '/images/menu/chat.png', '/chat', 0, 1),
|
||||||
|
(5, 'MJ 绘画', '/images/menu/mj.png', '/mj', 1, 1),
|
||||||
|
(6, 'SD 绘画', '/images/menu/sd.png', '/sd', 2, 1),
|
||||||
|
(7, '算力日志', '/images/menu/log.png', '/powerLog', 5, 1),
|
||||||
|
(8, '应用中心', '/images/menu/app.png', '/apps', 3, 1),
|
||||||
|
(9, '作品展示', '/images/menu/img-wall.png', '/images-wall', 4, 1),
|
||||||
|
(10, '会员计划', '/images/menu/member.png', '/member', 6, 1),
|
||||||
|
(11, '分享计划', '/images/menu/share.png', '/invite', 7, 1);
|
BIN
web/public/images/menu/app.png
Normal file
After Width: | Height: | Size: 741 B |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
BIN
web/public/images/menu/img-wall.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/images/menu/log.png
Normal file
After Width: | Height: | Size: 939 B |
BIN
web/public/images/menu/member.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
web/public/images/menu/share.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
@ -173,7 +173,7 @@ const routes = [
|
|||||||
name: 'admin-manger',
|
name: 'admin-manger',
|
||||||
meta: {title: '管理员'},
|
meta: {title: '管理员'},
|
||||||
component: () => import('@/views/admin/Manager.vue'),
|
component: () => import('@/views/admin/Manager.vue'),
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -313,12 +313,16 @@ httpGet("/api/config/get?key=system").then(res => {
|
|||||||
|
|
||||||
// 获取系统公告
|
// 获取系统公告
|
||||||
httpGet("/api/config/get?key=notice").then(res => {
|
httpGet("/api/config/get?key=notice").then(res => {
|
||||||
|
try {
|
||||||
notice.value = md.render(res.data['content'])
|
notice.value = md.render(res.data['content'])
|
||||||
const oldNotice = localStorage.getItem(noticeKey.value);
|
const oldNotice = localStorage.getItem(noticeKey.value);
|
||||||
// 如果公告有更新,则显示公告
|
// 如果公告有更新,则显示公告
|
||||||
if (oldNotice !== notice.value && notice.value.length > 10) {
|
if (oldNotice !== notice.value && notice.value.length > 10) {
|
||||||
showNotice.value = true
|
showNotice.value = true
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
ElMessage.error("获取系统配置失败:" + e.message)
|
ElMessage.error("获取系统配置失败:" + e.message)
|
||||||
})
|
})
|
||||||
|
@ -11,10 +11,9 @@
|
|||||||
<!-- -->
|
<!-- -->
|
||||||
<!-- </el-tooltip>-->
|
<!-- </el-tooltip>-->
|
||||||
<a @click="changeNav(item)" :class="item.path === curPath ? 'active' : ''">
|
<a @click="changeNav(item)" :class="item.path === curPath ? 'active' : ''">
|
||||||
<el-image :src="item.icon_path" :width="20" v-if="item.icon_path"/>
|
<el-image :src="item.icon" :width="20"/>
|
||||||
<i :class="'iconfont icon-' + item.icon" v-else></i>
|
|
||||||
</a>
|
</a>
|
||||||
<div :class="item.path === curPath ? 'title active' : 'title'">{{ item.title }}</div>
|
<div :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -37,21 +36,12 @@ import {ElMessage} from "element-plus";
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const logo = ref('/images/logo.png');
|
const logo = ref('/images/logo.png');
|
||||||
const navs = ref([
|
const navs = ref([])
|
||||||
{path: "/chat", icon_path: "/images/chat.png", title: "对话聊天"},
|
|
||||||
{path: "/mj", icon_path: "/images/mj.png", title: "MJ 绘画"},
|
|
||||||
{path: "/sd", icon_path: "/images/sd.png", title: "SD 绘画"},
|
|
||||||
{path: "/apps", icon: "menu", title: "应用中心"},
|
|
||||||
{path: "/images-wall", icon: "image-list", title: "作品展示"},
|
|
||||||
{path: "/powerLog", icon: "log", title: "消费日志"},
|
|
||||||
{path: "/member", icon: "vip-user", title: "会员计划"},
|
|
||||||
{path: "/invite", icon: "share", title: "推广计划"},
|
|
||||||
])
|
|
||||||
const curPath = ref(router.currentRoute.value.path)
|
const curPath = ref(router.currentRoute.value.path)
|
||||||
|
|
||||||
const changeNav = (item) => {
|
const changeNav = (item) => {
|
||||||
curPath.value = item.path
|
curPath.value = item.url
|
||||||
router.push(item.path)
|
router.push(item.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -60,6 +50,12 @@ onMounted(() => {
|
|||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
ElMessage.error("获取系统配置失败:" + e.message)
|
ElMessage.error("获取系统配置失败:" + e.message)
|
||||||
})
|
})
|
||||||
|
// 获取菜单
|
||||||
|
httpGet("/api/menu/list").then(res => {
|
||||||
|
navs.value = res.data
|
||||||
|
}).catch(e => {
|
||||||
|
ElMessage.error("获取系统菜单失败:" + e.message)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
247
web/src/views/admin/Menu.vue
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container menu" v-loading="loading">
|
||||||
|
|
||||||
|
<div class="handle-box">
|
||||||
|
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||||
|
<el-table-column prop="name" label="菜单名称">
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="sort" :data-id="scope.row.id">{{ scope.row.name }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="icon" label="菜单图标">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-image class="menu-icon" :src="scope.row.icon"/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="url" label="菜单URL"/>
|
||||||
|
<el-table-column prop="enabled" label="启用状态">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-switch v-model="scope.row['enabled']" @change="enable(scope.row)"/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
|
||||||
|
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)" :width="200">
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="showDialog"
|
||||||
|
:title="title"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||||
|
<el-form-item label="菜单名称:" prop="name">
|
||||||
|
<el-input v-model="item.name" autocomplete="off"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="菜单图标:" prop="icon">
|
||||||
|
<el-input v-model="item.icon" placeholder="菜单图标地址">
|
||||||
|
<template #append>
|
||||||
|
<el-upload
|
||||||
|
:auto-upload="true"
|
||||||
|
:show-file-list="false"
|
||||||
|
:http-request="uploadImg"
|
||||||
|
>
|
||||||
|
<el-icon class="uploader-icon">
|
||||||
|
<UploadFilled/>
|
||||||
|
</el-icon>
|
||||||
|
</el-upload>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="菜单URL:" prop="url">
|
||||||
|
<el-input v-model="item.url" autocomplete="off"/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="启用状态:" prop="enable">
|
||||||
|
<el-switch v-model="item.enabled"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="showDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="save">提交</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {onMounted, reactive, ref} from "vue";
|
||||||
|
import {httpGet, httpPost} from "@/utils/http";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||||
|
import {Plus, UploadFilled} from "@element-plus/icons-vue";
|
||||||
|
import {Sortable} from "sortablejs";
|
||||||
|
import Compressor from "compressorjs";
|
||||||
|
|
||||||
|
// 变量定义
|
||||||
|
const items = ref([])
|
||||||
|
const item = ref({})
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const title = ref("")
|
||||||
|
const rules = reactive({
|
||||||
|
name: [{required: true, message: '请输入菜单名称', trigger: 'change',}],
|
||||||
|
icon: [{required: true, message: '请上传菜单图标', trigger: 'change',}],
|
||||||
|
url: [{required: true, message: '请输入菜单地址', trigger: 'change',}],
|
||||||
|
})
|
||||||
|
const loading = ref(true)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const fetchData = () => {
|
||||||
|
// 获取数据
|
||||||
|
httpGet('/api/admin/menu/list').then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
// 初始化数据
|
||||||
|
const arr = res.data;
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||||
|
}
|
||||||
|
items.value = arr
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}).catch(() => {
|
||||||
|
ElMessage.error("获取数据失败");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
// 初始化拖动排序插件
|
||||||
|
Sortable.create(drawBodyWrapper, {
|
||||||
|
sort: true,
|
||||||
|
animation: 500,
|
||||||
|
onEnd({newIndex, oldIndex, from}) {
|
||||||
|
if (oldIndex === newIndex) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id'));
|
||||||
|
const ids = []
|
||||||
|
const sorts = []
|
||||||
|
sortedData.forEach((id, index) => {
|
||||||
|
ids.push(parseInt(id))
|
||||||
|
sorts.push(index)
|
||||||
|
})
|
||||||
|
|
||||||
|
httpPost("/api/admin/menu/sort", {ids: ids, sorts: sorts}).catch(e => {
|
||||||
|
ElMessage.error("排序失败:" + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const add = function () {
|
||||||
|
title.value = "新增菜单"
|
||||||
|
showDialog.value = true
|
||||||
|
item.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = function (row) {
|
||||||
|
title.value = "修改菜单"
|
||||||
|
showDialog.value = true
|
||||||
|
item.value = row
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = function () {
|
||||||
|
formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
showDialog.value = false
|
||||||
|
if (!item.value.id) {
|
||||||
|
item.value.sort_num = items.value.length + 1
|
||||||
|
}
|
||||||
|
httpPost('/api/admin/menu/save', item.value).then(() => {
|
||||||
|
ElMessage.success('操作成功!')
|
||||||
|
fetchData()
|
||||||
|
}).catch((e) => {
|
||||||
|
ElMessage.error('操作失败,' + e.message)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const enable = (row) => {
|
||||||
|
httpPost('/api/admin/menu/enable', {id: row.id, enabled: row.enabled}).then(() => {
|
||||||
|
ElMessage.success("操作成功!")
|
||||||
|
}).catch(e => {
|
||||||
|
ElMessage.error("操作失败:" + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove = function (row) {
|
||||||
|
httpGet('/api/admin/menu/remove?id=' + row.id).then(() => {
|
||||||
|
ElMessage.success("删除成功!")
|
||||||
|
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||||
|
return v1.id === v2.id
|
||||||
|
})
|
||||||
|
}).catch((e) => {
|
||||||
|
ElMessage.error("删除失败:" + e.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片上传
|
||||||
|
const uploadImg = (file) => {
|
||||||
|
// 压缩图片并上传
|
||||||
|
new Compressor(file.file, {
|
||||||
|
quality: 0.6,
|
||||||
|
success(result) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', result, result.name);
|
||||||
|
// 执行上传操作
|
||||||
|
httpPost('/api/admin/upload', formData).then((res) => {
|
||||||
|
item.value.icon = res.data.url
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
}).catch((e) => {
|
||||||
|
ElMessage.error('上传失败:' + e.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
error(e) {
|
||||||
|
ElMessage.error('上传失败:' + e.message)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.menu {
|
||||||
|
|
||||||
|
.opt-box {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
display flex;
|
||||||
|
justify-content flex-end
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width 36px
|
||||||
|
height 36px
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container list" v-loading="loading">
|
<div class="container product" v-loading="loading">
|
||||||
|
|
||||||
<div class="handle-box">
|
<div class="handle-box">
|
||||||
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||||
@ -153,13 +153,13 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const add = function () {
|
const add = function () {
|
||||||
title.value = "新增模型"
|
title.value = "新增产品"
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
item.value = {}
|
item.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const edit = function (row) {
|
const edit = function (row) {
|
||||||
title.value = "修改模型"
|
title.value = "修改产品"
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
item.value = row
|
item.value = row
|
||||||
}
|
}
|
||||||
@ -206,7 +206,7 @@ const remove = function (row) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.list {
|
.product {
|
||||||
|
|
||||||
.opt-box {
|
.opt-box {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
@ -253,6 +253,10 @@
|
|||||||
<el-button type="primary" @click="save('notice')">保存</el-button>
|
<el-button type="primary" @click="save('notice')">保存</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="菜单配置" name="menu">
|
||||||
|
<Menu/>
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -265,6 +269,7 @@ import {ElMessage} from "element-plus";
|
|||||||
import {InfoFilled, UploadFilled} from "@element-plus/icons-vue";
|
import {InfoFilled, UploadFilled} from "@element-plus/icons-vue";
|
||||||
import MdEditor from "md-editor-v3";
|
import MdEditor from "md-editor-v3";
|
||||||
import 'md-editor-v3/lib/style.css';
|
import 'md-editor-v3/lib/style.css';
|
||||||
|
import Menu from "@/views/admin/Menu.vue";
|
||||||
|
|
||||||
const activeName = ref('basic')
|
const activeName = ref('basic')
|
||||||
const system = ref({models: []})
|
const system = ref({models: []})
|
||||||
|