mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-02-18 20:34:27 +08:00
merge v4.2.4
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
||||
# 更新日志
|
||||
|
||||
## v4.2.4
|
||||
|
||||
- 功能优化:更改前端构建技术选型,使用 Vite 构建,提升构建速度和兼容性
|
||||
- 功能优化:使用 SSE 发送消息,替换原来的 Websocket 消息方案
|
||||
- 功能新增:管理后台支持设置默认昵称
|
||||
- 功能优化:支持 Suno v4.5 模型支持
|
||||
- 功能新增:用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。
|
||||
- 功能优化:修改重新回答功能,撤回千面的问答内容为可编辑内容,撤回的内容不会增加额外的上下文
|
||||
- 功能优化:优化聊天记录的存储结构,增加模型名称字段,支持存储更长的模型名称
|
||||
- Bug 修复:聊天应用绑定模型后无效,还是会轮询 API KEY,导致一会成功,一会请求失败。
|
||||
- 功能优化:如果管理后台没有启用会员充值菜单,移动端也不显示充值套餐功能
|
||||
|
||||
## v4.2.3
|
||||
|
||||
- 功能优化:增加模型分组与模型描述,采用卡片展示模式改进模型选择功能体验
|
||||
|
||||
@@ -100,6 +100,26 @@ func (s *AppServer) Run(db *gorm.DB) error {
|
||||
&model.UserLoginLog{},
|
||||
&model.DallJob{},
|
||||
)
|
||||
// 手动删除字段
|
||||
if db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.Order{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatItem{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.ChatItem{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatMessage{}, "deleted_at") {
|
||||
db.Migrator().DropColumn(&model.ChatMessage{}, "deleted_at")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.User{}, "chat_config") {
|
||||
db.Migrator().DropColumn(&model.User{}, "chat_config")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatModel{}, "category") {
|
||||
db.Migrator().DropColumn(&model.ChatModel{}, "category")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.ChatModel{}, "description") {
|
||||
db.Migrator().DropColumn(&model.ChatModel{}, "description")
|
||||
}
|
||||
|
||||
logger.Info("Database tables migrated successfully")
|
||||
|
||||
// 统计安装信息
|
||||
|
||||
@@ -52,17 +52,6 @@ type Delta struct {
|
||||
} `json:"function_call,omitempty"`
|
||||
}
|
||||
|
||||
// ChatSession 聊天会话对象
|
||||
type ChatSession struct {
|
||||
UserId uint `json:"user_id"`
|
||||
ClientIP string `json:"client_ip"` // 客户端 IP
|
||||
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
|
||||
Model ChatModel `json:"model"` // GPT 模型
|
||||
Start int64 `json:"start"` // 开始请求时间戳
|
||||
Tools []int `json:"tools"` // 工具函数列表
|
||||
Stream bool `json:"stream"` // 是否采用流式输出
|
||||
}
|
||||
|
||||
type ChatModel struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -162,10 +162,11 @@ type SystemConfig struct {
|
||||
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
|
||||
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
|
||||
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
|
||||
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
|
||||
Copyright string `json:"copyright"` // 版权信息
|
||||
DefaultNickname string `json:"default_nickname"` // 默认昵称
|
||||
ICP string `json:"icp"` // ICP 备案号
|
||||
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
|
||||
|
||||
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||
|
||||
@@ -16,7 +16,7 @@ type MKey interface {
|
||||
string | int | uint
|
||||
}
|
||||
type MValue interface {
|
||||
*WsClient | *ChatSession | context.CancelFunc | []any
|
||||
*WsClient | context.CancelFunc | []any
|
||||
}
|
||||
type LMap[K MKey, T MValue] struct {
|
||||
lock sync.RWMutex
|
||||
|
||||
@@ -209,20 +209,28 @@ func (h *ChatHandler) Messages(c *gin.Context) {
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
var messages = make([]vo.ChatMessage, 0)
|
||||
res := h.DB.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.HistoryMessage
|
||||
var v vo.ChatMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(item.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = item.Content
|
||||
}
|
||||
v.Content = content
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
Power int `json:"power"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Description string `json:"description"` //模型描述
|
||||
Category string `json:"category"` //模型类别
|
||||
Desc string `json:"desc"` //模型描述
|
||||
Tag string `json:"tag"` //模型标签
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId int `json:"key_id,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
@@ -66,8 +66,8 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||
item.Power = data.Power
|
||||
item.MaxTokens = data.MaxTokens
|
||||
item.MaxContext = data.MaxContext
|
||||
item.Description = data.Description
|
||||
item.Category = data.Category
|
||||
item.Desc = data.Desc
|
||||
item.Tag = data.Tag
|
||||
item.Temperature = data.Temperature
|
||||
item.KeyId = uint(data.KeyId)
|
||||
item.Type = data.Type
|
||||
@@ -100,12 +100,16 @@ func (h *ChatModelHandler) List(c *gin.Context) {
|
||||
session := h.DB.Session(&gorm.Session{})
|
||||
enable := h.GetBool(c, "enable")
|
||||
name := h.GetTrim(c, "name")
|
||||
modelType := h.GetTrim(c, "type")
|
||||
if enable {
|
||||
session = session.Where("enabled", enable)
|
||||
}
|
||||
if name != "" {
|
||||
session = session.Where("name LIKE ?", name+"%")
|
||||
}
|
||||
if modelType != "" {
|
||||
session = session.Where("type", modelType)
|
||||
}
|
||||
var items []model.ChatModel
|
||||
var cms = make([]vo.ChatModel, 0)
|
||||
res := session.Order("sort_num ASC").Find(&items)
|
||||
|
||||
@@ -106,8 +106,8 @@ func (h *RedeemHandler) Export(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 设置响应头,告诉浏览器这是一个附件,需要下载
|
||||
c.Header("Content-Disposition", "attachment; filename=output.csv")
|
||||
c.Header("Content-Type", "text/csv")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=output.csv")
|
||||
c.Header("Prompt-Type", "text/csv")
|
||||
|
||||
// 创建一个 CSV writer
|
||||
writer := csv.NewWriter(c.Writer)
|
||||
|
||||
@@ -178,6 +178,7 @@ func (h *UserHandler) Save(c *gin.Context) {
|
||||
Power: data.Power,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode(data.ChatRoles),
|
||||
ChatConfig: "{}",
|
||||
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||
}
|
||||
@@ -353,4 +354,4 @@ func (h *UserHandler) GenLoginLink(c *gin.Context) {
|
||||
}
|
||||
|
||||
resp.SUCCESS(c, tokenString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"geekai/store/vo"
|
||||
"geekai/utils"
|
||||
"geekai/utils/resp"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -36,13 +36,34 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ChatEventStart = "start"
|
||||
ChatEventEnd = "end"
|
||||
ChatEventError = "error"
|
||||
ChatEventMessageDelta = "message_delta"
|
||||
ChatEventTitle = "title"
|
||||
)
|
||||
|
||||
type ChatInput struct {
|
||||
UserId uint `json:"user_id"`
|
||||
RoleId uint `json:"role_id"`
|
||||
ModelId uint `json:"model_id"`
|
||||
ChatId string `json:"chat_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Tools []uint `json:"tools"`
|
||||
Stream bool `json:"stream"`
|
||||
Files []vo.File `json:"files"`
|
||||
ChatModel model.ChatModel `json:"chat_model,omitempty"`
|
||||
ChatRole model.ChatRole `json:"chat_role,omitempty"`
|
||||
LastMsgId uint `json:"last_msg_id,omitempty"` // 最后的消息ID,用于重新生成答案的时候过滤上下文
|
||||
}
|
||||
|
||||
type ChatHandler struct {
|
||||
BaseHandler
|
||||
redis *redis.Client
|
||||
uploadManager *oss.UploaderManager
|
||||
licenseService *service.LicenseService
|
||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||
ChatContexts *types.LMap[string, []any] // 聊天上下文 Map [chatId] => []Message
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
@@ -53,14 +74,74 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manag
|
||||
uploadManager: manager,
|
||||
licenseService: licenseService,
|
||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||
ChatContexts: types.NewLMap[string, []any](),
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
||||
// Chat 处理聊天请求
|
||||
func (h *ChatHandler) Chat(c *gin.Context) {
|
||||
var input ChatInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
resp.ERROR(c, types.InvalidArgs)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置SSE响应头
|
||||
c.Header("Prompt-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
|
||||
ctx, cancel := context.WithCancel(c.Request.Context())
|
||||
defer cancel()
|
||||
|
||||
// 使用旧的聊天数据覆盖模型和角色ID
|
||||
var chat model.ChatItem
|
||||
h.DB.Where("chat_id", input.ChatId).First(&chat)
|
||||
if chat.Id > 0 {
|
||||
input.ModelId = chat.ModelId
|
||||
input.RoleId = chat.RoleId
|
||||
}
|
||||
|
||||
// 验证聊天角色
|
||||
var chatRole model.ChatRole
|
||||
err := h.DB.First(&chatRole, input.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
pushMessage(c, ChatEventError, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!")
|
||||
return
|
||||
}
|
||||
input.ChatRole = chatRole
|
||||
|
||||
// 获取模型信息
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", input.ModelId).First(&chatModel).Error
|
||||
if err != nil || !chatModel.Enabled {
|
||||
pushMessage(c, ChatEventError, "当前AI模型暂未启用,请更换模型后再发起对话!")
|
||||
return
|
||||
}
|
||||
input.ChatModel = chatModel
|
||||
|
||||
// 发送消息
|
||||
err = h.sendMessage(ctx, input, c)
|
||||
if err != nil {
|
||||
pushMessage(c, ChatEventError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pushMessage(c, ChatEventEnd, "对话完成")
|
||||
}
|
||||
|
||||
func pushMessage(c *gin.Context, msgType string, content interface{}) {
|
||||
c.SSEvent("message", map[string]interface{}{
|
||||
"type": msgType,
|
||||
"body": content,
|
||||
})
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.Context) error {
|
||||
var user model.User
|
||||
res := h.DB.Model(&model.User{}).First(&user, session.UserId)
|
||||
res := h.DB.Model(&model.User{}).First(&user, input.UserId)
|
||||
if res.Error != nil {
|
||||
return errors.New("未授权用户,您正在进行非法操作!")
|
||||
}
|
||||
@@ -71,12 +152,12 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
return errors.New("User 对象转换失败," + err.Error())
|
||||
}
|
||||
|
||||
if userVo.Status == false {
|
||||
if !userVo.Status {
|
||||
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||
}
|
||||
|
||||
if userVo.Power < session.Model.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power)
|
||||
if userVo.Power < input.ChatModel.Power {
|
||||
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, input.ChatModel.Power)
|
||||
}
|
||||
|
||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||
@@ -84,30 +165,29 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
}
|
||||
|
||||
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
|
||||
promptTokens, _ := utils.CalcTokens(prompt, session.Model.Value)
|
||||
if promptTokens > session.Model.MaxContext {
|
||||
promptTokens, _ := utils.CalcTokens(input.Prompt, input.ChatModel.Value)
|
||||
if promptTokens > input.ChatModel.MaxContext {
|
||||
|
||||
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
|
||||
}
|
||||
|
||||
var req = types.ApiRequest{
|
||||
Model: session.Model.Value,
|
||||
Stream: session.Stream,
|
||||
Temperature: session.Model.Temperature,
|
||||
Model: input.ChatModel.Value,
|
||||
Stream: input.Stream,
|
||||
Temperature: input.ChatModel.Temperature,
|
||||
}
|
||||
// 兼容 OpenAI 模型
|
||||
if strings.HasPrefix(session.Model.Value, "o1-") ||
|
||||
strings.HasPrefix(session.Model.Value, "o3-") ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt") {
|
||||
req.MaxCompletionTokens = session.Model.MaxTokens
|
||||
session.Start = time.Now().Unix()
|
||||
if strings.HasPrefix(input.ChatModel.Value, "o1-") ||
|
||||
strings.HasPrefix(input.ChatModel.Value, "o3-") ||
|
||||
strings.HasPrefix(input.ChatModel.Value, "gpt") {
|
||||
req.MaxCompletionTokens = input.ChatModel.MaxTokens
|
||||
} else {
|
||||
req.MaxTokens = session.Model.MaxTokens
|
||||
req.MaxTokens = input.ChatModel.MaxTokens
|
||||
}
|
||||
|
||||
if len(session.Tools) > 0 && !strings.HasPrefix(session.Model.Value, "o1-") {
|
||||
if len(input.Tools) > 0 && !strings.HasPrefix(input.ChatModel.Value, "o1-") {
|
||||
var items []model.Function
|
||||
res = h.DB.Where("enabled", true).Where("id IN ?", session.Tools).Find(&items)
|
||||
res = h.DB.Where("enabled", true).Where("id IN ?", input.Tools).Find(&items)
|
||||
if res.Error == nil {
|
||||
var tools = make([]types.Tool, 0)
|
||||
for _, v := range items {
|
||||
@@ -138,25 +218,27 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
}
|
||||
|
||||
// 加载聊天上下文
|
||||
chatCtx := make([]interface{}, 0)
|
||||
messages := make([]interface{}, 0)
|
||||
chatCtx := make([]any, 0)
|
||||
messages := make([]any, 0)
|
||||
if h.App.SysConfig.EnableContext {
|
||||
if h.ChatContexts.Has(session.ChatId) {
|
||||
messages = h.ChatContexts.Get(session.ChatId)
|
||||
} else {
|
||||
_ = utils.JsonDecode(role.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
|
||||
if res.Error == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
_ = utils.JsonDecode(input.ChatRole.Context, &messages)
|
||||
if h.App.SysConfig.ContextDeep > 0 {
|
||||
var historyMessages []model.ChatMessage
|
||||
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
|
||||
if input.LastMsgId > 0 { // 重新生成逻辑
|
||||
dbSession = dbSession.Where("id < ?", input.LastMsgId)
|
||||
// 删除对应的聊天记录
|
||||
h.DB.Debug().Where("chat_id", input.ChatId).Where("id >= ?", input.LastMsgId).Delete(&model.ChatMessage{})
|
||||
}
|
||||
err = dbSession.Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages).Error
|
||||
if err == nil {
|
||||
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||
msg := historyMessages[i]
|
||||
ms := types.Message{Role: "user", Content: msg.Content}
|
||||
if msg.Type == types.ReplyMsg {
|
||||
ms.Role = "assistant"
|
||||
}
|
||||
chatCtx = append(chatCtx, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +253,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
v := messages[i]
|
||||
tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model)
|
||||
// 上下文 token 超出了模型的最大上下文长度
|
||||
if tokens+tks >= session.Model.MaxContext {
|
||||
if tokens+tks >= input.ChatModel.MaxContext {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -183,78 +265,106 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
|
||||
tokens += tks
|
||||
chatCtx = append(chatCtx, v)
|
||||
}
|
||||
|
||||
logger.Debugf("聊天上下文:%+v", chatCtx)
|
||||
}
|
||||
reqMgs := make([]interface{}, 0)
|
||||
reqMgs := make([]any, 0)
|
||||
|
||||
for i := len(chatCtx) - 1; i >= 0; i-- {
|
||||
reqMgs = append(reqMgs, chatCtx[i])
|
||||
}
|
||||
|
||||
fullPrompt := prompt
|
||||
text := prompt
|
||||
// extract files in prompt
|
||||
files := utils.ExtractFileURLs(prompt)
|
||||
logger.Debugf("detected FILES: %+v", files)
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
if len(files) > 0 && !(session.Model.Value == "gpt-4-all" ||
|
||||
strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") ||
|
||||
strings.HasSuffix(session.Model.Value, "claude-3")) {
|
||||
contents := make([]string, 0)
|
||||
var file model.File
|
||||
for _, v := range files {
|
||||
h.DB.Where("url = ?", v).First(&file)
|
||||
content, err := utils.ReadFileContent(v, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
} else {
|
||||
contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
}
|
||||
if len(contents) > 0 {
|
||||
fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
|
||||
}
|
||||
|
||||
tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
|
||||
if tokens > session.Model.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
}
|
||||
logger.Debug("最终Prompt:", fullPrompt)
|
||||
|
||||
// extract images from prompt
|
||||
imgURLs := utils.ExtractImgURLs(prompt)
|
||||
logger.Debugf("detected IMG: %+v", imgURLs)
|
||||
var content interface{}
|
||||
if len(imgURLs) > 0 {
|
||||
data := make([]interface{}, 0)
|
||||
for _, v := range imgURLs {
|
||||
text = strings.Replace(text, v, "", 1)
|
||||
data = append(data, gin.H{
|
||||
fileContents := make([]string, 0) // 文件内容
|
||||
var finalPrompt = input.Prompt
|
||||
imgList := make([]any, 0)
|
||||
for _, file := range input.Files {
|
||||
logger.Debugf("detected file: %+v", file.URL)
|
||||
// 处理图片
|
||||
if isImageURL(file.URL) {
|
||||
imgList = append(imgList, gin.H{
|
||||
"type": "image_url",
|
||||
"image_url": gin.H{
|
||||
"url": v,
|
||||
"url": file.URL,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// 如果不是逆向模型,则提取文件内容
|
||||
modelValue := input.ChatModel.Value
|
||||
if !(strings.Contains(modelValue, "-all") || strings.HasPrefix(modelValue, "gpt-4-gizmo") || strings.HasPrefix(modelValue, "claude")) {
|
||||
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
|
||||
if err != nil {
|
||||
logger.Error("error with read file: ", err)
|
||||
continue
|
||||
} else {
|
||||
fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
|
||||
}
|
||||
}
|
||||
}
|
||||
data = append(data, gin.H{
|
||||
"type": "text",
|
||||
"text": strings.TrimSpace(text),
|
||||
})
|
||||
content = data
|
||||
} else {
|
||||
content = fullPrompt
|
||||
}
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
})
|
||||
|
||||
logger.Debugf("%+v", req.Messages)
|
||||
if len(fileContents) > 0 {
|
||||
finalPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML):\n\n %s\n\n 问题:%s", strings.Join(fileContents, "\n"), input.Prompt)
|
||||
tokens, _ := utils.CalcTokens(finalPrompt, req.Model)
|
||||
if tokens > input.ChatModel.MaxContext {
|
||||
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
|
||||
}
|
||||
} else {
|
||||
finalPrompt = input.Prompt
|
||||
}
|
||||
|
||||
return h.sendOpenAiMessage(req, userVo, ctx, session, role, prompt, ws)
|
||||
if len(imgList) > 0 {
|
||||
imgList = append(imgList, map[string]interface{}{
|
||||
"type": "text",
|
||||
"text": input.Prompt,
|
||||
})
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": imgList,
|
||||
})
|
||||
} else {
|
||||
req.Messages = append(reqMgs, map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": finalPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
return h.sendOpenAiMessage(req, userVo, ctx, input, c)
|
||||
}
|
||||
|
||||
// 判断一个 URL 是否图片链接
|
||||
func isImageURL(url string) bool {
|
||||
// 检查是否是有效的URL
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查文件扩展名
|
||||
ext := strings.ToLower(path.Ext(url))
|
||||
validImageExts := map[string]bool{
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
".gif": true,
|
||||
".bmp": true,
|
||||
".webp": true,
|
||||
".svg": true,
|
||||
".ico": true,
|
||||
}
|
||||
|
||||
if !validImageExts[ext] {
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送HEAD请求检查Content-Type
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
resp, err := client.Head(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
return strings.HasPrefix(contentType, "image/")
|
||||
}
|
||||
|
||||
// Tokens 统计 token 数量
|
||||
@@ -323,15 +433,14 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||
|
||||
// 发送请求到 OpenAI 服务器
|
||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input ChatInput, apiKey *model.ApiKey) (*http.Response, error) {
|
||||
// if the chat model bind a KEY, use it directly
|
||||
if session.Model.KeyId > 0 {
|
||||
h.DB.Where("id", session.Model.KeyId).Find(apiKey)
|
||||
}
|
||||
// use the last unused key
|
||||
if apiKey.Id == 0 {
|
||||
if input.ChatModel.KeyId > 0 {
|
||||
h.DB.Where("id", input.ChatModel.KeyId).Find(apiKey)
|
||||
} else { // use the last unused key
|
||||
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
|
||||
}
|
||||
|
||||
if apiKey.Id == 0 {
|
||||
return nil, errors.New("no available key, please import key")
|
||||
}
|
||||
@@ -381,16 +490,16 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
|
||||
}
|
||||
|
||||
// 扣减用户算力
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
|
||||
func (h *ChatHandler) subUserPower(userVo vo.User, input ChatInput, promptTokens int, replyTokens int) {
|
||||
power := 1
|
||||
if session.Model.Power > 0 {
|
||||
power = session.Model.Power
|
||||
if input.ChatModel.Power > 0 {
|
||||
power = input.ChatModel.Power
|
||||
}
|
||||
|
||||
err := h.userService.DecreasePower(userVo.Id, power, model.PowerLog{
|
||||
Type: types.PowerConsume,
|
||||
Model: session.Model.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens),
|
||||
Model: input.ChatModel.Value,
|
||||
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", input.ChatModel.Name, promptTokens, replyTokens),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
@@ -401,19 +510,11 @@ func (h *ChatHandler) saveChatHistory(
|
||||
req types.ApiRequest,
|
||||
usage Usage,
|
||||
message types.Message,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
input ChatInput,
|
||||
userVo vo.User,
|
||||
promptCreatedAt time.Time,
|
||||
replyCreatedAt time.Time) {
|
||||
|
||||
// 更新上下文消息
|
||||
if h.App.SysConfig.EnableContext {
|
||||
chatCtx := req.Messages // 提问消息
|
||||
chatCtx = append(chatCtx, message) // 回复消息
|
||||
h.ChatContexts.Put(session.ChatId, chatCtx)
|
||||
}
|
||||
|
||||
// 追加聊天记录
|
||||
// for prompt
|
||||
var promptTokens, replyTokens, totalTokens int
|
||||
@@ -424,12 +525,15 @@ func (h *ChatHandler) saveChatHistory(
|
||||
}
|
||||
|
||||
historyUserMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: template.HTMLEscapeString(usage.Prompt),
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
RoleId: input.RoleId,
|
||||
Type: types.PromptMsg,
|
||||
Icon: userVo.Avatar,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: usage.Prompt,
|
||||
Files: input.Files,
|
||||
}),
|
||||
Tokens: promptTokens,
|
||||
TotalTokens: promptTokens,
|
||||
UseContext: true,
|
||||
@@ -452,12 +556,15 @@ func (h *ChatHandler) saveChatHistory(
|
||||
totalTokens = replyTokens + getTotalTokens(req)
|
||||
}
|
||||
historyReplyMsg := model.ChatMessage{
|
||||
UserId: userVo.Id,
|
||||
ChatId: session.ChatId,
|
||||
RoleId: role.Id,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: role.Icon,
|
||||
Content: usage.Content,
|
||||
UserId: userVo.Id,
|
||||
ChatId: input.ChatId,
|
||||
RoleId: input.RoleId,
|
||||
Type: types.ReplyMsg,
|
||||
Icon: input.ChatRole.Icon,
|
||||
Content: utils.JsonEncode(vo.MsgContent{
|
||||
Text: message.Content,
|
||||
Files: input.Files,
|
||||
}),
|
||||
Tokens: replyTokens,
|
||||
TotalTokens: totalTokens,
|
||||
UseContext: true,
|
||||
@@ -471,17 +578,17 @@ func (h *ChatHandler) saveChatHistory(
|
||||
}
|
||||
|
||||
// 更新用户算力
|
||||
if session.Model.Power > 0 {
|
||||
h.subUserPower(userVo, session, promptTokens, replyTokens)
|
||||
if input.ChatModel.Power > 0 {
|
||||
h.subUserPower(userVo, input, promptTokens, replyTokens)
|
||||
}
|
||||
// 保存当前会话
|
||||
var chatItem model.ChatItem
|
||||
err = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem).Error
|
||||
err = h.DB.Where("chat_id = ?", input.ChatId).First(&chatItem).Error
|
||||
if err != nil {
|
||||
chatItem.ChatId = session.ChatId
|
||||
chatItem.ChatId = input.ChatId
|
||||
chatItem.UserId = userVo.Id
|
||||
chatItem.RoleId = role.Id
|
||||
chatItem.ModelId = session.Model.Id
|
||||
chatItem.RoleId = input.RoleId
|
||||
chatItem.ModelId = input.ModelId
|
||||
if utf8.RuneCountInString(usage.Prompt) > 30 {
|
||||
chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..."
|
||||
} else {
|
||||
@@ -495,7 +602,7 @@ func (h *ChatHandler) saveChatHistory(
|
||||
}
|
||||
}
|
||||
|
||||
// 文本生成语音
|
||||
// TextToSpeech 文本生成语音
|
||||
func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
var data struct {
|
||||
ModelId int `json:"model_id"`
|
||||
@@ -509,13 +616,19 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
textHash := utils.Sha256(fmt.Sprintf("%d/%s", data.ModelId, data.Text))
|
||||
audioFile := fmt.Sprintf("%s/audio", h.App.Config.StaticDir)
|
||||
if _, err := os.Stat(audioFile); err != nil {
|
||||
os.MkdirAll(audioFile, 0755)
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(audioFile, 0755); err != nil {
|
||||
resp.ERROR(c, err.Error())
|
||||
return
|
||||
}
|
||||
audioFile = fmt.Sprintf("%s/%s.mp3", audioFile, textHash)
|
||||
if _, err := os.Stat(audioFile); err == nil {
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "audio/mpeg")
|
||||
c.Header("Content-Disposition", "attachment; filename=speech.mp3")
|
||||
c.Header("Prompt-Type", "audio/mpeg")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
|
||||
c.File(audioFile)
|
||||
return
|
||||
}
|
||||
@@ -579,9 +692,230 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "audio/mpeg")
|
||||
c.Header("Content-Disposition", "attachment; filename=speech.mp3")
|
||||
c.Header("Prompt-Type", "audio/mpeg")
|
||||
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
|
||||
|
||||
// 直接写入完整的音频数据到响应
|
||||
c.Writer.Write(audioBytes)
|
||||
_, err = c.Writer.Write(audioBytes)
|
||||
if err != nil {
|
||||
logger.Error("写入音频数据到响应失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// // OPenAI 消息发送实现
|
||||
// func (h *ChatHandler) sendOpenAiMessage(
|
||||
// req types.ApiRequest,
|
||||
// userVo vo.User,
|
||||
// ctx context.Context,
|
||||
// session *types.ChatSession,
|
||||
// role model.ChatRole,
|
||||
// prompt string,
|
||||
// c *gin.Context) error {
|
||||
// promptCreatedAt := time.Now() // 记录提问时间
|
||||
// start := time.Now()
|
||||
// var apiKey = model.ApiKey{}
|
||||
// response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
// logger.Info("HTTP请求完成,耗时:", time.Since(start))
|
||||
// if err != nil {
|
||||
// if strings.Contains(err.Error(), "context canceled") {
|
||||
// return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
// } else if strings.Contains(err.Error(), "no available key") {
|
||||
// return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
// }
|
||||
// return err
|
||||
// } else {
|
||||
// defer response.Body.Close()
|
||||
// }
|
||||
|
||||
// if response.StatusCode != 200 {
|
||||
// body, _ := io.ReadAll(response.Body)
|
||||
// return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
// contentType := response.Header.Get("Prompt-Type")
|
||||
// if strings.Contains(contentType, "text/event-stream") {
|
||||
// replyCreatedAt := time.Now() // 记录回复时间
|
||||
// // 循环读取 Chunk 消息
|
||||
// var message = types.Message{Role: "assistant"}
|
||||
// var contents = make([]string, 0)
|
||||
// var function model.Function
|
||||
// var toolCall = false
|
||||
// var arguments = make([]string, 0)
|
||||
// var reasoning = false
|
||||
|
||||
// pushMessage(c, ChatEventStart, "开始响应")
|
||||
// scanner := bufio.NewScanner(response.Body)
|
||||
// for scanner.Scan() {
|
||||
// line := scanner.Text()
|
||||
// if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||
// continue
|
||||
// }
|
||||
// var responseBody = types.ApiResponse{}
|
||||
// err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||
// if err != nil { // 数据解析出错
|
||||
// return errors.New(line)
|
||||
// }
|
||||
// if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
|
||||
// continue
|
||||
// }
|
||||
// if responseBody.Choices[0].Delta.Prompt == nil &&
|
||||
// responseBody.Choices[0].Delta.ToolCalls == nil &&
|
||||
// responseBody.Choices[0].Delta.ReasoningContent == "" {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
// pushMessage(c, ChatEventError, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
// break
|
||||
// }
|
||||
|
||||
// var tool types.ToolCall
|
||||
// if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
|
||||
// tool = responseBody.Choices[0].Delta.ToolCalls[0]
|
||||
// if toolCall && tool.Function.Name == "" {
|
||||
// arguments = append(arguments, tool.Function.Arguments)
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 兼容 Function Call
|
||||
// fun := responseBody.Choices[0].Delta.FunctionCall
|
||||
// if fun.Name != "" {
|
||||
// tool = *new(types.ToolCall)
|
||||
// tool.Function.Name = fun.Name
|
||||
// } else if toolCall {
|
||||
// arguments = append(arguments, fun.Arguments)
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if !utils.IsEmptyValue(tool) {
|
||||
// res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
|
||||
// if res.Error == nil {
|
||||
// toolCall = true
|
||||
// callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": callMsg,
|
||||
// })
|
||||
// contents = append(contents, callMsg)
|
||||
// }
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if responseBody.Choices[0].FinishReason == "tool_calls" ||
|
||||
// responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
||||
// break
|
||||
// }
|
||||
|
||||
// // output stopped
|
||||
// if responseBody.Choices[0].FinishReason != "" {
|
||||
// break // 输出完成或者输出中断了
|
||||
// } else { // 正常输出结果
|
||||
// // 兼容思考过程
|
||||
// if responseBody.Choices[0].Delta.ReasoningContent != "" {
|
||||
// reasoningContent := responseBody.Choices[0].Delta.ReasoningContent
|
||||
// if !reasoning {
|
||||
// reasoningContent = fmt.Sprintf("<think>%s", reasoningContent)
|
||||
// reasoning = true
|
||||
// }
|
||||
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": reasoningContent,
|
||||
// })
|
||||
// contents = append(contents, reasoningContent)
|
||||
// } else if responseBody.Choices[0].Delta.Prompt != "" {
|
||||
// finalContent := responseBody.Choices[0].Delta.Prompt
|
||||
// if reasoning {
|
||||
// finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Prompt)
|
||||
// reasoning = false
|
||||
// }
|
||||
// contents = append(contents, utils.InterfaceToString(finalContent))
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": finalContent,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// } // end for
|
||||
|
||||
// if err := scanner.Err(); err != nil {
|
||||
// if strings.Contains(err.Error(), "context canceled") {
|
||||
// logger.Info("用户取消了请求:", prompt)
|
||||
// } else {
|
||||
// logger.Error("信息读取出错:", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// if toolCall { // 调用函数完成任务
|
||||
// params := make(map[string]any)
|
||||
// _ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
||||
// logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
|
||||
// params["user_id"] = userVo.Id
|
||||
// var apiRes types.BizVo
|
||||
// r, err := req2.C().R().SetHeader("Body-Type", "application/json").
|
||||
// SetHeader("Authorization", function.Token).
|
||||
// SetBody(params).Post(function.Action)
|
||||
// errMsg := ""
|
||||
// if err != nil {
|
||||
// errMsg = err.Error()
|
||||
// } else {
|
||||
// all, _ := io.ReadAll(r.Body)
|
||||
// err = json.Unmarshal(all, &apiRes)
|
||||
// if err != nil {
|
||||
// errMsg = err.Error()
|
||||
// } else if apiRes.Code != types.Success {
|
||||
// errMsg = apiRes.Message
|
||||
// }
|
||||
// }
|
||||
|
||||
// if errMsg != "" {
|
||||
// errMsg = "调用函数工具出错:" + errMsg
|
||||
// contents = append(contents, errMsg)
|
||||
// } else {
|
||||
// errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
// contents = append(contents, errMsg)
|
||||
// }
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": errMsg,
|
||||
// })
|
||||
// }
|
||||
|
||||
// // 消息发送成功
|
||||
// if len(contents) > 0 {
|
||||
// usage := Usage{
|
||||
// Prompt: prompt,
|
||||
// Prompt: strings.Join(contents, ""),
|
||||
// PromptTokens: 0,
|
||||
// CompletionTokens: 0,
|
||||
// TotalTokens: 0,
|
||||
// }
|
||||
// message.Prompt = usage.Prompt
|
||||
// h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
// }
|
||||
// } else {
|
||||
// var respVo OpenAIResVo
|
||||
// body, err := io.ReadAll(response.Body)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("读取响应失败:%v", body)
|
||||
// }
|
||||
// err = json.Unmarshal(body, &respVo)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("解析响应失败:%v", body)
|
||||
// }
|
||||
// content := respVo.Choices[0].Message.Prompt
|
||||
// if strings.HasPrefix(req.Model, "o1-") {
|
||||
// content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Prompt)
|
||||
// }
|
||||
// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
|
||||
// "type": "text",
|
||||
// "content": content,
|
||||
// })
|
||||
// respVo.Usage.Prompt = prompt
|
||||
// respVo.Usage.Prompt = content
|
||||
// h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
@@ -104,8 +104,6 @@ func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
var chatIds = make([]string, 0)
|
||||
for _, chat := range chats {
|
||||
chatIds = append(chatIds, chat.ChatId)
|
||||
// 清空会话上下文
|
||||
h.ChatContexts.Delete(chat.ChatId)
|
||||
}
|
||||
err = h.DB.Transaction(func(tx *gorm.DB) error {
|
||||
res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
||||
@@ -133,20 +131,28 @@ func (h *ChatHandler) Clear(c *gin.Context) {
|
||||
func (h *ChatHandler) History(c *gin.Context) {
|
||||
chatId := c.Query("chat_id") // 会话 ID
|
||||
var items []model.ChatMessage
|
||||
var messages = make([]vo.HistoryMessage, 0)
|
||||
var messages = make([]vo.ChatMessage, 0)
|
||||
res := h.DB.Where("chat_id = ?", chatId).Find(&items)
|
||||
if res.Error != nil {
|
||||
resp.ERROR(c, "No history message")
|
||||
return
|
||||
} else {
|
||||
for _, item := range items {
|
||||
var v vo.HistoryMessage
|
||||
var v vo.ChatMessage
|
||||
err := utils.CopyObject(item, &v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// 解析内容
|
||||
var content vo.MsgContent
|
||||
err = utils.JsonDecode(item.Content, &content)
|
||||
if err != nil {
|
||||
content.Text = item.Content
|
||||
}
|
||||
v.Content = content
|
||||
v.CreatedAt = item.CreatedAt.Unix()
|
||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||
if err == nil {
|
||||
messages = append(messages, v)
|
||||
}
|
||||
messages = append(messages, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +185,6 @@ func (h *ChatHandler) Remove(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
||||
|
||||
// 清空会话上下文
|
||||
h.ChatContexts.Delete(chatId)
|
||||
resp.SUCCESS(c, types.OkMsg)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
req2 "github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
@@ -55,18 +56,16 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
req types.ApiRequest,
|
||||
userVo vo.User,
|
||||
ctx context.Context,
|
||||
session *types.ChatSession,
|
||||
role model.ChatRole,
|
||||
prompt string,
|
||||
ws *types.WsClient) error {
|
||||
input ChatInput,
|
||||
c *gin.Context) error {
|
||||
promptCreatedAt := time.Now() // 记录提问时间
|
||||
start := time.Now()
|
||||
var apiKey = model.ApiKey{}
|
||||
response, err := h.doRequest(ctx, req, session, &apiKey)
|
||||
response, err := h.doRequest(ctx, req, input, &apiKey)
|
||||
logger.Info("HTTP请求完成,耗时:", time.Since(start))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
return fmt.Errorf("用户取消了请求:%s", prompt)
|
||||
return fmt.Errorf("用户取消了请求:%s", input.Prompt)
|
||||
} else if strings.Contains(err.Error(), "no available key") {
|
||||
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||
}
|
||||
@@ -112,7 +111,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
}
|
||||
|
||||
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
|
||||
utils.SendChunkMsg(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
pushMessage(c, "text", "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
|
||||
break
|
||||
}
|
||||
|
||||
@@ -140,7 +139,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
if res.Error == nil {
|
||||
toolCall = true
|
||||
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
|
||||
utils.SendChunkMsg(ws, callMsg)
|
||||
pushMessage(c, "text", callMsg)
|
||||
contents = append(contents, callMsg)
|
||||
}
|
||||
continue
|
||||
@@ -163,7 +162,7 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
reasoning = true
|
||||
}
|
||||
|
||||
utils.SendChunkMsg(ws, reasoningContent)
|
||||
pushMessage(c, "text", reasoningContent)
|
||||
contents = append(contents, reasoningContent)
|
||||
} else if responseBody.Choices[0].Delta.Content != "" {
|
||||
finalContent := responseBody.Choices[0].Delta.Content
|
||||
@@ -172,14 +171,14 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
reasoning = false
|
||||
}
|
||||
contents = append(contents, utils.InterfaceToString(finalContent))
|
||||
utils.SendChunkMsg(ws, finalContent)
|
||||
pushMessage(c, "text", finalContent)
|
||||
}
|
||||
}
|
||||
} // end for
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
if strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Info("用户取消了请求:", prompt)
|
||||
logger.Info("用户取消了请求:", input.Prompt)
|
||||
} else {
|
||||
logger.Error("信息读取出错:", err)
|
||||
}
|
||||
@@ -214,20 +213,20 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
errMsg = utils.InterfaceToString(apiRes.Data)
|
||||
contents = append(contents, errMsg)
|
||||
}
|
||||
utils.SendChunkMsg(ws, errMsg)
|
||||
pushMessage(c, "text", errMsg)
|
||||
}
|
||||
|
||||
// 消息发送成功
|
||||
if len(contents) > 0 {
|
||||
usage := Usage{
|
||||
Prompt: prompt,
|
||||
Prompt: input.Prompt,
|
||||
Content: strings.Join(contents, ""),
|
||||
PromptTokens: 0,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: 0,
|
||||
}
|
||||
message.Content = usage.Content
|
||||
h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
|
||||
h.saveChatHistory(req, usage, message, input, userVo, promptCreatedAt, replyCreatedAt)
|
||||
}
|
||||
} else { // 非流式输出
|
||||
var respVo OpenAIResVo
|
||||
@@ -240,13 +239,10 @@ func (h *ChatHandler) sendOpenAiMessage(
|
||||
return fmt.Errorf("解析响应失败:%v", body)
|
||||
}
|
||||
content := respVo.Choices[0].Message.Content
|
||||
if strings.HasPrefix(req.Model, "o1-") {
|
||||
content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content)
|
||||
}
|
||||
utils.SendChunkMsg(ws, content)
|
||||
respVo.Usage.Prompt = prompt
|
||||
pushMessage(c, "text", content)
|
||||
respVo.Usage.Prompt = input.Prompt
|
||||
respVo.Usage.Content = content
|
||||
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
|
||||
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, input, userVo, promptCreatedAt, time.Now())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -137,13 +137,15 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
|
||||
salt := utils.RandString(8)
|
||||
user := model.User{
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
Username: data.Username,
|
||||
Password: utils.GenPassword(data.Password, salt),
|
||||
Avatar: "/images/avatar/user.png",
|
||||
Salt: salt,
|
||||
Status: true,
|
||||
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||
ChatConfig: "{}",
|
||||
ChatModels: "{}",
|
||||
Power: h.App.SysConfig.InitPower,
|
||||
}
|
||||
|
||||
// check if the username is existing
|
||||
@@ -170,10 +172,15 @@ func (h *UserHandler) Register(c *gin.Context) {
|
||||
if data.InviteCode != "" {
|
||||
user.Power += h.App.SysConfig.InvitePower
|
||||
}
|
||||
|
||||
if h.licenseService.GetLicense().Configs.DeCopy {
|
||||
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
|
||||
} else {
|
||||
user.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
|
||||
defaultNickname := h.App.SysConfig.DefaultNickname
|
||||
if defaultNickname == "" {
|
||||
defaultNickname = "极客学长"
|
||||
}
|
||||
user.Nickname = fmt.Sprintf("%s@%d", defaultNickname, utils.RandomNumber(6))
|
||||
}
|
||||
|
||||
tx := h.DB.Begin()
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
package handler
|
||||
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||
// * Use of this source code is governed by a Apache-2.0 license
|
||||
// * that can be found in the LICENSE file.
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geekai/core"
|
||||
"geekai/core/types"
|
||||
"geekai/service"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Websocket 连接处理 handler
|
||||
|
||||
type WebsocketHandler struct {
|
||||
BaseHandler
|
||||
wsService *service.WebsocketService
|
||||
chatHandler *ChatHandler
|
||||
}
|
||||
|
||||
func NewWebsocketHandler(app *core.AppServer, s *service.WebsocketService, db *gorm.DB, chatHandler *ChatHandler) *WebsocketHandler {
|
||||
return &WebsocketHandler{
|
||||
BaseHandler: BaseHandler{App: app, DB: db},
|
||||
chatHandler: chatHandler,
|
||||
wsService: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebsocketHandler) Client(c *gin.Context) {
|
||||
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
|
||||
ws, err := (&websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
Subprotocols: strings.Split(clientProtocols, ","),
|
||||
}).Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
clientId := c.Query("client_id")
|
||||
client := types.NewWsClient(ws, clientId)
|
||||
userId := h.GetLoginUserId(c)
|
||||
if userId == 0 {
|
||||
_ = client.Send([]byte("Invalid user_id"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
|
||||
_ = client.Send([]byte("Invalid user_id"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
h.wsService.Clients.Put(clientId, client)
|
||||
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
|
||||
go func() {
|
||||
for {
|
||||
_, msg, err := client.Receive()
|
||||
if err != nil {
|
||||
logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
|
||||
client.Close()
|
||||
h.wsService.Clients.Delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
var message types.InputMessage
|
||||
err = utils.JsonDecode(string(msg), &message)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debugf("Receive a message:%+v", message)
|
||||
if message.Type == types.MsgTypePing {
|
||||
utils.SendChannelMsg(client, types.ChPing, "pong")
|
||||
continue
|
||||
}
|
||||
|
||||
// 当前只处理聊天消息,其他消息全部丢弃
|
||||
var chatMessage types.ChatMessage
|
||||
err = utils.JsonDecode(utils.JsonEncode(message.Body), &chatMessage)
|
||||
if err != nil || message.Channel != types.ChChat {
|
||||
logger.Warnf("invalid message body:%+v", message.Body)
|
||||
continue
|
||||
}
|
||||
var chatRole model.ChatRole
|
||||
err = h.DB.First(&chatRole, chatMessage.RoleId).Error
|
||||
if err != nil || !chatRole.Enable {
|
||||
utils.SendAndFlush(client, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!!!")
|
||||
continue
|
||||
}
|
||||
// if the role bind a model_id, use role's bind model_id
|
||||
if chatRole.ModelId > 0 {
|
||||
chatMessage.RoleId = int(chatRole.ModelId)
|
||||
}
|
||||
// get model info
|
||||
var chatModel model.ChatModel
|
||||
err = h.DB.Where("id", chatMessage.ModelId).First(&chatModel).Error
|
||||
if err != nil || chatModel.Enabled == false {
|
||||
utils.SendAndFlush(client, "当前AI模型暂未启用,请更换模型后再发起对话!!!")
|
||||
continue
|
||||
}
|
||||
|
||||
session := &types.ChatSession{
|
||||
ClientIP: c.ClientIP(),
|
||||
UserId: userId,
|
||||
}
|
||||
|
||||
// use old chat data override the chat model and role ID
|
||||
var chat model.ChatItem
|
||||
h.DB.Where("chat_id", chatMessage.ChatId).First(&chat)
|
||||
if chat.Id > 0 {
|
||||
chatModel.Id = chat.ModelId
|
||||
chatMessage.RoleId = int(chat.RoleId)
|
||||
}
|
||||
|
||||
session.ChatId = chatMessage.ChatId
|
||||
session.Tools = chatMessage.Tools
|
||||
session.Stream = chatMessage.Stream
|
||||
// 复制模型数据
|
||||
err = utils.CopyObject(chatModel, &session.Model)
|
||||
if err != nil {
|
||||
logger.Error(err, chatModel)
|
||||
}
|
||||
session.Model.Id = chatModel.Id
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h.chatHandler.ReqCancelFunc.Put(clientId, cancel)
|
||||
err = h.chatHandler.sendMessage(ctx, session, chatRole, chatMessage.Content, client)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
utils.SendAndFlush(client, err.Error())
|
||||
} else {
|
||||
utils.SendMsg(client, types.ReplyMessage{Channel: types.ChChat, Type: types.MsgTypeEnd})
|
||||
logger.Infof("回答完毕: %v", message.Body)
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -248,6 +248,7 @@ func main() {
|
||||
}),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
|
||||
group := s.Engine.Group("/api/chat/")
|
||||
group.Any("message", h.Chat)
|
||||
group.GET("list", h.List)
|
||||
group.GET("detail", h.Detail)
|
||||
group.POST("update", h.Update)
|
||||
@@ -519,11 +520,6 @@ func main() {
|
||||
group := s.Engine.Group("/api/test")
|
||||
group.Any("sse", h.PostTest, h.SseTest)
|
||||
}),
|
||||
fx.Provide(service.NewWebsocketService),
|
||||
fx.Provide(handler.NewWebsocketHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.WebsocketHandler) {
|
||||
s.Engine.Any("/api/ws", h.Client)
|
||||
}),
|
||||
fx.Provide(handler.NewPromptHandler),
|
||||
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
|
||||
group := s.Engine.Group("/api/prompt")
|
||||
|
||||
@@ -24,15 +24,15 @@ func NewService() (*Service, error) {
|
||||
// 启动浏览器
|
||||
path, _ := launcher.LookPath()
|
||||
u := launcher.New().Bin(path).
|
||||
Headless(true). // 无头模式
|
||||
Set("disable-web-security", ""). // 禁用网络安全限制
|
||||
Set("disable-gpu", ""). // 禁用 GPU 加速
|
||||
Set("no-sandbox", ""). // 禁用沙箱模式
|
||||
Set("disable-setuid-sandbox", "").// 禁用 setuid 沙箱
|
||||
Headless(true). // 无头模式
|
||||
Set("disable-web-security", ""). // 禁用网络安全限制
|
||||
Set("disable-gpu", ""). // 禁用 GPU 加速
|
||||
Set("no-sandbox", ""). // 禁用沙箱模式
|
||||
Set("disable-setuid-sandbox", ""). // 禁用 setuid 沙箱
|
||||
MustLaunch()
|
||||
|
||||
browser := rod.New().ControlURL(u).MustConnect()
|
||||
|
||||
|
||||
return &Service{
|
||||
browser: browser,
|
||||
}, nil
|
||||
@@ -50,7 +50,7 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
if keyword == "" {
|
||||
return nil, errors.New("搜索关键词不能为空")
|
||||
}
|
||||
|
||||
|
||||
if maxPages <= 0 {
|
||||
maxPages = 1
|
||||
}
|
||||
@@ -59,18 +59,18 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
}
|
||||
|
||||
results := make([]SearchResult, 0)
|
||||
|
||||
|
||||
// 使用百度搜索
|
||||
searchURL := fmt.Sprintf("https://www.baidu.com/s?wd=%s", url.QueryEscape(keyword))
|
||||
|
||||
|
||||
// 设置页面超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
// 创建页面
|
||||
page := s.browser.MustPage()
|
||||
defer page.MustClose()
|
||||
|
||||
|
||||
// 设置视口大小
|
||||
err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||||
Width: 1280,
|
||||
@@ -79,19 +79,19 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("设置视口失败: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// 导航到搜索页面
|
||||
err = page.Context(ctx).Navigate(searchURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("导航到搜索页面失败: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// 等待搜索结果加载完成
|
||||
err = page.WaitLoad()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("等待页面加载完成失败: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// 分析当前页面的搜索结果
|
||||
for i := 0; i < maxPages; i++ {
|
||||
if i > 0 {
|
||||
@@ -100,52 +100,52 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
if err != nil || nextPage == nil {
|
||||
break // 没有下一页
|
||||
}
|
||||
|
||||
|
||||
err = nextPage.Click(proto.InputMouseButtonLeft, 1)
|
||||
if err != nil {
|
||||
break // 点击下一页失败
|
||||
}
|
||||
|
||||
|
||||
// 等待新页面加载
|
||||
err = page.WaitLoad()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 提取搜索结果
|
||||
resultElements, err := page.Elements(".result, .c-container")
|
||||
if err != nil || resultElements == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
for _, result := range resultElements {
|
||||
// 获取标题
|
||||
titleElement, err := result.Element("h3, .t")
|
||||
if err != nil || titleElement == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
title, err := titleElement.Text()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 获取 URL
|
||||
linkElement, err := titleElement.Element("a")
|
||||
if err != nil || linkElement == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
href, err := linkElement.Attribute("href")
|
||||
if err != nil || href == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 获取内容摘要 - 尝试多个可能的选择器
|
||||
var contentElement *rod.Element
|
||||
var content string
|
||||
|
||||
|
||||
// 尝试多个可能的选择器来适应不同版本的百度搜索结果
|
||||
selectors := []string{".content-right_8Zs40", ".c-abstract", ".content_LJ0WN", ".content"}
|
||||
for _, selector := range selectors {
|
||||
@@ -157,7 +157,7 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果所有选择器都失败,尝试直接从结果块中提取文本
|
||||
if content == "" {
|
||||
// 获取结果元素的所有文本
|
||||
@@ -173,21 +173,21 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 添加到结果集
|
||||
results = append(results, SearchResult{
|
||||
Title: title,
|
||||
URL: *href,
|
||||
Content: content,
|
||||
})
|
||||
|
||||
|
||||
// 限制结果数量,每页最多 10 条
|
||||
if len(results) >= 10*maxPages {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取真实 URL(百度搜索结果中的 URL 是短链接,需要跳转获取真实 URL)
|
||||
for i, result := range results {
|
||||
realURL, err := s.getRedirectURL(result.URL)
|
||||
@@ -195,7 +195,7 @@ func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error
|
||||
results[i].URL = realURL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -209,22 +209,22 @@ func (s *Service) getRedirectURL(shortURL string) (string, error) {
|
||||
defer func() {
|
||||
_ = page.Close()
|
||||
}()
|
||||
|
||||
|
||||
// 导航到短链接
|
||||
err = page.Navigate(shortURL)
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
|
||||
|
||||
// 等待重定向完成
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
|
||||
// 获取当前 URL
|
||||
info, err := page.Info()
|
||||
if err != nil {
|
||||
return shortURL, err // 返回原始URL
|
||||
}
|
||||
|
||||
|
||||
return info.URL, nil
|
||||
}
|
||||
|
||||
@@ -247,21 +247,21 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
log.Errorf("爬虫服务崩溃: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
service, err := NewService()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建爬虫服务失败: %v", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
|
||||
// 设置超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
// 使用goroutine和通道来处理超时
|
||||
resultChan := make(chan []SearchResult, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
|
||||
go func() {
|
||||
results, err := service.WebSearch(keyword, maxPages)
|
||||
if err != nil {
|
||||
@@ -270,7 +270,7 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
}
|
||||
resultChan <- results
|
||||
}()
|
||||
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -281,32 +281,32 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
if len(results) == 0 {
|
||||
return "未找到关于 \"" + keyword + "\" 的相关搜索结果", nil
|
||||
}
|
||||
|
||||
|
||||
// 格式化结果
|
||||
var builder strings.Builder
|
||||
builder.WriteString(fmt.Sprintf("为您找到关于 \"%s\" 的 %d 条搜索结果:\n\n", keyword, len(results)))
|
||||
|
||||
|
||||
for i, result := range results {
|
||||
// // 尝试打开链接获取实际内容
|
||||
// page := service.browser.MustPage()
|
||||
// defer page.MustClose()
|
||||
|
||||
|
||||
// // 设置页面超时
|
||||
// pageCtx, pageCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
// defer pageCancel()
|
||||
|
||||
|
||||
// // 导航到目标页面
|
||||
// err := page.Context(pageCtx).Navigate(result.URL)
|
||||
// if err == nil {
|
||||
// // 等待页面加载
|
||||
// _ = page.WaitLoad()
|
||||
|
||||
|
||||
// // 获取页面标题
|
||||
// title, err := page.Eval("() => document.title")
|
||||
// if err == nil && title.Value.String() != "" {
|
||||
// result.Title = title.Value.String()
|
||||
// }
|
||||
|
||||
|
||||
// // 获取页面主要内容
|
||||
// if content, err := page.Element("body"); err == nil {
|
||||
// if text, err := content.Text(); err == nil {
|
||||
@@ -315,11 +315,11 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
// if len(text) > 200 {
|
||||
// text = text[:200] + "..."
|
||||
// }
|
||||
// result.Content = text
|
||||
// result.Prompt = text
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, result.Title))
|
||||
builder.WriteString(fmt.Sprintf(" 链接: %s\n", result.URL))
|
||||
if result.Content != "" {
|
||||
@@ -327,7 +327,7 @@ func SearchWeb(keyword string, maxPages int) (string, error) {
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"geekai/store"
|
||||
"geekai/store/model"
|
||||
"geekai/utils"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
@@ -182,9 +181,6 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
|
||||
return "", fmt.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error)
|
||||
}
|
||||
|
||||
all, _ := io.ReadAll(r.Body)
|
||||
logger.Debugf("response: %+v", string(all))
|
||||
|
||||
// update the api key last use time
|
||||
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
|
||||
var imgURL string
|
||||
|
||||
@@ -94,6 +94,8 @@ func (s *Service) Run() {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("任务提交成功: %+v", r)
|
||||
|
||||
// 更新任务信息
|
||||
s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
|
||||
"task_id": r.Data,
|
||||
@@ -127,6 +129,7 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
||||
"continue_clip_id": task.RefSongId,
|
||||
"continue_at": task.ExtendSecs,
|
||||
"make_instrumental": task.Instrumental,
|
||||
"mv": task.Model,
|
||||
}
|
||||
// 灵感模式
|
||||
if task.Type == 1 {
|
||||
@@ -134,7 +137,6 @@ func (s *Service) Create(task types.SunoTask) (RespVo, error) {
|
||||
} else { // 自定义模式
|
||||
reqBody["prompt"] = task.Lyrics
|
||||
reqBody["tags"] = task.Tags
|
||||
reqBody["mv"] = task.Model
|
||||
reqBody["title"] = task.Title
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ Please remember, the final output must be the same language with user’s input.
|
||||
- What kinds of examples may need to be included, how many, and whether they are complex enough to benefit from placeholders.
|
||||
- Clarity and Conciseness: Use clear, specific language. Avoid unnecessary instructions or bland statements.
|
||||
- Formatting: Use markdown features for readability. DO NOT USE CODE BLOCKS UNLESS SPECIFICALLY REQUESTED.
|
||||
- Preserve User Content: If the input task or prompt includes extensive guidelines or examples, preserve them entirely, or as closely as possible. If they are vague, consider breaking down into sub-steps. Keep any details, guidelines, examples, variables, or placeholders provided by the user.
|
||||
- Preserve User Prompt: If the input task or prompt includes extensive guidelines or examples, preserve them entirely, or as closely as possible. If they are vague, consider breaking down into sub-steps. Keep any details, guidelines, examples, variables, or placeholders provided by the user.
|
||||
- Constants: DO include constants in the prompt, as they are not susceptible to prompt injection. Such as guides, rubrics, and examples.
|
||||
- Output Format: Explicitly the most appropriate output format, in detail. This should include length and syntax (e.g. short sentence, paragraph, JSON, etc.)
|
||||
- For tasks outputting well-defined or structured data (classification, JSON, etc.) bias toward outputting a JSON.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package service
|
||||
|
||||
import "geekai/core/types"
|
||||
|
||||
type WebsocketService struct {
|
||||
Clients *types.LMap[string, *types.WsClient] // clientId => Client
|
||||
}
|
||||
|
||||
func NewWebsocketService() *WebsocketService {
|
||||
return &WebsocketService{
|
||||
Clients: types.NewLMap[string, *types.WsClient](),
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,17 @@ import (
|
||||
)
|
||||
|
||||
type ChatMessage struct {
|
||||
Id int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
UserId uint `gorm:"column:user_id;type:int;not null;comment:用户 ID" json:"user_id"`
|
||||
ChatId string `gorm:"column:chat_id;type:char(40);not null;index;comment:会话 ID" json:"chat_id"`
|
||||
Type string `gorm:"column:type;type:varchar(10);not null;comment:类型:prompt|reply" json:"type"`
|
||||
Icon string `gorm:"column:icon;type:varchar(255);not null;comment:角色图标" json:"icon"`
|
||||
RoleId uint `gorm:"column:role_id;type:int;not null;comment:角色 ID" json:"role_id"`
|
||||
Model string `gorm:"column:model;type:varchar(30);comment:模型名称" json:"model"`
|
||||
RoleId uint `gorm:"column:role_id;type:int;not null;comment:角色 ID" json:"role_id"`
|
||||
Model string `gorm:"column:model;type:varchar(255);comment:模型名称" json:"model"`
|
||||
Content string `gorm:"column:content;type:text;not null;comment:聊天内容" json:"content"`
|
||||
Tokens int `gorm:"column:tokens;type:smallint;not null;comment:耗费 token 数量" json:"tokens"`
|
||||
TotalTokens int `gorm:"column:total_tokens;type:int;not null;comment:消耗总Token长度" json:"total_tokens"`
|
||||
UseContext bool `gorm:"column:use_context;type:tinyint(1);not null;comment:是否允许作为上下文语料" json:"use_context"`
|
||||
UseContext bool `gorm:"column:use_context;type:tinyint(1);not null;comment:是否允许作为上下文语料" json:"use_context"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
}
|
||||
@@ -5,20 +5,20 @@ import (
|
||||
)
|
||||
|
||||
type ChatModel struct {
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Description string `gorm:"column:description;type:varchar(1024);not null;default:'';comment:模型类型描述" json:"description"`
|
||||
Category string `gorm:"column:category;type:varchar(1024);not null;default:'';comment:模型类别" json:"category"`
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Desc string `gorm:"column:desc;type:varchar(1024);not null;default:'';comment:模型类型描述" json:"desc"`
|
||||
Tag string `gorm:"column:tag;type:varchar(1024);not null;default:'';comment:模型标签" json:"tag"`
|
||||
Type string `gorm:"column:type;type:varchar(10);not null;default:chat;comment:模型类型(chat,img)" json:"type"`
|
||||
Name string `gorm:"column:name;type:varchar(255);not null;comment:模型名称" json:"name"`
|
||||
Value string `gorm:"column:value;type:varchar(255);not null;comment:模型值" json:"value"`
|
||||
SortNum int `gorm:"column:sort_num;type:tinyint(1);not null;comment:排序数字" json:"sort_num"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启用模型" json:"enabled"`
|
||||
Enabled bool `gorm:"column:enabled;type:tinyint(1);not null;default:0;comment:是否启用模型" json:"enabled"`
|
||||
Power int `gorm:"column:power;type:smallint;not null;comment:消耗算力点数" json:"power"`
|
||||
Temperature float32 `gorm:"column:temperature;type:float(3,1);not null;default:1.0;comment:模型创意度" json:"temperature"`
|
||||
MaxTokens int `gorm:"column:max_tokens;type:int;not null;default:1024;comment:最大响应长度" json:"max_tokens"`
|
||||
MaxContext int `gorm:"column:max_context;type:int;not null;default:4096;comment:最大上下文长度" json:"max_context"`
|
||||
Open bool `gorm:"column:open;type:tinyint(1);not null;comment:是否开放模型" json:"open"`
|
||||
KeyId uint `gorm:"column:key_id;type:int;not null;comment:绑定API KEY ID" json:"key_id"`
|
||||
Open bool `gorm:"column:open;type:tinyint(1);not null;comment:是否开放模型" json:"open"`
|
||||
KeyId uint `gorm:"column:key_id;type:int;not null;comment:绑定API KEY ID" json:"key_id"`
|
||||
Options string `gorm:"column:options;type:text;not null;comment:模型自定义选项" json:"options"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime" json:"updated_at"`
|
||||
|
||||
@@ -5,27 +5,27 @@ import (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);uniqueIndex;not null;comment:用户名" json:"username"`
|
||||
Mobile string `gorm:"column:mobile;type:char(11);comment:手机号" json:"mobile"`
|
||||
Email string `gorm:"column:email;type:varchar(50);comment:邮箱地址" json:"email"`
|
||||
Nickname string `gorm:"column:nickname;type:varchar(30);not null;comment:昵称" json:"nickname"`
|
||||
Password string `gorm:"column:password;type:char(64);not null;comment:密码" json:"password"`
|
||||
Avatar string `gorm:"column:avatar;type:varchar(255);not null;comment:头像" json:"avatar"`
|
||||
Salt string `gorm:"column:salt;type:char(12);not null;comment:密码盐" json:"salt"`
|
||||
Power int `gorm:"column:power;type:int;not null;default:0;comment:剩余算力" json:"power"`
|
||||
ExpiredTime int64 `gorm:"column:expired_time;type:int;not null;comment:用户过期时间" json:"expired_time"`
|
||||
Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"`
|
||||
ChatConfig string `gorm:"column:chat_config;type:text;not null;comment:聊天配置json" json:"chat_config"`
|
||||
ChatRoles string `gorm:"column:chat_roles_json;type:text;not null;comment:聊天角色 json" json:"chat_roles_json"`
|
||||
ChatModels string `gorm:"column:chat_models_json;type:text;not null;comment:AI模型 json" json:"chat_models_json"`
|
||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||
Vip bool `gorm:"column:vip;type:tinyint(1);not null;default:0;comment:是否会员" json:"vip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
OpenId string `gorm:"column:openid;type:varchar(100);comment:第三方登录账号ID" json:"openid"`
|
||||
Platform string `gorm:"column:platform;type:varchar(30);comment:登录平台" json:"platform"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
Username string `gorm:"column:username;type:varchar(30);uniqueIndex;not null;comment:用户名" json:"username"`
|
||||
Mobile string `gorm:"column:mobile;type:char(11);comment:手机号" json:"mobile"`
|
||||
Email string `gorm:"column:email;type:varchar(50);comment:邮箱地址" json:"email"`
|
||||
Nickname string `gorm:"column:nickname;type:varchar(30);not null;comment:昵称" json:"nickname"`
|
||||
Password string `gorm:"column:password;type:char(64);not null;comment:密码" json:"password"`
|
||||
Avatar string `gorm:"column:avatar;type:varchar(255);not null;comment:头像" json:"avatar"`
|
||||
Salt string `gorm:"column:salt;type:char(12);not null;comment:密码盐" json:"salt"`
|
||||
Power int `gorm:"column:power;type:int;default:0;comment:剩余算力" json:"power"`
|
||||
ExpiredTime int64 `gorm:"column:expired_time;type:int;not null;comment:用户过期时间" json:"expired_time"`
|
||||
Status bool `gorm:"column:status;type:tinyint(1);not null;comment:当前状态" json:"status"`
|
||||
ChatConfig string `gorm:"column:chat_config_json;type:text;default:null;comment:聊天配置json" json:"chat_config"`
|
||||
ChatRoles string `gorm:"column:chat_roles_json;type:text;default:null;comment:聊天角色 json" json:"chat_roles"`
|
||||
ChatModels string `gorm:"column:chat_models_json;type:text;default:null;comment:AI模型 json" json:"chat_models"`
|
||||
LastLoginAt int64 `gorm:"column:last_login_at;type:int;not null;comment:最后登录时间" json:"last_login_at"`
|
||||
Vip bool `gorm:"column:vip;type:tinyint(1);not null;default:0;comment:是否会员" json:"vip"`
|
||||
LastLoginIp string `gorm:"column:last_login_ip;type:char(16);not null;comment:最后登录 IP" json:"last_login_ip"`
|
||||
OpenId string `gorm:"column:openid;type:varchar(100);comment:第三方登录账号ID" json:"openid"`
|
||||
Platform string `gorm:"column:platform;type:varchar(30);comment:登录平台" json:"platform"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (m *User) TableName() string {
|
||||
|
||||
@@ -9,16 +9,17 @@ package store
|
||||
|
||||
import (
|
||||
"geekai/core/types"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewGormConfig() *gorm.Config {
|
||||
return &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
Logger: logger.Default.LogMode(logger.Warn),
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "chatgpt_", // 设置表前缀
|
||||
SingularTable: false, // 使用单数表名形式
|
||||
@@ -36,9 +37,9 @@ func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB.SetMaxIdleConns(32)
|
||||
sqlDB.SetMaxOpenConns(512)
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package vo
|
||||
|
||||
type HistoryMessage struct {
|
||||
BaseVo
|
||||
ChatId string `json:"chat_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
RoleId uint `json:"role_id"`
|
||||
Model string `json:"model"`
|
||||
Type string `json:"type"`
|
||||
Icon string `json:"icon"`
|
||||
Tokens int `json:"tokens"`
|
||||
Content string `json:"content"`
|
||||
UseContext bool `json:"use_context"`
|
||||
}
|
||||
21
api/store/vo/chat_message.go
Normal file
21
api/store/vo/chat_message.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package vo
|
||||
|
||||
type MsgContent struct {
|
||||
Text string `json:"text"`
|
||||
Files []File `json:"files"`
|
||||
}
|
||||
|
||||
type ChatMessage struct {
|
||||
Id uint `json:"id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
ChatId string `json:"chat_id"`
|
||||
UserId uint `json:"user_id"`
|
||||
RoleId uint `json:"role_id"`
|
||||
Model string `json:"model"`
|
||||
Type string `json:"type"`
|
||||
Icon string `json:"icon"`
|
||||
Tokens int `json:"tokens"`
|
||||
Content MsgContent `json:"content"`
|
||||
UseContext bool `json:"use_context"`
|
||||
}
|
||||
@@ -10,8 +10,8 @@ type ChatModel struct {
|
||||
Open bool `json:"open"`
|
||||
MaxTokens int `json:"max_tokens"` // 最大响应长度
|
||||
MaxContext int `json:"max_context"` // 最大上下文长度
|
||||
Description string `json:"description"` // 模型描述
|
||||
Category string `json:"category"` //模型类别
|
||||
Desc string `json:"desc"` // 模型描述
|
||||
Tag string `json:"tag"` //模型标签
|
||||
Temperature float32 `json:"temperature"` // 模型温度
|
||||
KeyId uint `json:"key_id,omitempty"`
|
||||
KeyName string `json:"key_name"`
|
||||
|
||||
196
build/geekai-install.sh
Executable file
196
build/geekai-install.sh
Executable file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
# install-docker.sh — 自动识别 Linux 发行版并安装 Docker (Compose v2)
|
||||
# 支持国内镜像自动/手动切换,内置错误处理与友好提示。
|
||||
# ---------------------------------------------------------------
|
||||
set -Eeuo pipefail
|
||||
|
||||
# ────────────────────────── 彩色输出 ────────────────────────── #
|
||||
info() { printf "\e[32m[INFO]\e[0m %s\n" "$*"; }
|
||||
warn() { printf "\e[33m[WARN]\e[0m %s\n" "$*" >&2; }
|
||||
error() { printf "\e[31m[ERROR]\e[0m %s\n" "$*" >&2; exit 1; }
|
||||
trap 'error "脚本失败,命令:\"${BASH_COMMAND}\", 退出码 $?"' ERR
|
||||
|
||||
need_cmd() { command -v "$1" &>/dev/null; }
|
||||
as_root() { ((EUID==0)) && "$@" || sudo "$@"; }
|
||||
|
||||
# ─────────── 部署 GeekAI-Plus 函数 ─────────── #
|
||||
deploy_geekai_plus(){
|
||||
local repo=https://gitee.com/blackfox/geekai-plus-open.git
|
||||
local dir=${GEEKAI_DIR:-geekai-plus}
|
||||
info "部署 GeekAI-Plus 到目录 \"$dir\""
|
||||
need_cmd git || error "未找到 git,请检查安装步骤。"
|
||||
if [[ -d $dir ]]; then
|
||||
warn "目录 $dir 已存在,跳过克隆。"
|
||||
else
|
||||
git clone --depth 1 "$repo" "$dir"
|
||||
fi
|
||||
pushd "$dir" >/dev/null
|
||||
info "启动 docker compose…"
|
||||
if docker compose up -d; then
|
||||
info "GeekAI-Plus 部署完成!请访问 http://ip:8080。"
|
||||
else
|
||||
error "docker compose 启动失败。"
|
||||
fi
|
||||
popd >/dev/null
|
||||
}
|
||||
|
||||
# ─────────────────── 检测 Docker 是否已安装 ─────────────────── #
|
||||
if need_cmd docker && (docker compose version &>/dev/null || need_cmd docker-compose); then
|
||||
info "Docker 与 Compose 已安装,无需重复操作。"
|
||||
deploy_geekai_plus
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ────────────────────────── 解析发行版 ───────────────────────── #
|
||||
[[ -r /etc/os-release ]] || error "无法识别系统:缺少 /etc/os-release"
|
||||
. /etc/os-release
|
||||
OS_ID=${ID,,}
|
||||
OS_VER=${VERSION_ID:-unknown}
|
||||
ARCH=$(uname -m)
|
||||
|
||||
info "检测到系统:$PRETTY_NAME ($OS_ID $OS_VER, $ARCH)"
|
||||
|
||||
# ──────────────────── 镜像域名与自动回退逻辑 ──────────────────── #
|
||||
# ❶ 用户可通过 DOCKER_MIRROR 指定:
|
||||
# - aliyun → https://mirrors.aliyun.com/docker-ce
|
||||
# - tuna → https://mirrors.tuna.tsinghua.edu.cn/docker-ce
|
||||
# - official (默认) → https://download.docker.com
|
||||
#
|
||||
# ❷ 若未指定,则先探测官方域名能否连通;失败则自动切换到 aliyun。
|
||||
#
|
||||
choose_mirror() {
|
||||
local sel=${DOCKER_MIRROR:-auto}
|
||||
|
||||
case "$sel" in
|
||||
aliyun) MIRROR="https://mirrors.aliyun.com/docker-ce" ;;
|
||||
tuna) MIRROR="https://mirrors.tuna.tsinghua.edu.cn/docker-ce" ;;
|
||||
official) MIRROR="https://download.docker.com" ;;
|
||||
auto)
|
||||
MIRROR="https://download.docker.com"
|
||||
info "检测官方源连通性…"
|
||||
if ! curl -m 3 -sfL "${MIRROR}/linux/${OS_ID}/gpg" -o /dev/null; then
|
||||
warn "官方源不可达,回退至阿里云镜像。"
|
||||
MIRROR="https://mirrors.aliyun.com/docker-ce"
|
||||
fi ;;
|
||||
*)
|
||||
error "未知镜像标识:$sel(可选 aliyun|tuna|official)" ;;
|
||||
esac
|
||||
info "使用镜像源:$MIRROR"
|
||||
}
|
||||
choose_mirror
|
||||
|
||||
# ────────────────────────── 安装函数 ────────────────────────── #
|
||||
install_docker_debian_like() {
|
||||
info "使用 APT 安装 Docker"
|
||||
as_root apt-get update -y
|
||||
as_root apt-get install -y ca-certificates curl git gnupg lsb-release
|
||||
|
||||
as_root install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL "${MIRROR}/linux/${OS_ID}/gpg" \
|
||||
| as_root gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
${MIRROR}/linux/${OS_ID} $(lsb_release -cs) stable" \
|
||||
| as_root tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
|
||||
as_root apt-get update -y
|
||||
as_root apt-get install -y \
|
||||
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
}
|
||||
|
||||
install_docker_centos_like() {
|
||||
info "使用 YUM/DNF 安装 Docker"
|
||||
local pkgcmd
|
||||
if need_cmd dnf; then pkgcmd=dnf; else pkgcmd=yum; fi
|
||||
|
||||
as_root $pkgcmd -y install ${pkgcmd}-plugins-core git
|
||||
as_root $pkgcmd config-manager \
|
||||
--add-repo "${MIRROR}/linux/centos/docker-ce.repo"
|
||||
as_root $pkgcmd -y install \
|
||||
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
as_root systemctl enable --now docker
|
||||
}
|
||||
|
||||
install_docker_fedora() {
|
||||
info "使用 DNF 安装 Docker (Fedora)"
|
||||
as_root dnf -y install dnf-plugins-core
|
||||
as_root dnf config-manager --add-repo \
|
||||
"${MIRROR}/linux/fedora/docker-ce.repo"
|
||||
as_root dnf -y install \
|
||||
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin git
|
||||
as_root systemctl enable --now docker
|
||||
}
|
||||
|
||||
install_docker_arch() {
|
||||
info "使用 pacman 安装 Docker"
|
||||
as_root pacman -Sy --noconfirm docker docker-compose git
|
||||
as_root systemctl enable --now docker
|
||||
}
|
||||
|
||||
install_docker_opensuse() {
|
||||
info "使用 zypper 安装 Docker"
|
||||
as_root zypper -n in docker docker-compose git
|
||||
as_root systemctl enable --now docker
|
||||
}
|
||||
|
||||
install_docker_alpine() {
|
||||
info "使用 apk 安装 Docker"
|
||||
as_root apk add --no-cache docker docker-cli-compose git
|
||||
as_root rc-update add docker boot git
|
||||
as_root service docker start
|
||||
}
|
||||
|
||||
install_docker_fallback() {
|
||||
warn "发行版 \"$OS_ID\" 未做专门适配,执行官方一键脚本…"
|
||||
curl -fsSL get.docker.com | as_root sh
|
||||
}
|
||||
|
||||
# ────────────────────────── 分发安装 ────────────────────────── #
|
||||
case "$OS_ID" in
|
||||
debian|ubuntu|linuxmint) install_docker_debian_like ;;
|
||||
centos|rocky|almalinux|rhel) install_docker_centos_like ;;
|
||||
fedora) install_docker_fedora ;;
|
||||
arch|manjaro) install_docker_arch ;;
|
||||
opensuse*|suse|sles) install_docker_opensuse ;;
|
||||
alpine) install_docker_alpine ;;
|
||||
*) install_docker_fallback ;;
|
||||
esac
|
||||
|
||||
# ──────────────────── 安装后检查 & docker 组 ─────────────────── #
|
||||
need_cmd docker || error "Docker 安装后仍不可用,请检查日志。"
|
||||
as_root usermod -aG docker "${SUDO_USER:-$USER}" || true
|
||||
|
||||
# ──────────────────── (可选) 镜像加速器配置 ─────────────────── #
|
||||
if [[ "${ENABLE_REGISTRYMIRROR:-1}" == "1" ]]; then
|
||||
as_root mkdir -p /etc/docker
|
||||
cat <<-JSON | as_root tee /etc/docker/daemon.json >/dev/null
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://registry.docker-cn.com", "https://mirror.ccs.tencentyun.com","https://hub-mirror.c.163.com"
|
||||
]
|
||||
}
|
||||
JSON
|
||||
as_root systemctl restart docker
|
||||
info "已为 Docker 配置国内镜像加速器。"
|
||||
fi
|
||||
|
||||
# ────────────────────────── 最终信息 ────────────────────────── #
|
||||
info "Docker 版本:$(docker --version | cut -d',' -f1)"
|
||||
if docker compose version &>/dev/null; then
|
||||
info "Compose 版本:$(docker compose version --short)"
|
||||
elif need_cmd docker-compose; then
|
||||
info "Compose 版本:$(docker-compose --version | awk '{print $3}')"
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
╭─────────────────────────────────────────────────────────╮
|
||||
│ 安装完成! │
|
||||
│ · 请重新登录或执行 `newgrp docker` 以使用 docker 免 sudo │
|
||||
│ · 如需跳过镜像加速,可执行:ENABLE_REGISTRYMIRROR=0 ... │
|
||||
╰─────────────────────────────────────────────────────────╯
|
||||
EOF
|
||||
|
||||
deploy_geekai_plus
|
||||
|
||||
@@ -110,7 +110,7 @@ CREATE TABLE `chatgpt_chat_history` (
|
||||
`type` varchar(10) NOT NULL COMMENT '类型:prompt|reply',
|
||||
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色图标',
|
||||
`role_id` bigint NOT NULL COMMENT '角色 ID',
|
||||
`model` varchar(30) DEFAULT NULL COMMENT '模型名称',
|
||||
`model` varchar(255) DEFAULT NULL COMMENT '模型名称',
|
||||
`content` text NOT NULL COMMENT '聊天内容',
|
||||
`tokens` smallint NOT NULL COMMENT '耗费 token 数量',
|
||||
`total_tokens` bigint NOT NULL COMMENT '消耗总Token长度',
|
||||
@@ -134,7 +134,7 @@ CREATE TABLE `chatgpt_chat_items` (
|
||||
`role_id` bigint NOT NULL COMMENT '角色 ID',
|
||||
`title` varchar(100) NOT NULL COMMENT '会话标题',
|
||||
`model_id` bigint NOT NULL DEFAULT '0' COMMENT '模型 ID',
|
||||
`model` varchar(30) DEFAULT NULL COMMENT '模型名称',
|
||||
`model` varchar(255) DEFAULT NULL COMMENT '模型名称',
|
||||
`created_at` datetime NOT NULL COMMENT '创建时间',
|
||||
`updated_at` datetime NOT NULL COMMENT '更新时间',
|
||||
`deleted_at` datetime DEFAULT NULL
|
||||
@@ -465,7 +465,7 @@ CREATE TABLE `chatgpt_power_logs` (
|
||||
`type` tinyint(1) NOT NULL COMMENT '类型(1:充值,2:消费,3:退费)',
|
||||
`amount` smallint NOT NULL COMMENT '算力数值',
|
||||
`balance` bigint NOT NULL COMMENT '余额',
|
||||
`model` varchar(30) NOT NULL COMMENT '模型',
|
||||
`model` varchar(255) NOT NULL COMMENT '模型',
|
||||
`remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
|
||||
`mark` tinyint(1) NOT NULL COMMENT '资金类型(0:支出,1:收入)',
|
||||
`created_at` datetime NOT NULL COMMENT '创建时间'
|
||||
|
||||
3
database/update-v4.2.3.1.sql
Normal file
3
database/update-v4.2.3.1.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
|
||||
(4, 'privacy', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"default_nickname\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"# 隐私政策\\n\\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。\",\"updated\":true}'),
|
||||
(5, 'agreement', '{\"sd_neg_prompt\":\"\",\"mj_mode\":\"\",\"index_navs\":null,\"copyright\":\"\",\"default_nickname\":\"\",\"icp\":\"\",\"mark_map_text\":\"\",\"enabled_verify\":false,\"email_white_list\":null,\"translate_model_id\":0,\"max_file_size\":0,\"content\":\"# 用户协议\\n\\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。\",\"updated\":true}');
|
||||
@@ -2,4 +2,4 @@ ALTER TABLE `chatgpt_chat_models` ADD `category` VARCHAR(1024) NOT NULL DEFAULT
|
||||
ALTER TABLE `chatgpt_chat_models` ADD `description` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '模型类型描述' AFTER `id`;
|
||||
ALTER TABLE `chatgpt_orders` DROP `deleted_at`;
|
||||
ALTER TABLE `chatgpt_chat_history` DROP `deleted_at`;
|
||||
ALTER TABLE `chatgpt_chat_items` DROP `deleted_at`;
|
||||
ALTER TABLE `chatgpt_chat_items` DROP `deleted_at`;
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
|
||||
# 后端 API 程序
|
||||
geekai-api:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.2.3-amd64
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.2.4-amd64
|
||||
container_name: geekai-api
|
||||
restart: always
|
||||
depends_on:
|
||||
@@ -69,7 +69,7 @@ services:
|
||||
|
||||
# 前端应用
|
||||
geekai-web:
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.2.3-amd64
|
||||
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.2.4-amd64
|
||||
container_name: geekai-web
|
||||
restart: always
|
||||
depends_on:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
VUE_APP_API_HOST=http://localhost:5678
|
||||
VUE_APP_WS_HOST=ws://localhost:5678
|
||||
VUE_APP_USER=18888888888
|
||||
VUE_APP_PASS=12345678
|
||||
VUE_APP_ADMIN_USER=admin
|
||||
VUE_APP_ADMIN_PASS=admin123
|
||||
VUE_APP_KEY_PREFIX=GeekAI_DEV_
|
||||
VUE_APP_TITLE="Geek-AI 创作系统"
|
||||
VUE_APP_VERSION=v4.2.2
|
||||
VUE_APP_DOCS_URL=https://docs.geekai.me
|
||||
VUE_APP_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
VUE_APP_GITEE_URL=https://gitee.com/blackfox/geekai
|
||||
VUE_APP_GITCODE_URL=https://gitcode.com/yangjian102621/geekai
|
||||
VITE_API_HOST=http://localhost:5678
|
||||
VITE_WS_HOST=ws://localhost:5678
|
||||
VITE_USER=18888888888
|
||||
VITE_PASS=12345678
|
||||
VITE_ADMIN_USER=admin
|
||||
VITE_ADMIN_PASS=admin123
|
||||
VITE_KEY_PREFIX=GeekAI_DEV_
|
||||
VITE_TITLE="Geek-AI 创作系统"
|
||||
VITE_VERSION=v4.2.4
|
||||
VITE_DOCS_URL=https://docs.geekai.me
|
||||
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
VITE_GITEE_URL=https://gitee.com/blackfox/geekai
|
||||
VITE_GITCODE_URL=https://gitcode.com/yangjian102621/geekai
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
VUE_APP_API_HOST=
|
||||
VUE_APP_WS_HOST=
|
||||
VUE_APP_KEY_PREFIX=GeekAI_
|
||||
VUE_APP_VERSION=v4.2.2
|
||||
VITE_API_HOST=
|
||||
VITE_WS_HOST=
|
||||
VITE_KEY_PREFIX=GeekAI_
|
||||
VITE_VERSION=v4.2.4
|
||||
VUE_APP_TITLE="Geek-AI 创作系统"
|
||||
VUE_APP_DOCS_URL=https://docs.geekai.me
|
||||
VUE_APP_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
VUE_APP_GITEE_URL=https://gitee.com/blackfox/geekai
|
||||
VUE_APP_GITCODE_URL=https://gitcode.com/yangjian102621/geekai
|
||||
VITE_DOCS_URL=https://docs.geekai.me
|
||||
VITE_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
VITE_GITEE_URL=https://gitee.com/blackfox/geekai
|
||||
VITE_GITCODE_URL=https://gitcode.com/yangjian102621/geekai
|
||||
|
||||
78
web/auto-imports.d.ts
vendored
Normal file
78
web/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
content="width=device-width,initial-scale=1.0,user-scalable=no"
|
||||
/>
|
||||
<title>Geek-AI 创作助手</title>
|
||||
<!-- Add any global CSS files here if needed, e.g., <link rel="stylesheet" href="/css/global.css"> -->
|
||||
<!-- Favicon can be linked here, e.g., <link rel="icon" href="/favicon.ico"> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -15,5 +17,6 @@
|
||||
<strong>请开启JavaScript支持</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
13767
web/package-lock.json
generated
13767
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,12 +2,18 @@
|
||||
"name": "geekai-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-scroll/core": "^2.5.1",
|
||||
"@better-scroll/mouse-wheel": "^2.5.1",
|
||||
"@better-scroll/observe-dom": "^2.5.1",
|
||||
"@better-scroll/pull-up": "^2.5.1",
|
||||
"@better-scroll/scroll-bar": "^2.5.1",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^0.27.2",
|
||||
@@ -23,6 +29,7 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-emoji": "^2.0.0",
|
||||
"markdown-it-mathjax3": "^4.3.2",
|
||||
"marked": "^15.0.11",
|
||||
"markmap-common": "^0.16.0",
|
||||
"markmap-lib": "^0.16.1",
|
||||
"markmap-toolbar": "^0.17.0",
|
||||
@@ -32,51 +39,22 @@
|
||||
"pinia": "^2.1.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"qs": "^6.11.1",
|
||||
"@better-scroll/core": "^2.5.1",
|
||||
"@better-scroll/mouse-wheel": "^2.5.1",
|
||||
"@better-scroll/observe-dom": "^2.5.1",
|
||||
"@better-scroll/pull-up": "^2.5.1",
|
||||
"@better-scroll/scroll-bar": "^2.5.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"three": "^0.128.0",
|
||||
"vant": "^4.5.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.15",
|
||||
"unplugin-auto-import": "^0.18.5",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"vue-waterfall-plugin-next": "^2.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.18.6",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"postcss": "^8.4.49",
|
||||
"stylus": "^0.58.1",
|
||||
"stylus-loader": "^7.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"webpack": "^5.90.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
|
||||
111
web/src/App.vue
111
web/src/App.vue
@@ -5,97 +5,58 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElConfigProvider } from "element-plus";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
|
||||
import { isChrome, isMobile } from "@/utils/libs";
|
||||
import { showMessageInfo } from "@/utils/dialog";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import { getUserToken } from "@/store/session";
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { showMessageInfo } from '@/utils/dialog'
|
||||
import { isChrome, isMobile } from '@/utils/libs'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const debounce = (fn, delay) => {
|
||||
let timer;
|
||||
let timer
|
||||
return (...args) => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
clearTimeout(timer)
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
fn(...args);
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
fn(...args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
const _ResizeObserver = window.ResizeObserver;
|
||||
const _ResizeObserver = window.ResizeObserver
|
||||
window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
|
||||
constructor(callback) {
|
||||
callback = debounce(callback, 200);
|
||||
super(callback);
|
||||
callback = debounce(callback, 200)
|
||||
super(callback)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const store = useSharedStore();
|
||||
const store = useSharedStore()
|
||||
onMounted(() => {
|
||||
// 获取系统参数
|
||||
getSystemInfo().then((res) => {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "shortcut icon";
|
||||
link.href = res.data.logo;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'shortcut icon'
|
||||
link.href = res.data.logo
|
||||
document.head.appendChild(link)
|
||||
})
|
||||
if (!isChrome() && !isMobile()) {
|
||||
showMessageInfo("建议使用 Chrome 浏览器以获得最佳体验。");
|
||||
showMessageInfo('建议使用 Chrome 浏览器以获得最佳体验。')
|
||||
}
|
||||
|
||||
checkSession()
|
||||
.then(() => {
|
||||
store.setIsLogin(true);
|
||||
store.setIsLogin(true)
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {})
|
||||
|
||||
// 设置主题
|
||||
document.documentElement.setAttribute("data-theme", store.theme);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => store.isLogin,
|
||||
(val) => {
|
||||
if (val) {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handler = ref(0);
|
||||
// 初始化 websocket 连接
|
||||
const connect = () => {
|
||||
let host = process.env.VUE_APP_WS_HOST;
|
||||
if (host === "") {
|
||||
if (location.protocol === "https:") {
|
||||
host = "wss://" + location.host;
|
||||
} else {
|
||||
host = "ws://" + location.host;
|
||||
}
|
||||
}
|
||||
const clientId = getClientId();
|
||||
const _socket = new WebSocket(host + `/api/ws?client_id=${clientId}`, ["token", getUserToken()]);
|
||||
_socket.addEventListener("open", () => {
|
||||
console.log("WebSocket 已连接");
|
||||
handler.value = setInterval(() => {
|
||||
if (_socket.readyState === WebSocket.OPEN) {
|
||||
_socket.send(JSON.stringify({ type: "ping" }));
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
_socket.addEventListener("close", () => {
|
||||
clearInterval(handler.value);
|
||||
connect();
|
||||
});
|
||||
store.setSocket(_socket);
|
||||
};
|
||||
document.documentElement.setAttribute('data-theme', store.theme)
|
||||
})
|
||||
|
||||
// 打印 banner
|
||||
const banner = `
|
||||
|
||||
.oooooo. oooo .o. ooooo
|
||||
d8P' 'Y8b 888 .888. 888
|
||||
888 .ooooo. .ooooo. 888 oooo .8"888. 888
|
||||
@@ -103,18 +64,18 @@ const banner = `
|
||||
888 ooooo 888ooo888 888ooo888 888888. .88ooo8888. 888
|
||||
'88. .88' 888 .o 888 .o 888 88b. .8' 888. 888
|
||||
Y8bood8P' Y8bod8P' Y8bod8P' o888o o888o o88o o8888o o888o
|
||||
`
|
||||
console.log('%c' + banner + '', 'color: purple;font-size: 18px;')
|
||||
|
||||
`;
|
||||
console.log("%c" + banner + "", "color: purple;font-size: 18px;");
|
||||
|
||||
console.log("%c感谢大家为 GeekAI 做出的卓越贡献!", "color: green;font-size: 40px;font-family: '微软雅黑';");
|
||||
console.log(
|
||||
"%c项目源码:https://github.com/yangjian102621/geekai %c 您的 star 对我们非常重要!",
|
||||
'%c感谢大家为 GeekAI 做出的卓越贡献!',
|
||||
"color: green;font-size: 40px;font-family: '微软雅黑';"
|
||||
)
|
||||
console.log(
|
||||
'%c项目源码:https://github.com/yangjian102621/geekai %c 您的 star 对我们非常重要!',
|
||||
"color: green;font-size: 20px;font-family: '微软雅黑';",
|
||||
"color: red;font-size: 20px;font-family: '微软雅黑';"
|
||||
);
|
||||
|
||||
console.log("%c 愿你出走半生,归来仍是少年!大奉武夫许七安,前来凿阵!", "color: #7c39ed;font-size: 18px;font-family: '微软雅黑';");
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@@ -173,6 +134,4 @@ html, body {
|
||||
background #D6FBCC
|
||||
color #07C160
|
||||
}
|
||||
|
||||
@import '@/assets/iconfont/iconfont.css'
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
.chat-page {
|
||||
height: 100%;
|
||||
:deep (.el-message-box__message){
|
||||
:deep(.el-message-box__message){
|
||||
font-size: 18px !important
|
||||
}
|
||||
.newChat{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-if="files && files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
@@ -29,7 +29,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="content position-relative">
|
||||
<div v-html="content"></div>
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"
|
||||
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
|
||||
@@ -47,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-if="files && files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
@@ -71,7 +73,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="content position-relative">
|
||||
<div v-html="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"
|
||||
@@ -86,7 +90,6 @@
|
||||
|
||||
<script setup>
|
||||
import { FormatFileSize, GetFileIcon, GetFileType } from '@/store/system'
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { dateFormat, isImage, processPrompt } from '@/utils/libs'
|
||||
import { Clock } from '@element-plus/icons-vue'
|
||||
import hl from 'highlight.js'
|
||||
@@ -111,7 +114,7 @@ const md = new MarkdownIt({
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`
|
||||
// 处理代码高亮
|
||||
const preCode = hl.highlight(lang, str, true).value
|
||||
const preCode = hl.highlight(str, { language: lang, ignoreIllegals: true }).value
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
|
||||
}
|
||||
@@ -124,11 +127,15 @@ const md = new MarkdownIt({
|
||||
})
|
||||
md.use(mathjaxPlugin)
|
||||
md.use(emoji)
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {
|
||||
content: '',
|
||||
content: {
|
||||
text: '',
|
||||
files: [],
|
||||
},
|
||||
created_at: '',
|
||||
tokens: 0,
|
||||
model: '',
|
||||
@@ -141,8 +148,11 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
const finalTokens = ref(props.data.tokens)
|
||||
const content = ref(processPrompt(props.data.content))
|
||||
const files = ref([])
|
||||
const content = ref(processPrompt(props.data.content.text))
|
||||
const files = ref(props.data.content.files)
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits(['edit'])
|
||||
|
||||
onMounted(() => {
|
||||
processFiles()
|
||||
@@ -152,38 +162,6 @@ const processFiles = () => {
|
||||
if (!props.data.content) {
|
||||
return
|
||||
}
|
||||
|
||||
// 提取图片|文件链接
|
||||
const linkRegex = /(https?:\/\/\S+)/g
|
||||
const links = props.data.content.match(linkRegex)
|
||||
const urlPrefix = `${window.location.protocol}//${window.location.host}`
|
||||
if (links) {
|
||||
// 把本地链接转换为相对路径
|
||||
const _links = links.map((link) => {
|
||||
if (link.startsWith(urlPrefix)) {
|
||||
return link.replace(urlPrefix, '')
|
||||
}
|
||||
return link
|
||||
})
|
||||
// 合并数组并去重
|
||||
const urls = [...new Set([...links, ..._links])]
|
||||
httpPost('/api/upload/list', { urls: urls })
|
||||
.then((res) => {
|
||||
files.value = res.data.items
|
||||
|
||||
// for (let link of links) {
|
||||
// if (isExternalImg(link, files.value)) {
|
||||
// files.value.push({ url: link, ext: ".png" });
|
||||
// }
|
||||
// }
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
// 替换图片|文件链接
|
||||
for (let link of links) {
|
||||
content.value = content.value.replace(link, '')
|
||||
}
|
||||
}
|
||||
content.value = md.render(content.value.trim())
|
||||
}
|
||||
const isExternalImg = (link, files) => {
|
||||
@@ -475,4 +453,39 @@ const isExternalImg = (link, files) => {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.operations
|
||||
display none
|
||||
position absolute
|
||||
right 5px
|
||||
top 5px
|
||||
|
||||
.text-box
|
||||
&:hover
|
||||
.operations
|
||||
display flex
|
||||
gap 5px
|
||||
|
||||
.op-edit
|
||||
cursor pointer
|
||||
color #409eff
|
||||
font-size 16px
|
||||
|
||||
&:hover
|
||||
color darken(#409eff, 10%)
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content:hover .action-buttons {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<div class="chat-item">
|
||||
<div
|
||||
class="content-wrapper"
|
||||
v-html="md.render(processContent(data.content))"
|
||||
v-if="data.content"
|
||||
v-html="md.render(processContent(data.content.text))"
|
||||
v-if="data.content.text"
|
||||
></div>
|
||||
<div class="content-wrapper flex justify-start items-center" v-else>
|
||||
<span class="mr-2">AI 思考中</span> <Thinking :duration="1.5" />
|
||||
@@ -26,7 +26,7 @@
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-if="!readOnly" class="flex">
|
||||
<span class="bar-item" @click="reGenerate(data.prompt)">
|
||||
<span class="bar-item" @click="reGenerate(data.id)">
|
||||
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-tooltip>
|
||||
@@ -48,18 +48,6 @@
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
<!-- <span class="bar-item">-->
|
||||
<!-- <el-dropdown trigger="click">-->
|
||||
<!-- <span class="el-dropdown-link">-->
|
||||
<!-- <el-icon><More/></el-icon>-->
|
||||
<!-- </span>-->
|
||||
<!-- <template #dropdown>-->
|
||||
<!-- <el-dropdown-menu>-->
|
||||
<!-- <el-dropdown-item :icon="Headset" @click="synthesis(orgContent)">生成语音</el-dropdown-item>-->
|
||||
<!-- </el-dropdown-menu>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-dropdown>-->
|
||||
<!-- </span>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,8 +62,8 @@
|
||||
<div class="content-wrapper">
|
||||
<div
|
||||
class="content"
|
||||
v-html="md.render(processContent(data.content))"
|
||||
v-if="data.content"
|
||||
v-html="md.render(processContent(data.content.text))"
|
||||
v-if="data.content.text"
|
||||
></div>
|
||||
<div class="content flex justify-start items-center" v-else>
|
||||
<span class="mr-2">AI 思考中</span> <Thinking :duration="1.5" />
|
||||
@@ -83,16 +71,15 @@
|
||||
</div>
|
||||
<div class="bar text-gray-500" v-if="data.created_at">
|
||||
<span class="bar-item text-sm"> {{ dateFormat(data.created_at) }}</span>
|
||||
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
|
||||
<span class="bar-item bg">
|
||||
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
|
||||
<el-icon class="copy-reply" :data-clipboard-text="data.content">
|
||||
<el-icon class="copy-reply" :data-clipboard-text="data.content.text">
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-if="!readOnly" class="flex">
|
||||
<span class="bar-item bg" @click="reGenerate(data.prompt)">
|
||||
<span class="bar-item bg" @click="reGenerate(data.id)">
|
||||
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-tooltip>
|
||||
@@ -106,7 +93,7 @@
|
||||
placement="bottom"
|
||||
v-if="!isPlaying"
|
||||
>
|
||||
<i class="iconfont icon-speaker" @click="synthesis(data.content)"></i>
|
||||
<i class="iconfont icon-speaker" @click="synthesis(data.content.text)"></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
@@ -145,7 +132,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: {
|
||||
icon: '',
|
||||
content: '',
|
||||
content: {
|
||||
text: '',
|
||||
files: [],
|
||||
},
|
||||
created_at: '',
|
||||
tokens: 0,
|
||||
},
|
||||
@@ -233,9 +223,8 @@ const stopSynthesis = () => {
|
||||
}
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = (prompt) => {
|
||||
console.log(prompt)
|
||||
emits('regen', prompt)
|
||||
const reGenerate = (messageId) => {
|
||||
emits('regen', messageId)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="config-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:before-close="close"
|
||||
style="max-width: 600px"
|
||||
title="聊天配置"
|
||||
class="config-dialog"
|
||||
v-model="showDialog"
|
||||
:close-on-click-modal="true"
|
||||
:before-close="close"
|
||||
style="max-width: 600px"
|
||||
title="聊天配置"
|
||||
>
|
||||
<div class="chat-setting">
|
||||
<el-form :model="data" label-width="100px" label-position="left">
|
||||
<el-form-item label="聊天样式:">
|
||||
<el-radio-group v-model="data.style" @change="(val) => {store.setChatListStyle(val)}">
|
||||
<el-radio-group
|
||||
v-model="data.style"
|
||||
@change="
|
||||
(val) => {
|
||||
store.setChatListStyle(val)
|
||||
}
|
||||
"
|
||||
>
|
||||
<el-radio value="list">列表样式</el-radio>
|
||||
<el-radio value="chat">对话样式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="流式输出:">
|
||||
<el-switch v-model="data.stream" @change="(val) => {store.setChatStream(val)}" />
|
||||
<el-switch
|
||||
v-model="data.stream"
|
||||
@change="
|
||||
(val) => {
|
||||
store.setChatStream(val)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="语音音色:">
|
||||
<el-select v-model="data.ttsModel" placeholder="请选择语音音色" @change="changeTTSModel">
|
||||
@@ -31,10 +45,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref, onMounted} from "vue"
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import {httpGet} from "@/utils/http";
|
||||
const store = useSharedStore();
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet } from '@/utils/http'
|
||||
const store = useSharedStore()
|
||||
|
||||
const data = ref({
|
||||
style: store.chatListStyle,
|
||||
@@ -44,28 +58,26 @@ const data = ref({
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
});
|
||||
|
||||
const showDialog = computed(() => {
|
||||
return props.show
|
||||
})
|
||||
const emits = defineEmits(['hide']);
|
||||
|
||||
const showDialog = ref(props.show)
|
||||
const emits = defineEmits(['hide'])
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
emits('hide', false)
|
||||
}
|
||||
const models = ref([]);
|
||||
const models = ref([])
|
||||
onMounted(() => {
|
||||
// 获取模型列表
|
||||
httpGet("/api/model/list?type=tts").then((res) => {
|
||||
models.value = res.data;
|
||||
if (!data.ttsModel) {
|
||||
store.setTtsModel(models.value[0].id);
|
||||
httpGet('/api/model/list?type=tts').then((res) => {
|
||||
models.value = res.data
|
||||
if (!data.ttsModel && models.value.length > 0) {
|
||||
store.setTtsModel(models.value[0].id)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const changeTTSModel = (item) => {
|
||||
store.setTtsModel(item);
|
||||
store.setTtsModel(item)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -73,4 +85,4 @@ const changeTTSModel = (item) => {
|
||||
.chat-setting {
|
||||
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -17,45 +17,45 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { showMessageError } from "@/utils/dialog";
|
||||
import { getLicenseInfo, getSystemInfo } from "@/store/cache";
|
||||
import { getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref("");
|
||||
const version = ref(process.env.VUE_APP_VERSION);
|
||||
const gitURL = ref(process.env.VUE_APP_GITHUB_URL);
|
||||
const copyRight = ref("");
|
||||
const icp = ref("");
|
||||
const license = ref({});
|
||||
const title = ref('')
|
||||
const version = ref(import.meta.env.VITE_VERSION)
|
||||
const gitURL = ref(import.meta.env.VITE_GITHUB_URL)
|
||||
const copyRight = ref('')
|
||||
const icp = ref('')
|
||||
const license = ref({})
|
||||
const props = defineProps({
|
||||
textColor: {
|
||||
type: String,
|
||||
default: "#ffffff"
|
||||
}
|
||||
});
|
||||
default: '#ffffff',
|
||||
},
|
||||
})
|
||||
|
||||
// 获取系统配置
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
title.value = res.data.title ?? process.env.VUE_APP_TITLE;
|
||||
title.value = res.data.title ?? import.meta.env.VITE_TITLE
|
||||
copyRight.value =
|
||||
(res.data.copyright ? res.data.copyright : "极客学长") +
|
||||
" © 2023 - " +
|
||||
(res.data.copyright ? res.data.copyright : '极客学长') +
|
||||
' © 2023 - ' +
|
||||
new Date().getFullYear() +
|
||||
" All rights reserved";
|
||||
icp.value = res.data.icp;
|
||||
' All rights reserved'
|
||||
icp.value = res.data.icp
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取系统配置失败:" + e.message);
|
||||
});
|
||||
showMessageError('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
license.value = res.data;
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取 License 失败:" + e.message);
|
||||
});
|
||||
showMessageError('获取 License 失败:' + e.message)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
|
||||
@@ -13,7 +13,14 @@
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off">
|
||||
<el-input
|
||||
placeholder="请输入密码(8-16位)"
|
||||
maxlength="16"
|
||||
size="large"
|
||||
v-model="data.password"
|
||||
show-password
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock />
|
||||
@@ -24,7 +31,9 @@
|
||||
|
||||
<el-row class="btn-row" :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-button class="login-btn" type="primary" size="large" @click="submitLogin">登 录</el-button>
|
||||
<el-button class="login-btn" type="primary" size="large" @click="submitLogin"
|
||||
>登 录</el-button
|
||||
>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -33,7 +42,9 @@
|
||||
还没有账号?
|
||||
<el-button size="small" @click="login = false">注册</el-button>
|
||||
|
||||
<el-button type="info" class="forget" size="small" @click="showResetPass = true">忘记密码?</el-button>
|
||||
<el-button type="info" class="forget" size="small" @click="showResetPass = true"
|
||||
>忘记密码?</el-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="wechatLoginURL !== ''">
|
||||
<el-divider>
|
||||
@@ -42,7 +53,11 @@
|
||||
<div class="c-login flex justify-center">
|
||||
<div class="p-2 w-full">
|
||||
<a :href="wechatLoginURL">
|
||||
<el-button type="success" class="w-full" size="large" @click="setRoute(router.currentRoute.value.path)"
|
||||
<el-button
|
||||
type="success"
|
||||
class="w-full"
|
||||
size="large"
|
||||
@click="setRoute(router.currentRoute.value.path)"
|
||||
><i class="iconfont icon-wechat mr-2"></i> 微信登录
|
||||
</el-button>
|
||||
</a>
|
||||
@@ -58,7 +73,13 @@
|
||||
<el-tabs v-model="activeName" class="demo-tabs">
|
||||
<el-tab-pane label="手机注册" name="mobile" v-if="enableMobile">
|
||||
<div class="block">
|
||||
<el-input placeholder="手机号码" size="large" v-model="data.mobile" maxlength="11" autocomplete="off">
|
||||
<el-input
|
||||
placeholder="手机号码"
|
||||
size="large"
|
||||
v-model="data.mobile"
|
||||
maxlength="11"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Iphone />
|
||||
@@ -69,7 +90,13 @@
|
||||
<div class="block">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-input placeholder="验证码" size="large" maxlength="30" v-model="data.code" autocomplete="off">
|
||||
<el-input
|
||||
placeholder="验证码"
|
||||
size="large"
|
||||
maxlength="30"
|
||||
v-model="data.code"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Checked />
|
||||
@@ -96,7 +123,13 @@
|
||||
<div class="block">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-input placeholder="验证码" size="large" maxlength="30" v-model="data.code" autocomplete="off">
|
||||
<el-input
|
||||
placeholder="验证码"
|
||||
size="large"
|
||||
maxlength="30"
|
||||
v-model="data.code"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Checked />
|
||||
@@ -112,7 +145,12 @@
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="用户名注册" name="username" v-if="enableUser">
|
||||
<div class="block">
|
||||
<el-input placeholder="用户名" size="large" v-model="data.username" autocomplete="off">
|
||||
<el-input
|
||||
placeholder="用户名"
|
||||
size="large"
|
||||
v-model="data.username"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Iphone />
|
||||
@@ -124,7 +162,14 @@
|
||||
</el-tabs>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off">
|
||||
<el-input
|
||||
placeholder="请输入密码(8-16位)"
|
||||
maxlength="16"
|
||||
size="large"
|
||||
v-model="data.password"
|
||||
show-password
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock />
|
||||
@@ -134,7 +179,14 @@
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="重复密码(8-16位)" size="large" maxlength="16" v-model="data.repass" show-password autocomplete="off">
|
||||
<el-input
|
||||
placeholder="重复密码(8-16位)"
|
||||
size="large"
|
||||
maxlength="16"
|
||||
v-model="data.repass"
|
||||
show-password
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock />
|
||||
@@ -144,7 +196,12 @@
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="邀请码(可选)" size="large" v-model="data.invite_code" autocomplete="off">
|
||||
<el-input
|
||||
placeholder="邀请码(可选)"
|
||||
size="large"
|
||||
v-model="data.invite_code"
|
||||
autocomplete="off"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Message />
|
||||
@@ -154,7 +211,9 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<el-button class="login-btn w-full" type="primary" size="large" @click="submitRegister">注 册</el-button>
|
||||
<el-button class="login-btn w-full" type="primary" size="large" @click="submitRegister"
|
||||
>注 册</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="text text-sm flex justify-center items-center w-full pt-3">
|
||||
@@ -188,193 +247,193 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { setUserToken } from "@/store/session";
|
||||
import { validateEmail, validateMobile } from "@/utils/validate";
|
||||
import { Checked, Close, Iphone, Lock, Message } from "@element-plus/icons-vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import { arrayContains } from "@/utils/libs";
|
||||
import { getSystemInfo } from "@/store/cache";
|
||||
import Captcha from "@/components/Captcha.vue";
|
||||
import ResetPass from "@/components/ResetPass.vue";
|
||||
import { setRoute } from "@/store/system";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import Captcha from '@/components/Captcha.vue'
|
||||
import ResetPass from '@/components/ResetPass.vue'
|
||||
import SendMsg from '@/components/SendMsg.vue'
|
||||
import { getSystemInfo } from '@/store/cache'
|
||||
import { setUserToken } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { setRoute } from '@/store/system'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { arrayContains } from '@/utils/libs'
|
||||
import { validateEmail, validateMobile } from '@/utils/validate'
|
||||
import { Checked, Iphone, Lock, Message } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
});
|
||||
const showDialog = ref(false);
|
||||
})
|
||||
const showDialog = ref(false)
|
||||
watch(
|
||||
() => props.show,
|
||||
(newValue) => {
|
||||
showDialog.value = newValue;
|
||||
showDialog.value = newValue
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const login = ref(true);
|
||||
const login = ref(true)
|
||||
const data = ref({
|
||||
username: process.env.VUE_APP_USER,
|
||||
password: process.env.VUE_APP_PASS,
|
||||
mobile: "",
|
||||
email: "",
|
||||
repass: "",
|
||||
code: "",
|
||||
invite_code: "",
|
||||
});
|
||||
const enableMobile = ref(false);
|
||||
const enableEmail = ref(false);
|
||||
const enableUser = ref(false);
|
||||
const enableRegister = ref(true);
|
||||
const wechatLoginURL = ref("");
|
||||
const activeName = ref("");
|
||||
const wxImg = ref("/images/wx.png");
|
||||
const captchaRef = ref(null);
|
||||
username: import.meta.env.VITE_USER,
|
||||
password: import.meta.env.VITE_PASS,
|
||||
mobile: '',
|
||||
email: '',
|
||||
repass: '',
|
||||
code: '',
|
||||
invite_code: '',
|
||||
})
|
||||
const enableMobile = ref(false)
|
||||
const enableEmail = ref(false)
|
||||
const enableUser = ref(false)
|
||||
const enableRegister = ref(true)
|
||||
const wechatLoginURL = ref('')
|
||||
const activeName = ref('')
|
||||
const wxImg = ref('/images/wx.png')
|
||||
const captchaRef = ref(null)
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(["hide", "success"]);
|
||||
const action = ref("login");
|
||||
const enableVerify = ref(false);
|
||||
const showResetPass = ref(false);
|
||||
const router = useRouter();
|
||||
const store = useSharedStore();
|
||||
const emits = defineEmits(['hide', 'success'])
|
||||
const action = ref('login')
|
||||
const enableVerify = ref(false)
|
||||
const showResetPass = ref(false)
|
||||
const router = useRouter()
|
||||
const store = useSharedStore()
|
||||
|
||||
onMounted(() => {
|
||||
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`;
|
||||
httpGet("/api/user/clogin?return_url=" + returnURL)
|
||||
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
|
||||
httpGet('/api/user/clogin?return_url=' + returnURL)
|
||||
.then((res) => {
|
||||
wechatLoginURL.value = res.data.url;
|
||||
wechatLoginURL.value = res.data.url
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e.message);
|
||||
});
|
||||
console.log(e.message)
|
||||
})
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const registerWays = res.data["register_ways"];
|
||||
if (arrayContains(registerWays, "username")) {
|
||||
enableUser.value = true;
|
||||
activeName.value = "username";
|
||||
const registerWays = res.data['register_ways']
|
||||
if (arrayContains(registerWays, 'username')) {
|
||||
enableUser.value = true
|
||||
activeName.value = 'username'
|
||||
}
|
||||
if (arrayContains(registerWays, "email")) {
|
||||
enableEmail.value = true;
|
||||
activeName.value = "email";
|
||||
if (arrayContains(registerWays, 'email')) {
|
||||
enableEmail.value = true
|
||||
activeName.value = 'email'
|
||||
}
|
||||
if (arrayContains(registerWays, "mobile")) {
|
||||
enableMobile.value = true;
|
||||
activeName.value = "mobile";
|
||||
if (arrayContains(registerWays, 'mobile')) {
|
||||
enableMobile.value = true
|
||||
activeName.value = 'mobile'
|
||||
}
|
||||
// 是否启用注册
|
||||
enableRegister.value = res.data["enabled_register"];
|
||||
enableRegister.value = res.data['enabled_register']
|
||||
// 使用后台上传的客服微信二维码
|
||||
if (res.data["wechat_card_url"] !== "") {
|
||||
wxImg.value = res.data["wechat_card_url"];
|
||||
if (res.data['wechat_card_url'] !== '') {
|
||||
wxImg.value = res.data['wechat_card_url']
|
||||
}
|
||||
enableVerify.value = res.data["enabled_verify"];
|
||||
enableVerify.value = res.data['enabled_verify']
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
const submit = (verifyData) => {
|
||||
if (action.value === "login") {
|
||||
doLogin(verifyData);
|
||||
} else if (action.value === "register") {
|
||||
doRegister(verifyData);
|
||||
if (action.value === 'login') {
|
||||
doLogin(verifyData)
|
||||
} else if (action.value === 'register') {
|
||||
doRegister(verifyData)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 登录操作
|
||||
const submitLogin = () => {
|
||||
if (!data.value.username) {
|
||||
return ElMessage.error("请输入用户名");
|
||||
return ElMessage.error('请输入用户名')
|
||||
}
|
||||
if (!data.value.password) {
|
||||
return ElMessage.error("请输入密码");
|
||||
return ElMessage.error('请输入密码')
|
||||
}
|
||||
if (enableVerify.value) {
|
||||
captchaRef.value.loadCaptcha();
|
||||
action.value = "login";
|
||||
captchaRef.value.loadCaptcha()
|
||||
action.value = 'login'
|
||||
} else {
|
||||
doLogin({});
|
||||
doLogin({})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const doLogin = (verifyData) => {
|
||||
data.value.key = verifyData.key;
|
||||
data.value.dots = verifyData.dots;
|
||||
data.value.x = verifyData.x;
|
||||
httpPost("/api/user/login", data.value)
|
||||
data.value.key = verifyData.key
|
||||
data.value.dots = verifyData.dots
|
||||
data.value.x = verifyData.x
|
||||
httpPost('/api/user/login', data.value)
|
||||
.then((res) => {
|
||||
setUserToken(res.data.token);
|
||||
store.setIsLogin(true);
|
||||
ElMessage.success("登录成功!");
|
||||
emits("hide");
|
||||
emits("success");
|
||||
setUserToken(res.data.token)
|
||||
store.setIsLogin(true)
|
||||
ElMessage.success('登录成功!')
|
||||
emits('hide')
|
||||
emits('success')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("登录失败," + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 注册操作
|
||||
const submitRegister = () => {
|
||||
if (activeName.value === "username" && data.value.username === "") {
|
||||
return ElMessage.error("请输入用户名");
|
||||
if (activeName.value === 'username' && data.value.username === '') {
|
||||
return ElMessage.error('请输入用户名')
|
||||
}
|
||||
|
||||
if (activeName.value === "mobile" && !validateMobile(data.value.mobile)) {
|
||||
return ElMessage.error("请输入合法的手机号");
|
||||
if (activeName.value === 'mobile' && !validateMobile(data.value.mobile)) {
|
||||
return ElMessage.error('请输入合法的手机号')
|
||||
}
|
||||
|
||||
if (activeName.value === "email" && !validateEmail(data.value.email)) {
|
||||
return ElMessage.error("请输入合法的邮箱地址");
|
||||
if (activeName.value === 'email' && !validateEmail(data.value.email)) {
|
||||
return ElMessage.error('请输入合法的邮箱地址')
|
||||
}
|
||||
|
||||
if (data.value.password.length < 8) {
|
||||
return ElMessage.error("密码的长度为8-16个字符");
|
||||
return ElMessage.error('密码的长度为8-16个字符')
|
||||
}
|
||||
if (data.value.repass !== data.value.password) {
|
||||
return ElMessage.error("两次输入密码不一致");
|
||||
return ElMessage.error('两次输入密码不一致')
|
||||
}
|
||||
|
||||
if ((activeName.value === "mobile" || activeName.value === "email") && data.value.code === "") {
|
||||
return ElMessage.error("请输入验证码");
|
||||
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
|
||||
return ElMessage.error('请输入验证码')
|
||||
}
|
||||
if (enableVerify.value && activeName.value === "username") {
|
||||
captchaRef.value.loadCaptcha();
|
||||
action.value = "register";
|
||||
if (enableVerify.value && activeName.value === 'username') {
|
||||
captchaRef.value.loadCaptcha()
|
||||
action.value = 'register'
|
||||
} else {
|
||||
doRegister({});
|
||||
doRegister({})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const doRegister = (verifyData) => {
|
||||
data.value.key = verifyData.key;
|
||||
data.value.dots = verifyData.dots;
|
||||
data.value.x = verifyData.x;
|
||||
data.value.reg_way = activeName.value;
|
||||
httpPost("/api/user/register", data.value)
|
||||
data.value.key = verifyData.key
|
||||
data.value.dots = verifyData.dots
|
||||
data.value.x = verifyData.x
|
||||
data.value.reg_way = activeName.value
|
||||
httpPost('/api/user/register', data.value)
|
||||
.then((res) => {
|
||||
setUserToken(res.data.token);
|
||||
setUserToken(res.data.token)
|
||||
ElMessage.success({
|
||||
message: "注册成功!",
|
||||
message: '注册成功!',
|
||||
onClose: () => {
|
||||
emits("hide");
|
||||
emits("success");
|
||||
emits('hide')
|
||||
emits('success')
|
||||
},
|
||||
duration: 1000,
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("注册失败," + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('注册失败,' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
@@ -38,11 +38,7 @@
|
||||
<div class="call-controls">
|
||||
<el-tooltip content="长按发送语音" placement="top">
|
||||
<ripple-button>
|
||||
<button
|
||||
class="call-button answer"
|
||||
@mousedown="startRecording"
|
||||
@mouseup="stopRecording"
|
||||
>
|
||||
<button class="call-button answer" @mousedown="startRecording" @mouseup="stopRecording">
|
||||
<i class="iconfont icon-mic-bold"></i>
|
||||
</button>
|
||||
</ripple-button>
|
||||
@@ -58,159 +54,155 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RippleButton from "@/components/ui/RippleButton.vue";
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { RealtimeClient } from "@openai/realtime-api-beta";
|
||||
import { WavRecorder, WavStreamPlayer } from "@/lib/wavtools/index.js";
|
||||
import { instructions } from "@/utils/conversation_config.js";
|
||||
import { WavRenderer } from "@/utils/wav_renderer";
|
||||
import { showMessageError } from "@/utils/dialog";
|
||||
import { getUserToken } from "@/store/session";
|
||||
import RippleButton from '@/components/ui/RippleButton.vue'
|
||||
import { WavRecorder, WavStreamPlayer } from '@/lib/wavtools/index.js'
|
||||
import { getUserToken } from '@/store/session'
|
||||
import { instructions } from '@/utils/conversation_config.js'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { WavRenderer } from '@/utils/wav_renderer'
|
||||
import { RealtimeClient } from '@openai/realtime-api-beta'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars,no-undef
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: "100vh"
|
||||
}
|
||||
});
|
||||
default: '100vh',
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line no-undef
|
||||
const emits = defineEmits(["close"]);
|
||||
const emits = defineEmits(['close'])
|
||||
|
||||
/********************** connection animation code *************************/
|
||||
const fullText = "正在接通中...";
|
||||
const connectingText = ref("");
|
||||
let index = 0;
|
||||
const fullText = '正在接通中...'
|
||||
const connectingText = ref('')
|
||||
let index = 0
|
||||
const typeText = () => {
|
||||
if (index < fullText.length) {
|
||||
connectingText.value += fullText[index];
|
||||
index++;
|
||||
setTimeout(typeText, 200); // 每300毫秒显示一个字
|
||||
connectingText.value += fullText[index]
|
||||
index++
|
||||
setTimeout(typeText, 200) // 每300毫秒显示一个字
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
connectingText.value = "";
|
||||
index = 0;
|
||||
typeText();
|
||||
}, 1000); // 等待1秒后重新开始
|
||||
connectingText.value = ''
|
||||
index = 0
|
||||
typeText()
|
||||
}, 1000) // 等待1秒后重新开始
|
||||
}
|
||||
};
|
||||
}
|
||||
/*************************** end of code ****************************************/
|
||||
|
||||
/********************** conversation process code ***************************/
|
||||
const leftVoiceActive = ref(false);
|
||||
const rightVoiceActive = ref(false);
|
||||
const leftVoiceActive = ref(false)
|
||||
const rightVoiceActive = ref(false)
|
||||
|
||||
const animateVoice = () => {
|
||||
leftVoiceActive.value = Math.random() > 0.5;
|
||||
rightVoiceActive.value = Math.random() > 0.5;
|
||||
};
|
||||
leftVoiceActive.value = Math.random() > 0.5
|
||||
rightVoiceActive.value = Math.random() > 0.5
|
||||
}
|
||||
|
||||
const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 }));
|
||||
const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 }));
|
||||
let host = process.env.VUE_APP_WS_HOST;
|
||||
if (host === "") {
|
||||
if (location.protocol === "https:") {
|
||||
host = "wss://" + location.host;
|
||||
const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 }))
|
||||
const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 }))
|
||||
let host = import.meta.env.VITE_WS_HOST
|
||||
if (host === '') {
|
||||
if (location.protocol === 'https:') {
|
||||
host = 'wss://' + location.host
|
||||
} else {
|
||||
host = "ws://" + location.host;
|
||||
host = 'ws://' + location.host
|
||||
}
|
||||
}
|
||||
const client = ref(
|
||||
new RealtimeClient({
|
||||
url: `${host}/api/realtime`,
|
||||
apiKey: getUserToken(),
|
||||
dangerouslyAllowAPIKeyInBrowser: true
|
||||
dangerouslyAllowAPIKeyInBrowser: true,
|
||||
})
|
||||
);
|
||||
)
|
||||
// // Set up client instructions and transcription
|
||||
client.value.updateSession({
|
||||
instructions: instructions,
|
||||
turn_detection: null,
|
||||
input_audio_transcription: { model: "whisper-1" },
|
||||
voice: "alloy"
|
||||
});
|
||||
input_audio_transcription: { model: 'whisper-1' },
|
||||
voice: 'alloy',
|
||||
})
|
||||
|
||||
// set voice wave canvas
|
||||
const clientCanvasRef = ref(null);
|
||||
const serverCanvasRef = ref(null);
|
||||
const isConnected = ref(false);
|
||||
const isRecording = ref(false);
|
||||
const backgroundAudio = ref(null);
|
||||
const hangUpAudio = ref(null);
|
||||
const clientCanvasRef = ref(null)
|
||||
const serverCanvasRef = ref(null)
|
||||
const isConnected = ref(false)
|
||||
const isRecording = ref(false)
|
||||
const backgroundAudio = ref(null)
|
||||
const hangUpAudio = ref(null)
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
const connect = async () => {
|
||||
if (isConnected.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
// 播放背景音乐
|
||||
if (backgroundAudio.value) {
|
||||
backgroundAudio.value.play().catch((error) => {
|
||||
console.error("播放失败,可能是浏览器的自动播放策略导致的:", error);
|
||||
});
|
||||
console.error('播放失败,可能是浏览器的自动播放策略导致的:', error)
|
||||
})
|
||||
}
|
||||
// 模拟拨号延时
|
||||
await sleep(3000);
|
||||
await sleep(3000)
|
||||
try {
|
||||
await client.value.connect();
|
||||
await wavRecorder.value.begin();
|
||||
await wavStreamPlayer.value.connect();
|
||||
console.log("对话连接成功!");
|
||||
await client.value.connect()
|
||||
await wavRecorder.value.begin()
|
||||
await wavStreamPlayer.value.connect()
|
||||
console.log('对话连接成功!')
|
||||
if (!client.value.isConnected()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
isConnected.value = true;
|
||||
backgroundAudio.value?.pause();
|
||||
backgroundAudio.value.currentTime = 0;
|
||||
isConnected.value = true
|
||||
backgroundAudio.value?.pause()
|
||||
backgroundAudio.value.currentTime = 0
|
||||
client.value.sendUserMessageContent([
|
||||
{
|
||||
type: "input_text",
|
||||
text: "你好,我是极客学长!"
|
||||
}
|
||||
]);
|
||||
if (client.value.getTurnDetectionType() === "server_vad") {
|
||||
await wavRecorder.value.record((data) =>
|
||||
client.value.appendInputAudio(data.mono)
|
||||
);
|
||||
type: 'input_text',
|
||||
text: '你好,我是极客学长!',
|
||||
},
|
||||
])
|
||||
if (client.value.getTurnDetectionType() === 'server_vad') {
|
||||
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 开始语音输入
|
||||
const startRecording = async () => {
|
||||
if (isRecording.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
isRecording.value = true;
|
||||
isRecording.value = true
|
||||
try {
|
||||
const trackSampleOffset = await wavStreamPlayer.value.interrupt();
|
||||
const trackSampleOffset = await wavStreamPlayer.value.interrupt()
|
||||
if (trackSampleOffset?.trackId) {
|
||||
const { trackId, offset } = trackSampleOffset;
|
||||
client.value.cancelResponse(trackId, offset);
|
||||
const { trackId, offset } = trackSampleOffset
|
||||
client.value.cancelResponse(trackId, offset)
|
||||
}
|
||||
await wavRecorder.value.record((data) =>
|
||||
client.value.appendInputAudio(data.mono)
|
||||
);
|
||||
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono))
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 结束语音输入
|
||||
const stopRecording = async () => {
|
||||
try {
|
||||
isRecording.value = false;
|
||||
await wavRecorder.value.pause();
|
||||
client.value.createResponse();
|
||||
isRecording.value = false
|
||||
await wavRecorder.value.pause()
|
||||
client.value.createResponse()
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// const changeTurnEndType = async (value) => {
|
||||
// if (value === 'none' && wavRecorder.value.getStatus() === 'recording') {
|
||||
@@ -228,111 +220,110 @@ const stopRecording = async () => {
|
||||
// 初始化 WaveRecorder 组件和 RealtimeClient 事件处理
|
||||
const initialize = async () => {
|
||||
// Set up render loops for the visualization canvas
|
||||
let isLoaded = true;
|
||||
let isLoaded = true
|
||||
const render = () => {
|
||||
if (isLoaded) {
|
||||
if (clientCanvasRef.value) {
|
||||
const canvas = clientCanvasRef.value;
|
||||
const canvas = clientCanvasRef.value
|
||||
if (!canvas.width || !canvas.height) {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
canvas.width = canvas.offsetWidth
|
||||
canvas.height = canvas.offsetHeight
|
||||
}
|
||||
const ctx = canvas.getContext("2d");
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
const result = wavRecorder.value.recording
|
||||
? wavRecorder.value.getFrequencies("voice")
|
||||
: { values: new Float32Array([0]) };
|
||||
WavRenderer.drawBars(canvas, ctx, result.values, "#0099ff", 10, 0, 8);
|
||||
? wavRecorder.value.getFrequencies('voice')
|
||||
: { values: new Float32Array([0]) }
|
||||
WavRenderer.drawBars(canvas, ctx, result.values, '#0099ff', 10, 0, 8)
|
||||
}
|
||||
}
|
||||
if (serverCanvasRef.value) {
|
||||
const canvas = serverCanvasRef.value;
|
||||
const canvas = serverCanvasRef.value
|
||||
if (!canvas.width || !canvas.height) {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
canvas.width = canvas.offsetWidth
|
||||
canvas.height = canvas.offsetHeight
|
||||
}
|
||||
const ctx = canvas.getContext("2d");
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
const result = wavStreamPlayer.value.analyser
|
||||
? wavStreamPlayer.value.getFrequencies("voice")
|
||||
: { values: new Float32Array([0]) };
|
||||
WavRenderer.drawBars(canvas, ctx, result.values, "#009900", 10, 0, 8);
|
||||
? wavStreamPlayer.value.getFrequencies('voice')
|
||||
: { values: new Float32Array([0]) }
|
||||
WavRenderer.drawBars(canvas, ctx, result.values, '#009900', 10, 0, 8)
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(render);
|
||||
requestAnimationFrame(render)
|
||||
}
|
||||
};
|
||||
render();
|
||||
}
|
||||
render()
|
||||
|
||||
client.value.on("error", (event) => {
|
||||
showMessageError(event.error);
|
||||
});
|
||||
client.value.on('error', (event) => {
|
||||
showMessageError(event.error)
|
||||
})
|
||||
|
||||
client.value.on("realtime.event", (re) => {
|
||||
if (re.event.type === "error") {
|
||||
showMessageError(re.event.error);
|
||||
client.value.on('realtime.event', (re) => {
|
||||
if (re.event.type === 'error') {
|
||||
showMessageError(re.event.error)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
client.value.on("conversation.interrupted", async () => {
|
||||
const trackSampleOffset = await wavStreamPlayer.value.interrupt();
|
||||
client.value.on('conversation.interrupted', async () => {
|
||||
const trackSampleOffset = await wavStreamPlayer.value.interrupt()
|
||||
if (trackSampleOffset?.trackId) {
|
||||
const { trackId, offset } = trackSampleOffset;
|
||||
client.value.cancelResponse(trackId, offset);
|
||||
const { trackId, offset } = trackSampleOffset
|
||||
client.value.cancelResponse(trackId, offset)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
client.value.on("conversation.updated", async ({ item, delta }) => {
|
||||
client.value.on('conversation.updated', async ({ item, delta }) => {
|
||||
// console.log('item updated', item, delta)
|
||||
if (delta?.audio) {
|
||||
wavStreamPlayer.value.add16BitPCM(delta.audio, item.id);
|
||||
wavStreamPlayer.value.add16BitPCM(delta.audio, item.id)
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const voiceInterval = ref(null);
|
||||
const voiceInterval = ref(null)
|
||||
onMounted(() => {
|
||||
initialize();
|
||||
initialize()
|
||||
// 启动聊天进行中的动画
|
||||
voiceInterval.value = setInterval(animateVoice, 200);
|
||||
typeText();
|
||||
});
|
||||
voiceInterval.value = setInterval(animateVoice, 200)
|
||||
typeText()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(voiceInterval.value);
|
||||
client.value.reset();
|
||||
});
|
||||
clearInterval(voiceInterval.value)
|
||||
client.value.reset()
|
||||
})
|
||||
|
||||
// 挂断通话
|
||||
const hangUp = async () => {
|
||||
try {
|
||||
isConnected.value = false;
|
||||
isConnected.value = false
|
||||
// 停止播放拨号音乐
|
||||
if (backgroundAudio.value?.currentTime) {
|
||||
backgroundAudio.value?.pause();
|
||||
backgroundAudio.value.currentTime = 0;
|
||||
backgroundAudio.value?.pause()
|
||||
backgroundAudio.value.currentTime = 0
|
||||
}
|
||||
// 断开客户端的连接
|
||||
client.value.reset();
|
||||
client.value.reset()
|
||||
// 中断语音输入和输出服务
|
||||
await wavRecorder.value.end();
|
||||
await wavStreamPlayer.value.interrupt();
|
||||
await wavRecorder.value.end()
|
||||
await wavStreamPlayer.value.interrupt()
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
} finally {
|
||||
// 播放挂断音乐
|
||||
hangUpAudio.value?.play();
|
||||
emits("close");
|
||||
hangUpAudio.value?.play()
|
||||
emits('close')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
defineExpose({ connect, hangUp });
|
||||
defineExpose({ connect, hangUp })
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
|
||||
@import "@/assets/css/realtime.styl"
|
||||
<style lang="stylus" scoped>
|
||||
@import "../assets/css/realtime.styl"
|
||||
</style>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<el-progress type="circle" :percentage="item.progress" :width="100" color="#47fff1" />
|
||||
</div>
|
||||
</div>
|
||||
<el-image fit="cover" v-else>
|
||||
<el-image fit="cover" class="w-full h-full" v-else>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<i class="iconfont icon-quick-start"></i>
|
||||
<div class="image-slot flex flex-col justify-center items-center w-full h-full">
|
||||
<i class="iconfont icon-quick-start text-2xl mb-2"></i>
|
||||
<span>任务正在排队中</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,7 +22,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
@@ -30,9 +30,9 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "~@/assets/css/running-job-list.styl"
|
||||
<style lang="stylus" scoped>
|
||||
@import "../assets/css/running-job-list.styl"
|
||||
</style>
|
||||
|
||||
@@ -58,63 +58,63 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {getSystemInfo} from "@/store/cache";
|
||||
import { getSystemInfo } from '@/store/cache'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE);
|
||||
const version = ref(process.env.VUE_APP_VERSION);
|
||||
const title = ref(import.meta.env.VITE_TITLE)
|
||||
const version = ref(import.meta.env.VITE_VERSION)
|
||||
|
||||
const samples = ref([
|
||||
"用小学生都能听懂的术语解释什么是量子纠缠",
|
||||
"能给一位6岁男孩的生日会提供一些创造性的建议吗?",
|
||||
"如何用 Go 语言实现支持代理 Http client 请求?"
|
||||
]);
|
||||
'用小学生都能听懂的术语解释什么是量子纠缠',
|
||||
'能给一位6岁男孩的生日会提供一些创造性的建议吗?',
|
||||
'如何用 Go 语言实现支持代理 Http client 请求?',
|
||||
])
|
||||
|
||||
const plugins = ref([
|
||||
{
|
||||
value: "今日早报",
|
||||
text: "今日早报:获取当天全球的热门新闻事件列表"
|
||||
value: '今日早报',
|
||||
text: '今日早报:获取当天全球的热门新闻事件列表',
|
||||
},
|
||||
{
|
||||
value: "微博热搜",
|
||||
text: "微博热搜:新浪微博热搜榜,微博当日热搜榜单"
|
||||
value: '微博热搜',
|
||||
text: '微博热搜:新浪微博热搜榜,微博当日热搜榜单',
|
||||
},
|
||||
{
|
||||
value: "今日头条",
|
||||
text: "今日头条:给用户推荐当天的头条新闻,周榜热文"
|
||||
}
|
||||
]);
|
||||
value: '今日头条',
|
||||
text: '今日头条:给用户推荐当天的头条新闻,周榜热文',
|
||||
},
|
||||
])
|
||||
|
||||
const capabilities = ref([
|
||||
{
|
||||
text: "轻松扮演翻译专家,程序员,AI 女友,文案高手...",
|
||||
value: ""
|
||||
text: '轻松扮演翻译专家,程序员,AI 女友,文案高手...',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
text: "国产大语言模型支持,百度文心,科大讯飞,ChatGLM...",
|
||||
value: ""
|
||||
text: '国产大语言模型支持,百度文心,科大讯飞,ChatGLM...',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
text: "绘画:马斯克开拖拉机,20世纪,中国农村。3:2",
|
||||
value: "绘画:马斯克开拖拉机,20世纪,中国农村。3:2"
|
||||
}
|
||||
]);
|
||||
text: '绘画:马斯克开拖拉机,20世纪,中国农村。3:2',
|
||||
value: '绘画:马斯克开拖拉机,20世纪,中国农村。3:2',
|
||||
},
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
title.value = res.data.title;
|
||||
title.value = res.data.title
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
const emits = defineEmits(["send"]);
|
||||
const emits = defineEmits(['send'])
|
||||
const send = (text) => {
|
||||
emits("send", text);
|
||||
};
|
||||
emits('send', text)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="stylus">
|
||||
.welcome {
|
||||
|
||||
@@ -12,14 +12,22 @@
|
||||
|
||||
<div class="breadcrumb">
|
||||
<el-breadcrumb :separator-icon="ArrowRight">
|
||||
<el-breadcrumb-item v-for="item in breadcrumb" :key="item.title">{{ item.title }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-for="item in breadcrumb" :key="item.title">{{
|
||||
item.title
|
||||
}}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-user-con">
|
||||
<!-- 切换主题 -->
|
||||
<el-switch style="margin-right: 10px" v-model="dark" inline-prompt :active-action-icon="Moon"
|
||||
:inactive-action-icon="Sunny" @change="changeTheme"/>
|
||||
<el-switch
|
||||
style="margin-right: 10px"
|
||||
v-model="dark"
|
||||
inline-prompt
|
||||
:active-action-icon="Moon"
|
||||
:inactive-action-icon="Sunny"
|
||||
@change="changeTheme"
|
||||
/>
|
||||
<!-- 用户名下拉菜单 -->
|
||||
<el-dropdown class="user-name" :hide-on-click="true" trigger="click">
|
||||
<span class="el-dropdown-link">
|
||||
@@ -30,7 +38,9 @@
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item><i class="iconfont icon-version"></i> 当前版本:{{ version }}</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
><i class="iconfont icon-version"></i> 当前版本:{{ version }}</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item divided @click="logout">
|
||||
<i class="iconfont icon-logout"></i>
|
||||
<span>退出登录</span>
|
||||
@@ -43,103 +53,103 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {getMenuItems, useSidebarStore} from "@/store/sidebar";
|
||||
import {useRouter} from "vue-router";
|
||||
import {ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny} from "@element-plus/icons-vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {removeAdminToken} from "@/store/session";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import { removeAdminToken } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { getMenuItems, useSidebarStore } from '@/store/sidebar'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const version = ref(process.env.VUE_APP_VERSION);
|
||||
const avatar = ref("/images/user-info.jpg");
|
||||
const sidebar = useSidebarStore();
|
||||
const router = useRouter();
|
||||
const breadcrumb = ref([]);
|
||||
const version = ref(import.meta.env.VITE_VERSION)
|
||||
const avatar = ref('/images/user-info.jpg')
|
||||
const sidebar = useSidebarStore()
|
||||
const router = useRouter()
|
||||
const breadcrumb = ref([])
|
||||
|
||||
const store = useSharedStore();
|
||||
const dark = ref(store.theme === "dark");
|
||||
const theme = ref(store.theme);
|
||||
const store = useSharedStore()
|
||||
const dark = ref(store.theme === 'dark')
|
||||
const theme = ref(store.theme)
|
||||
watch(
|
||||
() => store.theme,
|
||||
() => store.theme,
|
||||
(val) => {
|
||||
theme.value = val;
|
||||
theme.value = val
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const changeTheme = () => {
|
||||
store.setTheme(dark.value ? "dark" : "light");
|
||||
};
|
||||
store.setTheme(dark.value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
router.afterEach((to) => {
|
||||
initBreadCrumb(to.path);
|
||||
});
|
||||
initBreadCrumb(to.path)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initBreadCrumb(router.currentRoute.value.path);
|
||||
});
|
||||
initBreadCrumb(router.currentRoute.value.path)
|
||||
})
|
||||
|
||||
// 初始化面包屑导航
|
||||
const initBreadCrumb = (path) => {
|
||||
breadcrumb.value = [{ title: "首页" }];
|
||||
const items = getMenuItems();
|
||||
breadcrumb.value = [{ title: '首页' }]
|
||||
const items = getMenuItems()
|
||||
if (items) {
|
||||
let bk = false;
|
||||
let bk = false
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].index === path) {
|
||||
breadcrumb.value.push({
|
||||
title: items[i].title,
|
||||
path: items[i].index,
|
||||
});
|
||||
break;
|
||||
})
|
||||
break
|
||||
}
|
||||
if (bk) {
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
if (items[i]["subs"]) {
|
||||
const subs = items[i]["subs"];
|
||||
if (items[i]['subs']) {
|
||||
const subs = items[i]['subs']
|
||||
for (let j = 0; j < subs.length; j++) {
|
||||
if (subs[j].index === path) {
|
||||
breadcrumb.value.push({
|
||||
title: items[i].title,
|
||||
path: items[i].index,
|
||||
});
|
||||
})
|
||||
breadcrumb.value.push({
|
||||
title: subs[j].title,
|
||||
path: subs[j].index,
|
||||
});
|
||||
bk = true;
|
||||
break;
|
||||
})
|
||||
bk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 侧边栏折叠
|
||||
const collapseChange = () => {
|
||||
sidebar.handleCollapse();
|
||||
};
|
||||
sidebar.handleCollapse()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (document.body.clientWidth < 1024) {
|
||||
collapseChange();
|
||||
collapseChange()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const logout = function () {
|
||||
httpGet("/api/admin/logout")
|
||||
httpGet('/api/admin/logout')
|
||||
.then(() => {
|
||||
removeAdminToken();
|
||||
router.replace("/admin/login");
|
||||
removeAdminToken()
|
||||
router.replace('/admin/login')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("注销失败: " + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('注销失败: ' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="stylus">
|
||||
.admin-header {
|
||||
|
||||
@@ -6,35 +6,41 @@
|
||||
</div>
|
||||
|
||||
<div class="chat-icon">
|
||||
<van-image :src="icon"/>
|
||||
<van-image :src="icon" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {showNotify} from "vant";
|
||||
import Clipboard from 'clipboard'
|
||||
import { showNotify } from 'vant'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars,no-undef
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
type: Object,
|
||||
default: {
|
||||
text: '',
|
||||
files: [],
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '/images/user-icon.png',
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
const contentRef = ref(null)
|
||||
const content = computed(() => {
|
||||
return props.content.text
|
||||
})
|
||||
onMounted(() => {
|
||||
const clipboard = new Clipboard(contentRef.value);
|
||||
const clipboard = new Clipboard(contentRef.value)
|
||||
clipboard.on('success', () => {
|
||||
showNotify({type: 'success', message: '复制成功', duration: 1000})
|
||||
showNotify({ type: 'success', message: '复制成功', duration: 1000 })
|
||||
})
|
||||
clipboard.on('error', () => {
|
||||
showNotify({type: 'danger', message: '复制失败', duration: 2000})
|
||||
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -100,4 +106,4 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -28,8 +28,11 @@ import { showImagePreview } from 'vant'
|
||||
import Thinking from '../Thinking.vue'
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
type: Object,
|
||||
default: {
|
||||
text: '',
|
||||
files: [],
|
||||
},
|
||||
},
|
||||
orgContent: {
|
||||
type: String,
|
||||
@@ -41,6 +44,9 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const content = computed(() => {
|
||||
return props.content.text
|
||||
})
|
||||
const contentRef = ref(null)
|
||||
onMounted(() => {
|
||||
const imgs = contentRef.value.querySelectorAll('img')
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import {randString} from "@/utils/libs";
|
||||
import Storage from "good-storage";
|
||||
import {removeAdminInfo} from "@/store/cache";
|
||||
import { removeAdminInfo } from '@/store/cache'
|
||||
import { randString } from '@/utils/libs'
|
||||
import Storage from 'good-storage'
|
||||
|
||||
/**
|
||||
* storage handler
|
||||
*/
|
||||
|
||||
const UserTokenKey = process.env.VUE_APP_KEY_PREFIX + "Authorization";
|
||||
const AdminTokenKey = process.env.VUE_APP_KEY_PREFIX + "Admin-Authorization"
|
||||
const UserTokenKey = import.meta.env.VITE_KEY_PREFIX + 'Authorization'
|
||||
const AdminTokenKey = import.meta.env.VITE_KEY_PREFIX + 'Admin-Authorization'
|
||||
|
||||
export function getSessionId() {
|
||||
return randString(42)
|
||||
return randString(42)
|
||||
}
|
||||
|
||||
export function getUserToken() {
|
||||
return Storage.get(UserTokenKey) ?? ""
|
||||
return Storage.get(UserTokenKey) ?? ''
|
||||
}
|
||||
export function setUserToken(token) {
|
||||
// 刷新 session 缓存
|
||||
Storage.set(UserTokenKey, token)
|
||||
// 刷新 session 缓存
|
||||
Storage.set(UserTokenKey, token)
|
||||
}
|
||||
|
||||
export function removeUserToken() {
|
||||
Storage.remove(UserTokenKey)
|
||||
Storage.remove(UserTokenKey)
|
||||
}
|
||||
|
||||
export function getAdminToken() {
|
||||
return Storage.get(AdminTokenKey) ?? ""
|
||||
return Storage.get(AdminTokenKey) ?? ''
|
||||
}
|
||||
|
||||
export function setAdminToken(token) {
|
||||
Storage.set(AdminTokenKey, token)
|
||||
Storage.set(AdminTokenKey, token)
|
||||
}
|
||||
|
||||
export function removeAdminToken() {
|
||||
Storage.remove(AdminTokenKey)
|
||||
removeAdminInfo()
|
||||
Storage.remove(AdminTokenKey)
|
||||
removeAdminInfo()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {defineStore} from "pinia";
|
||||
import Storage from "good-storage";
|
||||
import errorIcon from "@/assets/img/failed.png";
|
||||
import loadingIcon from "@/assets/img/loading.gif";
|
||||
import errorIcon from '@/assets/img/failed.png'
|
||||
import loadingIcon from '@/assets/img/loading.gif'
|
||||
import Storage from 'good-storage'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
let waterfallOptions = {
|
||||
// 唯一key值
|
||||
rowKey: "id",
|
||||
rowKey: 'id',
|
||||
// 卡片之间的间隙
|
||||
gutter: 10,
|
||||
// 是否有周围的gutter
|
||||
@@ -44,16 +44,16 @@ let waterfallOptions = {
|
||||
},
|
||||
},
|
||||
// 动画效果
|
||||
animationEffect: "animate__fadeInUp",
|
||||
animationEffect: 'animate__fadeInUp',
|
||||
// 动画时间
|
||||
animationDuration: 1000,
|
||||
// 动画延迟
|
||||
animationDelay: 300,
|
||||
animationCancel: false,
|
||||
// 背景色
|
||||
backgroundColor: "",
|
||||
backgroundColor: '',
|
||||
// imgSelector
|
||||
imgSelector: "img_thumb",
|
||||
imgSelector: 'img_thumb',
|
||||
// 是否跨域
|
||||
crossOrigin: true,
|
||||
// 加载配置
|
||||
@@ -61,102 +61,62 @@ let waterfallOptions = {
|
||||
loading: loadingIcon,
|
||||
error: errorIcon,
|
||||
ratioCalculator: (width, height) => {
|
||||
const minRatio = 3 / 4;
|
||||
const maxRatio = 4 / 3;
|
||||
const curRatio = height / width;
|
||||
const minRatio = 3 / 4
|
||||
const maxRatio = 4 / 3
|
||||
const curRatio = height / width
|
||||
if (curRatio < minRatio) {
|
||||
return minRatio;
|
||||
return minRatio
|
||||
} else if (curRatio > maxRatio) {
|
||||
return maxRatio;
|
||||
return maxRatio
|
||||
} else {
|
||||
return curRatio;
|
||||
return curRatio
|
||||
}
|
||||
},
|
||||
},
|
||||
// 是否懒加载
|
||||
lazyload: true,
|
||||
align: "center",
|
||||
align: 'center',
|
||||
}
|
||||
|
||||
export const useSharedStore = defineStore("shared", {
|
||||
export const useSharedStore = defineStore('shared', {
|
||||
state: () => ({
|
||||
showLoginDialog: false,
|
||||
chatListStyle: Storage.get("chat_list_style", "chat"),
|
||||
chatStream: Storage.get("chat_stream", true),
|
||||
socket: { conn: null, handlers: {} },
|
||||
theme: Storage.get("theme", "light"),
|
||||
chatListStyle: Storage.get('chat_list_style', 'chat'),
|
||||
chatStream: Storage.get('chat_stream', true),
|
||||
theme: Storage.get('theme', 'light'),
|
||||
isLogin: false,
|
||||
chatListExtend: Storage.get("chat_list_extend", true),
|
||||
ttsModel: Storage.get("tts_model", ""),
|
||||
chatListExtend: Storage.get('chat_list_extend', true),
|
||||
ttsModel: Storage.get('tts_model', ''),
|
||||
waterfallOptions,
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
setShowLoginDialog(value) {
|
||||
this.showLoginDialog = value;
|
||||
this.showLoginDialog = value
|
||||
},
|
||||
setChatListStyle(value) {
|
||||
this.chatListStyle = value;
|
||||
Storage.set("chat_list_style", value);
|
||||
this.chatListStyle = value
|
||||
Storage.set('chat_list_style', value)
|
||||
},
|
||||
setChatStream(value) {
|
||||
this.chatStream = value;
|
||||
Storage.set("chat_stream", value);
|
||||
},
|
||||
setSocket(value) {
|
||||
for (const key in this.socket.handlers) {
|
||||
this.setMessageHandler(value, this.socket.handlers[key]);
|
||||
}
|
||||
this.socket.conn = value;
|
||||
this.chatStream = value
|
||||
Storage.set('chat_stream', value)
|
||||
},
|
||||
setChatListExtend(value) {
|
||||
this.chatListExtend = value;
|
||||
Storage.set("chat_list_extend", value);
|
||||
},
|
||||
addMessageHandler(key, callback) {
|
||||
if (!this.socket.handlers[key]) {
|
||||
this.socket.handlers[key] = callback;
|
||||
}
|
||||
this.setMessageHandler(this.socket.conn, callback);
|
||||
},
|
||||
|
||||
setMessageHandler(conn, callback) {
|
||||
if (!conn) {
|
||||
return;
|
||||
}
|
||||
conn.addEventListener("message", (event) => {
|
||||
try {
|
||||
if (event.data instanceof Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8");
|
||||
reader.onload = () => {
|
||||
callback(JSON.parse(String(reader.result)));
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeMessageHandler(key) {
|
||||
if (this.socket.conn && this.socket.conn.readyState === WebSocket.OPEN) {
|
||||
this.socket.conn.removeEventListener("message", this.socket.handlers[key]);
|
||||
}
|
||||
delete this.socket.handlers[key];
|
||||
this.chatListExtend = value
|
||||
Storage.set('chat_list_extend', value)
|
||||
},
|
||||
setTheme(theme) {
|
||||
this.theme = theme;
|
||||
document.documentElement.setAttribute("data-theme", theme); // 设置 HTML 的 data-theme 属性
|
||||
Storage.set("theme", theme);
|
||||
this.theme = theme
|
||||
document.documentElement.setAttribute('data-theme', theme) // 设置 HTML 的 data-theme 属性
|
||||
Storage.set('theme', theme)
|
||||
},
|
||||
setIsLogin(value) {
|
||||
this.isLogin = value;
|
||||
this.isLogin = value
|
||||
},
|
||||
|
||||
setTtsModel(value) {
|
||||
this.ttsModel = value;
|
||||
Storage.set("tts_model", value);
|
||||
this.ttsModel = value
|
||||
Storage.set('tts_model', value)
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -5,50 +5,50 @@
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import Storage from "good-storage";
|
||||
import Storage from 'good-storage'
|
||||
|
||||
export function GetFileIcon(ext) {
|
||||
const files = {
|
||||
".docx": "doc.png",
|
||||
".doc": "doc.png",
|
||||
".xls": "xls.png",
|
||||
".xlsx": "xls.png",
|
||||
".csv": "xls.png",
|
||||
".ppt": "ppt.png",
|
||||
".pptx": "ppt.png",
|
||||
".md": "md.png",
|
||||
".pdf": "pdf.png",
|
||||
".sql": "sql.png",
|
||||
".mp3": "mp3.png",
|
||||
".wav": "mp3.png",
|
||||
".mp4": "mp4.png",
|
||||
".avi": "mp4.png",
|
||||
}
|
||||
if (files[ext]) {
|
||||
return '/images/ext/' + files[ext]
|
||||
}
|
||||
const files = {
|
||||
'.docx': 'doc.png',
|
||||
'.doc': 'doc.png',
|
||||
'.xls': 'xls.png',
|
||||
'.xlsx': 'xls.png',
|
||||
'.csv': 'xls.png',
|
||||
'.ppt': 'ppt.png',
|
||||
'.pptx': 'ppt.png',
|
||||
'.md': 'md.png',
|
||||
'.pdf': 'pdf.png',
|
||||
'.sql': 'sql.png',
|
||||
'.mp3': 'mp3.png',
|
||||
'.wav': 'mp3.png',
|
||||
'.mp4': 'mp4.png',
|
||||
'.avi': 'mp4.png',
|
||||
}
|
||||
if (files[ext]) {
|
||||
return '/images/ext/' + files[ext]
|
||||
}
|
||||
|
||||
return '/images/ext/file.png'
|
||||
return '/images/ext/file.png'
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
export function GetFileType (ext) {
|
||||
return ext.replace(".", "").toUpperCase()
|
||||
export function GetFileType(ext) {
|
||||
return ext.replace('.', '').toUpperCase()
|
||||
}
|
||||
|
||||
// 将文件大小转成字符
|
||||
export function FormatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
export function setRoute(path) {
|
||||
Storage.set(process.env.VUE_APP_KEY_PREFIX + 'ROUTE_',path)
|
||||
Storage.set(import.meta.env.VITE_KEY_PREFIX + 'ROUTE_', path)
|
||||
}
|
||||
|
||||
export function getRoute() {
|
||||
return Storage.get(process.env.VUE_APP_KEY_PREFIX + 'ROUTE_')
|
||||
return Storage.get(import.meta.env.VITE_KEY_PREFIX + 'ROUTE_')
|
||||
}
|
||||
|
||||
@@ -6,95 +6,106 @@
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import axios from 'axios'
|
||||
import {getAdminToken, getUserToken, removeAdminToken, removeUserToken} from "@/store/session";
|
||||
import { getAdminToken, getUserToken, removeAdminToken, removeUserToken } from '@/store/session'
|
||||
|
||||
axios.defaults.timeout = 180000
|
||||
axios.defaults.baseURL = process.env.VUE_APP_API_HOST
|
||||
axios.defaults.withCredentials = true;
|
||||
// axios.defaults.baseURL = process.env.VUE_APP_API_HOST
|
||||
axios.defaults.withCredentials = true
|
||||
//axios.defaults.headers.post['Content-Type'] = 'application/json'
|
||||
|
||||
// HTTP拦截器
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
// set token
|
||||
config.headers['Authorization'] = getUserToken();
|
||||
config.headers['Admin-Authorization'] = getAdminToken();
|
||||
return config
|
||||
}, error => {
|
||||
return Promise.reject(error)
|
||||
})
|
||||
(config) => {
|
||||
// set token
|
||||
config.headers['Authorization'] = getUserToken()
|
||||
config.headers['Admin-Authorization'] = getAdminToken()
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
axios.interceptors.response.use(
|
||||
response => {
|
||||
return response
|
||||
}, error => {
|
||||
if (error.response.status === 401) {
|
||||
if (error.response.request.responseURL.indexOf("/api/admin") !== -1) {
|
||||
removeAdminToken()
|
||||
} else {
|
||||
console.log("FUCK")
|
||||
removeUserToken()
|
||||
}
|
||||
error.response.data.message = "请先登录"
|
||||
return Promise.reject(error.response.data)
|
||||
}
|
||||
if (error.response.status === 400) {
|
||||
return Promise.reject(new Error(error.response.data.message))
|
||||
} else {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (error.response.status === 401) {
|
||||
if (error.response.request.responseURL.indexOf('/api/admin') !== -1) {
|
||||
removeAdminToken()
|
||||
} else {
|
||||
removeUserToken()
|
||||
}
|
||||
error.response.data.message = '请先登录'
|
||||
return Promise.reject(error.response.data)
|
||||
}
|
||||
if (error.response.status === 400) {
|
||||
return Promise.reject(new Error(error.response.data.message))
|
||||
} else {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// send a http get request
|
||||
export function httpGet(url, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get(url, {
|
||||
params: params
|
||||
}).then(response => {
|
||||
resolve(response.data)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(url, {
|
||||
params: params,
|
||||
})
|
||||
.then((response) => {
|
||||
resolve(response.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// send a http post request
|
||||
export function httpPost(url, data = {}, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post(url, data, options).then(response => {
|
||||
resolve(response.data)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(url, data, options)
|
||||
.then((response) => {
|
||||
resolve(response.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function httpDownload(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
responseType: 'blob' // 将响应类型设置为 `blob`
|
||||
}).then(response => {
|
||||
resolve(response)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
responseType: 'blob', // 将响应类型设置为 `blob`
|
||||
})
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function httpPostDownload(url, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
data: data,
|
||||
responseType: 'blob' // 将响应类型设置为 `blob`
|
||||
}).then(response => {
|
||||
resolve(response)
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
data: data,
|
||||
responseType: 'blob', // 将响应类型设置为 `blob`
|
||||
})
|
||||
}
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,252 +8,250 @@
|
||||
/**
|
||||
* Util lib functions
|
||||
*/
|
||||
import { showConfirmDialog } from "vant";
|
||||
import { showConfirmDialog } from 'vant'
|
||||
|
||||
// generate a random string
|
||||
export function randString(length) {
|
||||
const str = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
const size = str.length;
|
||||
let buf = [];
|
||||
const str = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
const size = str.length
|
||||
let buf = []
|
||||
for (let i = 0; i < length; i++) {
|
||||
const rand = Math.random() * size;
|
||||
buf.push(str.charAt(rand));
|
||||
const rand = Math.random() * size
|
||||
buf.push(str.charAt(rand))
|
||||
}
|
||||
return buf.join("");
|
||||
return buf.join('')
|
||||
}
|
||||
|
||||
export function UUID() {
|
||||
let d = new Date().getTime();
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
let d = new Date().getTime()
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (d + Math.random() * 16) % 16 | 0
|
||||
d = Math.floor(d / 16)
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
// 判断是否是移动设备
|
||||
export function isMobile() {
|
||||
const userAgent = navigator.userAgent;
|
||||
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
|
||||
return mobileRegex.test(userAgent);
|
||||
const userAgent = navigator.userAgent
|
||||
const mobileRegex =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i
|
||||
return mobileRegex.test(userAgent)
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
export function dateFormat(timestamp, format) {
|
||||
if (!timestamp) {
|
||||
return "";
|
||||
return ''
|
||||
} else if (timestamp < 9680917502) {
|
||||
timestamp = timestamp * 1000;
|
||||
timestamp = timestamp * 1000
|
||||
}
|
||||
let year, month, day, HH, mm, ss;
|
||||
let time = new Date(timestamp);
|
||||
let timeDate;
|
||||
year = time.getFullYear(); // 年
|
||||
month = time.getMonth() + 1; // 月
|
||||
day = time.getDate(); // 日
|
||||
HH = time.getHours(); // 时
|
||||
mm = time.getMinutes(); // 分
|
||||
ss = time.getSeconds(); // 秒
|
||||
let year, month, day, HH, mm, ss
|
||||
let time = new Date(timestamp)
|
||||
let timeDate
|
||||
year = time.getFullYear() // 年
|
||||
month = time.getMonth() + 1 // 月
|
||||
day = time.getDate() // 日
|
||||
HH = time.getHours() // 时
|
||||
mm = time.getMinutes() // 分
|
||||
ss = time.getSeconds() // 秒
|
||||
|
||||
month = month < 10 ? "0" + month : month;
|
||||
day = day < 10 ? "0" + day : day;
|
||||
HH = HH < 10 ? "0" + HH : HH; // 时
|
||||
mm = mm < 10 ? "0" + mm : mm; // 分
|
||||
ss = ss < 10 ? "0" + ss : ss; // 秒
|
||||
month = month < 10 ? '0' + month : month
|
||||
day = day < 10 ? '0' + day : day
|
||||
HH = HH < 10 ? '0' + HH : HH // 时
|
||||
mm = mm < 10 ? '0' + mm : mm // 分
|
||||
ss = ss < 10 ? '0' + ss : ss // 秒
|
||||
|
||||
switch (format) {
|
||||
case "yyyy":
|
||||
timeDate = String(year);
|
||||
break;
|
||||
case "yyyy-MM":
|
||||
timeDate = year + "-" + month;
|
||||
break;
|
||||
case "yyyy-MM-dd":
|
||||
timeDate = year + "-" + month + "-" + day;
|
||||
break;
|
||||
case "yyyy/MM/dd":
|
||||
timeDate = year + "/" + month + "/" + day;
|
||||
break;
|
||||
case "yyyy-MM-dd HH:mm:ss":
|
||||
timeDate = year + "-" + month + "-" + day + " " + HH + ":" + mm + ":" + ss;
|
||||
break;
|
||||
case "HH:mm:ss":
|
||||
timeDate = HH + ":" + mm + ":" + ss;
|
||||
break;
|
||||
case "MM":
|
||||
timeDate = String(month);
|
||||
break;
|
||||
case 'yyyy':
|
||||
timeDate = String(year)
|
||||
break
|
||||
case 'yyyy-MM':
|
||||
timeDate = year + '-' + month
|
||||
break
|
||||
case 'yyyy-MM-dd':
|
||||
timeDate = year + '-' + month + '-' + day
|
||||
break
|
||||
case 'yyyy/MM/dd':
|
||||
timeDate = year + '/' + month + '/' + day
|
||||
break
|
||||
case 'yyyy-MM-dd HH:mm:ss':
|
||||
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss
|
||||
break
|
||||
case 'HH:mm:ss':
|
||||
timeDate = HH + ':' + mm + ':' + ss
|
||||
break
|
||||
case 'MM':
|
||||
timeDate = String(month)
|
||||
break
|
||||
default:
|
||||
timeDate = year + "-" + month + "-" + day + " " + HH + ":" + mm + ":" + ss;
|
||||
break;
|
||||
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss
|
||||
break
|
||||
}
|
||||
return timeDate;
|
||||
return timeDate
|
||||
}
|
||||
|
||||
export function formatTime(time) {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = Math.floor(time % 60)
|
||||
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||
}
|
||||
|
||||
// 判断数组中是否包含某个元素
|
||||
export function arrayContains(array, value, compare) {
|
||||
if (!array) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof compare !== "function") {
|
||||
if (typeof compare !== 'function') {
|
||||
compare = function (v1, v2) {
|
||||
return v1 === v2;
|
||||
};
|
||||
return v1 === v2
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (compare(array[i], value)) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// 删除数组中指定的元素
|
||||
export function removeArrayItem(array, value, compare) {
|
||||
if (typeof compare !== "function") {
|
||||
if (typeof compare !== 'function') {
|
||||
compare = function (v1, v2) {
|
||||
return v1 === v2;
|
||||
};
|
||||
return v1 === v2
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (compare(array[i], value)) {
|
||||
array.splice(i, 1);
|
||||
break;
|
||||
array.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
return array;
|
||||
return array
|
||||
}
|
||||
|
||||
// 渲染输入的换行符
|
||||
export function renderInputText(text) {
|
||||
const replaceRegex = /(\n\r|\r\n|\r|\n)/g;
|
||||
text = text || "";
|
||||
return text.replace(replaceRegex, "<br/>");
|
||||
const replaceRegex = /(\n\r|\r\n|\r|\n)/g
|
||||
text = text || ''
|
||||
return text.replace(replaceRegex, '<br/>')
|
||||
}
|
||||
|
||||
// 拷贝对象
|
||||
export function copyObj(origin) {
|
||||
return JSON.parse(JSON.stringify(origin));
|
||||
return JSON.parse(JSON.stringify(origin))
|
||||
}
|
||||
|
||||
export function disabledDate(time) {
|
||||
return time.getTime() < Date.now();
|
||||
return time.getTime() < Date.now()
|
||||
}
|
||||
|
||||
// 字符串截取
|
||||
export function substr(str, length) {
|
||||
let result = "";
|
||||
let count = 0;
|
||||
let result = ''
|
||||
let count = 0
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charAt(i);
|
||||
const charCode = str.charCodeAt(i);
|
||||
const char = str.charAt(i)
|
||||
const charCode = str.charCodeAt(i)
|
||||
|
||||
// 判断字符是否为中文字符
|
||||
if (charCode >= 0x4e00 && charCode <= 0x9fff) {
|
||||
// 中文字符算两个字符
|
||||
count += 2;
|
||||
count += 2
|
||||
} else {
|
||||
count++;
|
||||
count++
|
||||
}
|
||||
|
||||
if (count <= length) {
|
||||
result += char;
|
||||
result += char
|
||||
} else {
|
||||
result += " ...";
|
||||
break;
|
||||
result += ' ...'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
export function isImage(url) {
|
||||
const expr = /\.(jpg|jpeg|png|gif|bmp|svg)$/i;
|
||||
return expr.test(url);
|
||||
const expr = /\.(jpg|jpeg|png|gif|bmp|svg)$/i
|
||||
return expr.test(url)
|
||||
}
|
||||
|
||||
export function processContent(content) {
|
||||
if (!content) {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
|
||||
// 如果是图片链接地址,则直接替换成图片标签
|
||||
const linkRegex = /(https?:\/\/\S+)/g;
|
||||
const links = content.match(linkRegex);
|
||||
const linkRegex = /(https?:\/\/\S+)/g
|
||||
const links = content.match(linkRegex)
|
||||
if (links) {
|
||||
for (let link of links) {
|
||||
if (isImage(link)) {
|
||||
const index = content.indexOf(link);
|
||||
if (content.substring(index - 1, 2) !== "]") {
|
||||
content = content.replace(link, "\n\n");
|
||||
const index = content.indexOf(link)
|
||||
if (content.substring(index - 1, 2) !== ']') {
|
||||
content = content.replace(link, '\n\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理推理标签
|
||||
if (content.includes("<think>")) {
|
||||
if (content.includes('<think>')) {
|
||||
content = content.replace(/<think>(.*?)<\/think>/gs, (match, content) => {
|
||||
if (content.length > 10) {
|
||||
return `<blockquote>${content}</blockquote>`;
|
||||
return `<blockquote>${content}</blockquote>`
|
||||
}
|
||||
return "";
|
||||
});
|
||||
return ''
|
||||
})
|
||||
content = content.replace(/<think>(.*?)$/gs, (match, content) => {
|
||||
if (content.length > 10) {
|
||||
return `<blockquote>${content}</blockquote>`;
|
||||
return `<blockquote>${content}</blockquote>`
|
||||
}
|
||||
return "";
|
||||
});
|
||||
return ''
|
||||
})
|
||||
}
|
||||
|
||||
// 支持 \[ 公式标签
|
||||
content = content.replace(/\\\[/g, "$$").replace(/\\\]/g, "$$");
|
||||
content = content.replace(/\\\(\\boxed\{(\d+)\}\\\)/g, '<span class="boxed">$1</span>');
|
||||
return content;
|
||||
content = content.replace(/\\\[/g, '$$').replace(/\\\]/g, '$$')
|
||||
content = content.replace(/\\\(\\boxed\{(\d+)\}\\\)/g, '<span class="boxed">$1</span>')
|
||||
return content
|
||||
}
|
||||
|
||||
export function processPrompt(prompt) {
|
||||
return prompt.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
return prompt.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
// 判断是否为微信浏览器
|
||||
export function isWeChatBrowser() {
|
||||
return /MicroMessenger/i.test(navigator.userAgent);
|
||||
return /MicroMessenger/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
export function showLoginDialog(router) {
|
||||
showConfirmDialog({
|
||||
title: "登录",
|
||||
message: "此操作需要登录才能进行,前往登录?",
|
||||
title: '登录',
|
||||
message: '此操作需要登录才能进行,前往登录?',
|
||||
})
|
||||
.then(() => {
|
||||
router.push("/mobile/login");
|
||||
router.push('/mobile/login')
|
||||
})
|
||||
.catch(() => {
|
||||
// on cancel
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export const replaceImg = (img) => {
|
||||
if (!img.startsWith("http")) {
|
||||
img = `${location.protocol}//${location.host}${img}`;
|
||||
if (img.startsWith('http')) {
|
||||
return img
|
||||
}
|
||||
const devHost = process.env.VUE_APP_API_HOST;
|
||||
const localhost = "http://localhost:5678";
|
||||
if (img.includes(localhost)) {
|
||||
return img?.replace(localhost, devHost);
|
||||
}
|
||||
return img;
|
||||
};
|
||||
export function isChrome() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
return /chrome/.test(userAgent) && !/edg/.test(userAgent);
|
||||
return `${location.protocol}//${location.host}${img}`
|
||||
}
|
||||
|
||||
// 判断是否 google 浏览器
|
||||
export function isChrome() {
|
||||
const userAgent = navigator.userAgent.toLowerCase()
|
||||
return /chrome/.test(userAgent) && !/edg/.test(userAgent)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
<div class="flex h-20">
|
||||
<ul class="scrollbar-type-nav">
|
||||
<li :class="{ active: typeId === '' }" @click="getAppList('')">全部分类</li>
|
||||
<li v-for="item in appTypes" :key="item.id" :class="{ active: typeId === item.id }" @click="getAppList(item.id)">
|
||||
<li
|
||||
v-for="item in appTypes"
|
||||
:key="item.id"
|
||||
:class="{ active: typeId === item.id }"
|
||||
@click="getAppList(item.id)"
|
||||
>
|
||||
<div class="image" v-if="item.icon">
|
||||
<el-image :src="item.icon" fit="cover" />
|
||||
</div>
|
||||
@@ -27,37 +32,25 @@
|
||||
<div class="info-text">{{ scope.item.hello_msg }}</div>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<el-button size="small" class="sm-btn-theme" @click="useRole(scope.item)">使用</el-button>
|
||||
<el-button size="small" class="sm-btn-theme" @click="useRole(scope.item)"
|
||||
>使用</el-button
|
||||
>
|
||||
<el-tooltip content="从工作区移除" placement="top" v-if="hasRole(scope.item.key)">
|
||||
<el-button size="small" type="danger" @click="updateRole(scope.item, 'remove')">移除</el-button>
|
||||
<el-button size="small" type="danger" @click="updateRole(scope.item, 'remove')"
|
||||
>移除</el-button
|
||||
>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="添加到工作区" placement="top" v-else>
|
||||
<el-button size="small" style="--el-color-primary: #009999" @click="updateRole(scope.item, 'add')">添加</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
style="--el-color-primary: #009999"
|
||||
@click="updateRole(scope.item, 'add')"
|
||||
>添加</el-button
|
||||
>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="app-item">-->
|
||||
<!-- <el-image :src="scope.item.icon" fit="cover"/>-->
|
||||
<!-- <div class="title">-->
|
||||
<!-- <span class="name">{{ scope.item.name }}</span>-->
|
||||
<!-- <div class="opt">-->
|
||||
<!-- <div v-if="hasRole(scope.item.key)">-->
|
||||
<!-- <el-button size="small" type="success" @click="useRole(scope.item)">使用</el-button>-->
|
||||
<!-- <el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button>-->
|
||||
<!-- </div>-->
|
||||
<!-- <el-button v-else size="small"-->
|
||||
<!-- style="--el-color-primary:#009999"-->
|
||||
<!-- @click="updateRole(scope.item, 'add')">-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <Plus/>-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- <span>添加应用</span>-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="hello-msg" ref="elements">{{ scope.item.intro }}</div>-->
|
||||
<!-- </div>-->
|
||||
</template>
|
||||
</ItemList>
|
||||
<div v-else style="width: 100%">
|
||||
@@ -69,112 +62,112 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { checkSession } from "@/store/cache";
|
||||
import { arrayContains, removeArrayItem, substr } from "@/utils/libs";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import ItemList from '@/components/ItemList.vue'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { arrayContains, removeArrayItem, substr } from '@/utils/libs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const listBoxHeight = window.innerHeight - 133;
|
||||
const listBoxHeight = window.innerHeight - 133
|
||||
|
||||
const typeId = ref("");
|
||||
const appTypes = ref([]);
|
||||
const list = ref([]);
|
||||
const roles = ref([]);
|
||||
const store = useSharedStore();
|
||||
const typeId = ref('')
|
||||
const appTypes = ref([])
|
||||
const list = ref([])
|
||||
const roles = ref([])
|
||||
const store = useSharedStore()
|
||||
|
||||
onMounted(() => {
|
||||
getAppType();
|
||||
getAppList();
|
||||
getRoles();
|
||||
});
|
||||
getAppType()
|
||||
getAppList()
|
||||
getRoles()
|
||||
})
|
||||
|
||||
const getRoles = () => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
roles.value = user.chat_roles;
|
||||
roles.value = user.chat_roles
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e.message);
|
||||
});
|
||||
};
|
||||
console.log(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const getAppType = () => {
|
||||
httpGet("/api/app/type/list")
|
||||
httpGet('/api/app/type/list')
|
||||
.then((res) => {
|
||||
appTypes.value = res.data;
|
||||
appTypes.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取分类失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取分类失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const getAppList = (tid = "") => {
|
||||
typeId.value = tid;
|
||||
httpGet("/api/app/list", { tid })
|
||||
const getAppList = (tid = '') => {
|
||||
typeId.value = tid
|
||||
httpGet('/api/app/list', { tid })
|
||||
.then((res) => {
|
||||
const items = res.data;
|
||||
const items = res.data
|
||||
// 处理 hello message
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
items[i].intro = substr(items[i].hello_msg, 80);
|
||||
items[i].intro = substr(items[i].hello_msg, 80)
|
||||
}
|
||||
list.value = items;
|
||||
list.value = items
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取应用失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取应用失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const updateRole = (row, opt) => {
|
||||
checkSession()
|
||||
.then(() => {
|
||||
const title = ref("");
|
||||
if (opt === "add") {
|
||||
title.value = "添加应用";
|
||||
const exists = arrayContains(roles.value, row.key);
|
||||
const title = ref('')
|
||||
if (opt === 'add') {
|
||||
title.value = '添加应用'
|
||||
const exists = arrayContains(roles.value, row.key)
|
||||
if (exists) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
roles.value.push(row.key);
|
||||
roles.value.push(row.key)
|
||||
} else {
|
||||
title.value = "移除应用";
|
||||
const exists = arrayContains(roles.value, row.key);
|
||||
title.value = '移除应用'
|
||||
const exists = arrayContains(roles.value, row.key)
|
||||
if (!exists) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
roles.value = removeArrayItem(roles.value, row.key);
|
||||
roles.value = removeArrayItem(roles.value, row.key)
|
||||
}
|
||||
httpPost("/api/app/update", { keys: roles.value })
|
||||
httpPost('/api/app/update', { keys: roles.value })
|
||||
.then(() => {
|
||||
ElMessage.success({
|
||||
message: title.value + "成功!",
|
||||
message: title.value + '成功!',
|
||||
duration: 1000,
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error(title.value + "失败:" + e.message);
|
||||
});
|
||||
ElMessage.error(title.value + '失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
store.setShowLoginDialog(true);
|
||||
});
|
||||
};
|
||||
store.setShowLoginDialog(true)
|
||||
})
|
||||
}
|
||||
|
||||
const hasRole = (roleKey) => {
|
||||
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2);
|
||||
};
|
||||
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
const useRole = (role) => {
|
||||
router.push(`/chat?role_id=${role.id}`);
|
||||
};
|
||||
router.push(`/chat?role_id=${role.id}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/chat-app.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
<style lang="stylus" scoped>
|
||||
@import "../assets/css/chat-app.styl"
|
||||
@import "../assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
type="info"
|
||||
style="margin-left: 8px; flex-shrink: 0"
|
||||
>
|
||||
{{ getSelectedModel()?.power }}算力
|
||||
{{ getSelectedModel() && getSelectedModel().power }}算力
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-button>
|
||||
@@ -264,7 +264,12 @@
|
||||
<welcome @send="autofillPrompt" />
|
||||
</div>
|
||||
<div v-for="item in chatData" :key="item.id" v-else>
|
||||
<chat-prompt v-if="item.type === 'prompt'" :data="item" :list-style="listStyle" />
|
||||
<chat-prompt
|
||||
v-if="item.type === 'prompt'"
|
||||
:data="item"
|
||||
:list-style="listStyle"
|
||||
@edit="editUserPrompt"
|
||||
/>
|
||||
<chat-reply
|
||||
v-else-if="item.type === 'reply'"
|
||||
:data="item"
|
||||
@@ -315,7 +320,10 @@
|
||||
|
||||
<span class="tool-item-btn">
|
||||
<el-tooltip class="box-item" effect="dark" content="上传附件">
|
||||
<file-select :user-id="loginUser?.id" @selected="insertFile" />
|
||||
<file-select
|
||||
:user-id="loginUser && loginUser.id"
|
||||
@selected="insertFile"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
@@ -414,13 +422,15 @@ import {
|
||||
Share,
|
||||
VideoPause,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getUserToken } from '../store/session'
|
||||
|
||||
const title = ref('GeekAI-智能助手')
|
||||
const logo = ref('')
|
||||
@@ -694,153 +704,216 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
window.onresize = () => resizeElement()
|
||||
store.addMessageHandler('chat', (data) => {
|
||||
// 丢去非本频道和本客户端的消息
|
||||
if (data.channel !== 'chat' || data.clientId !== getClientId()) {
|
||||
return
|
||||
})
|
||||
|
||||
// 初始化数据
|
||||
const initData = async () => {
|
||||
try {
|
||||
// 获取角色列表
|
||||
const roleRes = await httpGet('/api/app/list')
|
||||
roles.value = roleRes.data
|
||||
if (roles.value.length > 0) {
|
||||
roleId.value = roles.value[0].id
|
||||
}
|
||||
|
||||
if (data.type === 'error') {
|
||||
ElMessage.error(data.body)
|
||||
return
|
||||
// 获取模型列表
|
||||
const modelRes = await httpGet('/api/model/list')
|
||||
models.value = modelRes.data
|
||||
if (models.value.length > 0) {
|
||||
modelID.value = models.value[0].id
|
||||
}
|
||||
|
||||
const chatRole = getRoleById(roleId.value)
|
||||
if (isNewMsg.value && data.type !== 'end') {
|
||||
const prePrompt = chatData.value[chatData.value.length - 1]?.content
|
||||
isNewMsg.value = false
|
||||
lineBuffer.value = data.body
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
if (reply) {
|
||||
reply['content'] = lineBuffer.value
|
||||
}
|
||||
} else if (data.type === 'end') {
|
||||
// 消息接收完毕
|
||||
// 追加当前会话到会话列表
|
||||
if (newChatItem.value !== null) {
|
||||
newChatItem.value['title'] = tmpChatTitle.value
|
||||
newChatItem.value['chat_id'] = chatId.value
|
||||
chatList.value.unshift(newChatItem.value)
|
||||
newChatItem.value = null // 只追加一次
|
||||
}
|
||||
// 获取用户信息
|
||||
const user = await checkSession()
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
|
||||
enableInput()
|
||||
lineBuffer.value = '' // 清空缓冲
|
||||
// 获取聊天列表
|
||||
const chatRes = await httpGet('/api/chat/list')
|
||||
allChats.value = chatRes.data
|
||||
chatList.value = allChats.value
|
||||
if (chatId.value) {
|
||||
loadChatHistory(chatId.value)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
isLogin.value = false
|
||||
} else {
|
||||
console.warn('初始化数据失败:' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送 SSE 请求
|
||||
const sendSSERequest = async (message) => {
|
||||
try {
|
||||
await fetchEventSource('/api/chat/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: getUserToken(),
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
openWhenHidden: true,
|
||||
onopen(response) {
|
||||
if (response.ok && response.status === 200) {
|
||||
console.log('SSE connection opened')
|
||||
} else {
|
||||
throw new Error(`Failed to open SSE connection: ${response.status}`)
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
try {
|
||||
const data = JSON.parse(msg.data)
|
||||
if (data.type === 'error') {
|
||||
ElMessage.error(data.body)
|
||||
enableInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'end') {
|
||||
enableInput()
|
||||
lineBuffer.value = '' // 清空缓冲
|
||||
|
||||
// 获取 token
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
httpPost('/api/chat/tokens', {
|
||||
text: '',
|
||||
model: getModelValue(modelID.value),
|
||||
chat_id: chatId.value,
|
||||
})
|
||||
.then((res) => {
|
||||
reply['created_at'] = new Date().getTime()
|
||||
reply['tokens'] = res.data
|
||||
// 将聊天框的滚动条滑动到最底部
|
||||
nextTick(() => {
|
||||
document
|
||||
.getElementById('chat-box')
|
||||
.scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
isNewMsg.value = true
|
||||
tmpChatTitle.value = message.prompt
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'text') {
|
||||
if (isNewMsg.value) {
|
||||
isNewMsg.value = false
|
||||
lineBuffer.value = data.body
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
if (reply) {
|
||||
reply['content'].text = lineBuffer.value
|
||||
}
|
||||
} else {
|
||||
lineBuffer.value += data.body
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
if (reply) {
|
||||
reply['content'].text = lineBuffer.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 token
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
httpPost('/api/chat/tokens', {
|
||||
text: '',
|
||||
model: getModelValue(modelID.value),
|
||||
chat_id: chatId.value,
|
||||
})
|
||||
.then((res) => {
|
||||
reply['created_at'] = new Date().getTime()
|
||||
reply['tokens'] = res.data
|
||||
// 将聊天框的滚动条滑动到最底部
|
||||
nextTick(() => {
|
||||
document
|
||||
.getElementById('chat-box')
|
||||
.scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
localStorage.setItem('chat_id', chatId.value)
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
isNewMsg.value = true
|
||||
} else if (data.type === 'text') {
|
||||
lineBuffer.value += data.body
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
if (reply) {
|
||||
reply['content'] = lineBuffer.value
|
||||
}
|
||||
}
|
||||
// 将聊天框的滚动条滑动到最底部
|
||||
nextTick(() => {
|
||||
document
|
||||
.getElementById('chat-box')
|
||||
.scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
localStorage.setItem('chat_id', chatId.value)
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error)
|
||||
enableInput()
|
||||
ElMessage.error('消息处理出错,请重试')
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
console.error('SSE Error:', err)
|
||||
enableInput()
|
||||
ElMessage.error('连接已断开,请重试')
|
||||
},
|
||||
onclose() {
|
||||
console.log('SSE connection closed')
|
||||
enableInput()
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error)
|
||||
enableInput()
|
||||
ElMessage.error('发送消息失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = (messageId) => {
|
||||
if (!isLogin.value) {
|
||||
console.log('未登录')
|
||||
store.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (canSend.value === false) {
|
||||
ElMessage.warning('AI 正在作答中,请稍后...')
|
||||
return
|
||||
}
|
||||
|
||||
if (prompt.value.trim().length === 0 || canSend.value === false) {
|
||||
showMessageError('请输入要发送的消息!')
|
||||
return false
|
||||
}
|
||||
|
||||
// 追加消息
|
||||
chatData.value.push({
|
||||
type: 'prompt',
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: {
|
||||
text: prompt.value,
|
||||
files: files.value,
|
||||
},
|
||||
model: getModelValue(modelID.value),
|
||||
created_at: new Date().getTime() / 1000,
|
||||
})
|
||||
|
||||
// 初始化模型分类和分组
|
||||
updateModelCategories()
|
||||
updateGroupedModels()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
store.removeMessageHandler('chat')
|
||||
})
|
||||
// 初始化数据
|
||||
const initData = () => {
|
||||
// 加载模型
|
||||
httpGet('/api/model/list?type=chat')
|
||||
.then((res) => {
|
||||
models.value = res.data
|
||||
if (!modelID.value) {
|
||||
modelID.value = models.value[0].id
|
||||
}
|
||||
// 加载角色列表
|
||||
httpGet(`/api/app/list/user`, { id: roleId.value })
|
||||
.then((res) => {
|
||||
roles.value = res.data
|
||||
if (!roleId.value) {
|
||||
roleId.value = roles.value[0]['id']
|
||||
}
|
||||
|
||||
// 如果登录状态就创建对话连接
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
newChat()
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取聊天角色失败: ' + e.messages)
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('加载模型失败: ' + e.message)
|
||||
})
|
||||
|
||||
// 获取会话列表
|
||||
httpGet('/api/chat/list')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
chatList.value = res.data
|
||||
allChats.value = res.data
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('加载会话列表失败!')
|
||||
})
|
||||
|
||||
// 允许在输入框粘贴文件
|
||||
inputRef.value.addEventListener('paste', (event) => {
|
||||
const items = (event.clipboardData || window.clipboardData).items
|
||||
for (let item of items) {
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile()
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
loading.value = true
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData)
|
||||
.then((res) => {
|
||||
files.value.push(res.data)
|
||||
ElMessage.success({ message: '上传成功', duration: 500 })
|
||||
loading.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('文件上传失败:' + e.message)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
// 添加空回复消息
|
||||
const _role = getRoleById(roleId.value)
|
||||
chatData.value.push({
|
||||
chat_id: chatId,
|
||||
role_id: roleId.value,
|
||||
type: 'reply',
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
content: {
|
||||
text: '',
|
||||
files: [],
|
||||
},
|
||||
})
|
||||
|
||||
nextTick(() => {
|
||||
document
|
||||
.getElementById('chat-box')
|
||||
.scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
})
|
||||
|
||||
showHello.value = false
|
||||
disableInput(false)
|
||||
|
||||
// 异步发送 SSE 请求
|
||||
sendSSERequest({
|
||||
user_id: loginUser.value.id,
|
||||
role_id: roleId.value,
|
||||
model_id: modelID.value,
|
||||
chat_id: chatId.value,
|
||||
prompt: prompt.value,
|
||||
tools: toolSelected.value,
|
||||
stream: stream.value,
|
||||
files: files.value,
|
||||
last_msg_id: messageId,
|
||||
})
|
||||
|
||||
prompt.value = ''
|
||||
files.value = []
|
||||
row.value = 1
|
||||
}
|
||||
|
||||
const getRoleById = function (rid) {
|
||||
@@ -854,7 +927,6 @@ const getRoleById = function (rid) {
|
||||
|
||||
const resizeElement = function () {
|
||||
chatListHeight.value = window.innerHeight - 240
|
||||
// chatBoxHeight.value = window.innerHeight;
|
||||
mainWinHeight.value = window.innerHeight - 50
|
||||
chatBoxHeight.value = window.innerHeight - 101 - 82 - 38
|
||||
}
|
||||
@@ -1036,85 +1108,6 @@ const autofillPrompt = (text) => {
|
||||
inputRef.value.focus()
|
||||
sendMessage()
|
||||
}
|
||||
// 发送消息
|
||||
const sendMessage = function () {
|
||||
if (!isLogin.value) {
|
||||
console.log('未登录')
|
||||
store.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (store.socket.conn.readyState !== WebSocket.OPEN) {
|
||||
ElMessage.warning('连接断开,正在重连...')
|
||||
return
|
||||
}
|
||||
|
||||
if (canSend.value === false) {
|
||||
ElMessage.warning('AI 正在作答中,请稍后...')
|
||||
return
|
||||
}
|
||||
|
||||
if (prompt.value.trim().length === 0 || canSend.value === false) {
|
||||
showMessageError('请输入要发送的消息!')
|
||||
return false
|
||||
}
|
||||
// 如果携带了文件,则串上文件地址
|
||||
let content = prompt.value
|
||||
if (files.value.length > 0) {
|
||||
content += files.value.map((file) => file.url).join(' ')
|
||||
}
|
||||
// else if (files.value.length > 1) {
|
||||
// showMessageError("当前只支持上传一个文件!");
|
||||
// return false;
|
||||
// }
|
||||
// 追加消息
|
||||
chatData.value.push({
|
||||
type: 'prompt',
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: content,
|
||||
model: getModelValue(modelID.value),
|
||||
created_at: new Date().getTime() / 1000,
|
||||
})
|
||||
// 添加空回复消息
|
||||
const _role = getRoleById(roleId.value)
|
||||
chatData.value.push({
|
||||
chat_id: chatId,
|
||||
role_id: roleId.value,
|
||||
type: 'reply',
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
content: '',
|
||||
})
|
||||
|
||||
nextTick(() => {
|
||||
document
|
||||
.getElementById('chat-box')
|
||||
.scrollTo(0, document.getElementById('chat-box').scrollHeight)
|
||||
})
|
||||
|
||||
showHello.value = false
|
||||
disableInput(false)
|
||||
store.socket.conn.send(
|
||||
JSON.stringify({
|
||||
channel: 'chat',
|
||||
type: 'text',
|
||||
body: {
|
||||
role_id: roleId.value,
|
||||
model_id: modelID.value,
|
||||
chat_id: chatId.value,
|
||||
content: content,
|
||||
tools: toolSelected.value,
|
||||
stream: stream.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
tmpChatTitle.value = content
|
||||
prompt.value = ''
|
||||
files.value = []
|
||||
row.value = 1
|
||||
return true
|
||||
}
|
||||
|
||||
const clearAllChats = function () {
|
||||
ElMessageBox.confirm('清除所有对话?此操作不可撤销!', '警告', {
|
||||
@@ -1157,7 +1150,10 @@ const loadChatHistory = function (chatId) {
|
||||
type: 'reply',
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
content: _role['hello_msg'],
|
||||
content: {
|
||||
text: _role['hello_msg'],
|
||||
files: [],
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1181,6 +1177,7 @@ const loadChatHistory = function (chatId) {
|
||||
})
|
||||
}
|
||||
|
||||
// 停止生成
|
||||
const stopGenerate = function () {
|
||||
showStopGenerate.value = false
|
||||
httpGet('/api/chat/stop?session_id=' + getClientId()).then(() => {
|
||||
@@ -1189,30 +1186,100 @@ const stopGenerate = function () {
|
||||
}
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = function (prompt) {
|
||||
disableInput(false)
|
||||
const text = '重新回答下述问题:' + prompt
|
||||
// 追加消息
|
||||
chatData.value.push({
|
||||
type: 'prompt',
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: text,
|
||||
const reGenerate = function (messageId) {
|
||||
// 恢复发送按钮状态
|
||||
canSend.value = true
|
||||
showStopGenerate.value = false
|
||||
console.log(messageId)
|
||||
|
||||
chatData.value = chatData.value.filter((item) => item.id < messageId)
|
||||
// 保存用户消息内容,填入输入框
|
||||
const userPrompt = chatData.value[chatData.value.length - 1].content.text
|
||||
// 删除用户消息
|
||||
const lastMessage = chatData.value.pop()
|
||||
// 填入输入框
|
||||
prompt.value = userPrompt
|
||||
sendMessage(lastMessage.id)
|
||||
// 将光标定位到输入框并聚焦
|
||||
nextTick(() => {
|
||||
if (inputRef.value) {
|
||||
inputRef.value.focus()
|
||||
// 触发输入事件以更新文本高度
|
||||
onInput({ keyCode: null })
|
||||
}
|
||||
})
|
||||
store.socket.conn.send(
|
||||
JSON.stringify({
|
||||
channel: 'chat',
|
||||
type: 'text',
|
||||
body: {
|
||||
}
|
||||
|
||||
// 编辑用户消息
|
||||
const editUserPrompt = function (messageId) {
|
||||
// 找到要编辑的消息及其索引
|
||||
let messageIndex = -1
|
||||
let messageContent = ''
|
||||
|
||||
for (let i = 0; i < chatData.value.length; i++) {
|
||||
if (chatData.value[i].id === messageId) {
|
||||
messageIndex = i
|
||||
messageContent = chatData.value[i].content
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (messageIndex === -1) return
|
||||
|
||||
// 弹出编辑对话框
|
||||
ElMessageBox.prompt('', '编辑消息', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: messageContent,
|
||||
inputType: 'textarea',
|
||||
customClass: 'edit-prompt-dialog',
|
||||
roundButton: true,
|
||||
})
|
||||
.then(({ value }) => {
|
||||
if (value.trim() === '') {
|
||||
ElMessage.warning('消息内容不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户消息
|
||||
chatData.value[messageIndex].content = value
|
||||
|
||||
// 移除该消息之后的所有消息
|
||||
chatData.value = chatData.value.slice(0, messageIndex + 1)
|
||||
|
||||
// 添加空回复消息
|
||||
const _role = getRoleById(roleId.value)
|
||||
chatData.value.push({
|
||||
chat_id: chatId,
|
||||
role_id: roleId.value,
|
||||
model_id: modelID.value,
|
||||
chat_id: chatId.value,
|
||||
content: text,
|
||||
tools: toolSelected.value,
|
||||
stream: stream.value,
|
||||
},
|
||||
type: 'reply',
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
content: '',
|
||||
})
|
||||
|
||||
disableInput(false)
|
||||
|
||||
// 发送编辑后的消息
|
||||
store.socket.conn.send(
|
||||
JSON.stringify({
|
||||
channel: 'chat',
|
||||
type: 'text',
|
||||
body: {
|
||||
role_id: roleId.value,
|
||||
model_id: modelID.value,
|
||||
chat_id: chatId.value,
|
||||
content: value,
|
||||
tools: toolSelected.value,
|
||||
stream: stream.value,
|
||||
edit_message: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消编辑
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const chatName = ref('')
|
||||
@@ -1296,11 +1363,11 @@ const realtimeChat = () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import "@/assets/css/chat-plus.styl"
|
||||
@import '../assets/css/chat-plus.styl'
|
||||
</style>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '@/assets/css/markdown/vue.css';
|
||||
@import '../assets/css/markdown/vue.css';
|
||||
.notice-dialog {
|
||||
.el-dialog__header {
|
||||
padding-bottom 0
|
||||
|
||||
@@ -288,16 +288,16 @@
|
||||
<script setup>
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Delete, InfoFilled, Picture } from '@element-plus/icons-vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Clipboard from 'clipboard'
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import { showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { Delete, InfoFilled } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
|
||||
@@ -585,7 +585,7 @@ const changeModel = (model) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '@/assets/css/image-dall.styl';
|
||||
@import '@/assets/css/custom-scroll.styl';
|
||||
<style lang="stylus" scoped>
|
||||
@import '../assets/css/image-dall.styl';
|
||||
@import '../assets/css/custom-scroll.styl';
|
||||
</style>
|
||||
|
||||
@@ -168,7 +168,7 @@ const routerViewKey = ref(0)
|
||||
const showConfigDialog = ref(false)
|
||||
const license = ref({ de_copy: true })
|
||||
const showLoginDialog = ref(false)
|
||||
const githubURL = ref(process.env.VUE_APP_GITHUB_URL)
|
||||
const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
|
||||
|
||||
/**
|
||||
* 从路径名中提取第一个路径段
|
||||
@@ -297,6 +297,6 @@ const loginSuccess = () => {
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import "@/assets/css/home.styl"
|
||||
@import "../assets/css/custom-scroll.styl"
|
||||
@import "../assets/css/home.styl"
|
||||
</style>
|
||||
|
||||
@@ -204,7 +204,10 @@
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>如需自定义比例,在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h 例如: 1 cat --ar 21:9 </span>
|
||||
<span
|
||||
>如需自定义比例,在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h
|
||||
例如: 1 cat --ar 21:9
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,8 +333,11 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>如需自定义比例,在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h 例如: 1 cat --ar 21:9 </span>
|
||||
</div>
|
||||
<span
|
||||
>如需自定义比例,在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h
|
||||
例如: 1 cat --ar 21:9
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -545,7 +551,10 @@
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<div class="flex-row justify-start items-center">
|
||||
<span>如需自定义比例,在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h 例如: 1 cat --ar 21:9 </span>
|
||||
<span
|
||||
>如需自定义比例,在绘画指令最后加一个空格然后加上指令(宽高比) --ar w:h
|
||||
例如: 1 cat --ar 21:9
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1213,8 +1222,8 @@ const generate = () => {
|
||||
return ElMessage.error('换脸操作需要上传两张图片')
|
||||
}
|
||||
|
||||
const regex = /(^|\s)--ar\s+(\d+:\d+)/;
|
||||
const match = regex.exec(params.value.prompt);
|
||||
const regex = /(^|\s)--ar\s+(\d+:\d+)/
|
||||
const match = regex.exec(params.value.prompt)
|
||||
if (match) {
|
||||
params.value.rate = match[2]
|
||||
}
|
||||
@@ -1349,6 +1358,6 @@ const generatePrompt = () => {
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '@/assets/css/image-mj.styl';
|
||||
@import '@/assets/css/custom-scroll.styl';
|
||||
@import '../assets/css/image-mj.styl';
|
||||
@import '../assets/css/custom-scroll.styl';
|
||||
</style>
|
||||
|
||||
@@ -471,21 +471,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Delete, DocumentCopy, InfoFilled, Orange } from '@element-plus/icons-vue'
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import { Delete, InfoFilled, Orange } from '@element-plus/icons-vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Clipboard from 'clipboard'
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import SdTaskView from '@/components/SdTaskView.vue'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import SdTaskView from '@/components/SdTaskView.vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
|
||||
@@ -753,6 +753,6 @@ const generatePrompt = () => {
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '@/assets/css/image-sd.styl';
|
||||
@import '@/assets/css/custom-scroll.styl';
|
||||
@import '../assets/css/image-sd.styl';
|
||||
@import '../assets/css/custom-scroll.styl';
|
||||
</style>
|
||||
|
||||
@@ -11,11 +11,7 @@
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="waterfall"
|
||||
:style="{ height: listBoxHeight + 'px' }"
|
||||
id="waterfall-box"
|
||||
>
|
||||
<div class="waterfall" :style="{ height: listBoxHeight + 'px' }" id="waterfall-box">
|
||||
<Waterfall
|
||||
v-if="imgType === 'mj'"
|
||||
id="waterfall-mj"
|
||||
@@ -55,11 +51,7 @@
|
||||
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
|
||||
>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-tooltip class="box-item" content="复制提示词" placement="top">
|
||||
<el-button
|
||||
type="info"
|
||||
circle
|
||||
@@ -70,16 +62,8 @@
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="画同款"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@click="drawSameMj(item)"
|
||||
>
|
||||
<el-tooltip class="box-item" content="画同款" placement="top">
|
||||
<el-button type="primary" circle @click="drawSameMj(item)">
|
||||
<i class="iconfont icon-palette"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
@@ -129,11 +113,7 @@
|
||||
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
|
||||
>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-tooltip class="box-item" content="复制提示词" placement="top">
|
||||
<el-button
|
||||
type="info"
|
||||
circle
|
||||
@@ -144,16 +124,8 @@
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="画同款"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@click="drawSameSd(item)"
|
||||
>
|
||||
<el-tooltip class="box-item" content="画同款" placement="top">
|
||||
<el-button type="primary" circle @click="drawSameSd(item)">
|
||||
<i class="iconfont icon-palette"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
@@ -203,11 +175,7 @@
|
||||
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
|
||||
>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-tooltip class="box-item" content="复制提示词" placement="top">
|
||||
<el-button
|
||||
type="info"
|
||||
circle
|
||||
@@ -261,7 +229,7 @@
|
||||
<el-image-viewer
|
||||
@close="
|
||||
() => {
|
||||
previewURL = '';
|
||||
previewURL = ''
|
||||
}
|
||||
"
|
||||
v-if="previewURL !== ''"
|
||||
@@ -271,142 +239,139 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { DocumentCopy, Picture } from "@element-plus/icons-vue";
|
||||
import { httpGet } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import { useRouter } from "vue-router";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
import SdTaskView from "@/components/SdTaskView.vue";
|
||||
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
|
||||
import "vue-waterfall-plugin-next/dist/style.css";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import SdTaskView from '@/components/SdTaskView.vue'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
|
||||
const store = useSharedStore();
|
||||
const waterfallOptions = store.waterfallOptions;
|
||||
const store = useSharedStore()
|
||||
const waterfallOptions = store.waterfallOptions
|
||||
|
||||
const data = ref({
|
||||
mj: [],
|
||||
sd: [],
|
||||
dall: [],
|
||||
});
|
||||
const loading = ref(true);
|
||||
const isOver = ref(false);
|
||||
const imgType = ref("mj"); // 图片类别
|
||||
const listBoxHeight = window.innerHeight - 124;
|
||||
const showTaskDialog = ref(false);
|
||||
const item = ref({});
|
||||
const previewURL = ref("");
|
||||
})
|
||||
const loading = ref(true)
|
||||
const isOver = ref(false)
|
||||
const imgType = ref('mj') // 图片类别
|
||||
const listBoxHeight = window.innerHeight - 124
|
||||
const showTaskDialog = ref(false)
|
||||
const item = ref({})
|
||||
const previewURL = ref('')
|
||||
|
||||
const previewImg = (item) => {
|
||||
previewURL.value = item.img_url;
|
||||
};
|
||||
previewURL.value = item.img_url
|
||||
}
|
||||
|
||||
const page = ref(0);
|
||||
const pageSize = ref(15);
|
||||
const page = ref(0)
|
||||
const pageSize = ref(15)
|
||||
// 获取下一页数据
|
||||
const getNext = () => {
|
||||
if (isOver.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
page.value = page.value + 1;
|
||||
let url = "";
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
let url = ''
|
||||
switch (imgType.value) {
|
||||
case "mj":
|
||||
url = "/api/mj/imgWall";
|
||||
break;
|
||||
case "sd":
|
||||
url = "/api/sd/imgWall";
|
||||
break;
|
||||
case "dall":
|
||||
url = "/api/dall/imgWall";
|
||||
break;
|
||||
case 'mj':
|
||||
url = '/api/mj/imgWall'
|
||||
break
|
||||
case 'sd':
|
||||
url = '/api/sd/imgWall'
|
||||
break
|
||||
case 'dall':
|
||||
url = '/api/dall/imgWall'
|
||||
break
|
||||
}
|
||||
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`)
|
||||
.then((res) => {
|
||||
if (!res.data.items || res.data.items.length === 0) {
|
||||
isOver.value = true;
|
||||
loading.value = false;
|
||||
return;
|
||||
isOver.value = true
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 生成缩略图
|
||||
const imageList = res.data.items;
|
||||
const imageList = res.data.items
|
||||
for (let i = 0; i < imageList.length; i++) {
|
||||
imageList[i]["img_thumb"] =
|
||||
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
|
||||
imageList[i]['img_thumb'] = imageList[i]['img_url'] + '?imageView2/4/w/300/h/0/q/75'
|
||||
}
|
||||
if (data.value[imgType.value].length === 0) {
|
||||
data.value[imgType.value] = imageList;
|
||||
return;
|
||||
data.value[imgType.value] = imageList
|
||||
return
|
||||
}
|
||||
|
||||
if (imageList.length < pageSize.value) {
|
||||
isOver.value = true;
|
||||
isOver.value = true
|
||||
}
|
||||
data.value[imgType.value] = data.value[imgType.value].concat(imageList);
|
||||
data.value[imgType.value] = data.value[imgType.value].concat(imageList)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取图片失败:" + e.message);
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取图片失败:' + e.message)
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
getNext();
|
||||
getNext()
|
||||
|
||||
const clipboard = ref(null);
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
clipboard.value = new Clipboard(".copy-prompt-wall");
|
||||
clipboard.value.on("success", () => {
|
||||
ElMessage.success("复制成功!");
|
||||
});
|
||||
clipboard.value = new Clipboard('.copy-prompt-wall')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
|
||||
clipboard.value.on("error", () => {
|
||||
ElMessage.error("复制失败!");
|
||||
});
|
||||
});
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
});
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
const changeImgType = () => {
|
||||
console.log(imgType.value);
|
||||
document.getElementById("waterfall-box").scrollTo(0, 0);
|
||||
page.value = 0;
|
||||
console.log(imgType.value)
|
||||
document.getElementById('waterfall-box').scrollTo(0, 0)
|
||||
page.value = 0
|
||||
data.value = {
|
||||
mj: [],
|
||||
sd: [],
|
||||
dall: [],
|
||||
};
|
||||
loading.value = true;
|
||||
isOver.value = false;
|
||||
nextTick(() => getNext());
|
||||
};
|
||||
}
|
||||
loading.value = true
|
||||
isOver.value = false
|
||||
nextTick(() => getNext())
|
||||
}
|
||||
|
||||
const showTask = (row) => {
|
||||
item.value = row;
|
||||
showTaskDialog.value = true;
|
||||
};
|
||||
item.value = row
|
||||
showTaskDialog.value = true
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
const drawSameSd = (row) => {
|
||||
router.push({
|
||||
name: "image-sd",
|
||||
name: 'image-sd',
|
||||
params: { copyParams: JSON.stringify(row.params) },
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const drawSameMj = (row) => {
|
||||
router.push({ name: "image-mj", params: { prompt: row.prompt } });
|
||||
};
|
||||
router.push({ name: 'image-mj', params: { prompt: row.prompt } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '@/assets/css/images-wall.styl';
|
||||
@import '@/assets/css/custom-scroll.styl';
|
||||
@import '../assets/css/images-wall.styl';
|
||||
@import '../assets/css/custom-scroll.styl';
|
||||
</style>
|
||||
|
||||
@@ -34,6 +34,15 @@
|
||||
>登录/注册</el-button
|
||||
>
|
||||
</span>
|
||||
<span v-if="isLogin">
|
||||
<el-button
|
||||
@click="logout"
|
||||
class="btn-go animate__animated animate__pulse animate__infinite"
|
||||
round
|
||||
>
|
||||
退出登录
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</el-menu>
|
||||
</div>
|
||||
@@ -75,17 +84,17 @@
|
||||
import FooterBar from '@/components/FooterBar.vue'
|
||||
import ThemeChange from '@/components/ThemeChange.vue'
|
||||
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { removeUserToken } from '@/store/session'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { isMobile } from '@/utils/libs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
if (isMobile()) {
|
||||
router.push('/mobile/index')
|
||||
}
|
||||
// if (isMobile()) {
|
||||
// router.push('/mobile/index')
|
||||
// }
|
||||
|
||||
const title = ref('')
|
||||
const logo = ref('')
|
||||
@@ -93,9 +102,9 @@ const slogan = ref('')
|
||||
const license = ref({ de_copy: true })
|
||||
|
||||
const isLogin = ref(false)
|
||||
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
|
||||
const githubURL = ref(process.env.VUE_APP_GITHUB_URL)
|
||||
const giteeURL = ref(process.env.VUE_APP_GITEE_URL)
|
||||
const docsURL = ref(import.meta.env.VITE_DOCS_URL)
|
||||
const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
|
||||
const giteeURL = ref(import.meta.env.VITE_GITEE_URL)
|
||||
const navs = ref([])
|
||||
|
||||
const iconMap = ref({
|
||||
@@ -195,8 +204,19 @@ const rainbowColor = (index) => {
|
||||
const hue = (index * 40) % 360 // 每个字符间隔40度,形成彩虹色
|
||||
return `hsl(${hue}, 90%, 50%)` // 色调(hue),饱和度(70%),亮度(50%)
|
||||
}
|
||||
|
||||
const logout = function () {
|
||||
httpGet('/api/user/logout')
|
||||
.then(() => {
|
||||
removeUserToken()
|
||||
router.push('/login')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('注销失败:' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/index.styl"
|
||||
@import '../assets/css/index.styl'
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<div class="info">
|
||||
我们非常欢迎您把此应用分享给您身边的朋友,分享成功注册后您和被邀请人都将获得
|
||||
<strong>{{ invitePower }}</strong>
|
||||
算力额度作为奖励。 你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友。
|
||||
算力额度作为奖励。
|
||||
你可以保存下面的二维码或者直接复制分享您的专属推广链接发送给微信好友。
|
||||
</div>
|
||||
|
||||
<div class="invite-qrcode">
|
||||
@@ -16,7 +17,9 @@
|
||||
|
||||
<div class="invite-url">
|
||||
<span>{{ inviteURL }}</span>
|
||||
<el-button type="primary" plain class="copy-link" :data-clipboard-text="inviteURL">复制链接</el-button>
|
||||
<el-button type="primary" plain class="copy-link" :data-clipboard-text="inviteURL"
|
||||
>复制链接</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,79 +91,79 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import QRCode from "qrcode";
|
||||
import { httpGet } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import InviteList from "@/components/InviteList.vue";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import InviteList from '@/components/InviteList.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const inviteURL = ref("");
|
||||
const qrImg = ref("/images/wx.png");
|
||||
const invitePower = ref(0);
|
||||
const hits = ref(0);
|
||||
const regNum = ref(0);
|
||||
const rate = ref(0);
|
||||
const isLogin = ref(false);
|
||||
const store = useSharedStore();
|
||||
const inviteURL = ref('')
|
||||
const qrImg = ref('/images/wx.png')
|
||||
const invitePower = ref(0)
|
||||
const hits = ref(0)
|
||||
const regNum = ref(0)
|
||||
const rate = ref(0)
|
||||
const isLogin = ref(false)
|
||||
const store = useSharedStore()
|
||||
|
||||
onMounted(() => {
|
||||
initData();
|
||||
initData()
|
||||
|
||||
// 复制链接
|
||||
const clipboard = new Clipboard(".copy-link");
|
||||
clipboard.on("success", () => {
|
||||
ElMessage.success("复制成功!");
|
||||
});
|
||||
const clipboard = new Clipboard('.copy-link')
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
|
||||
clipboard.on("error", () => {
|
||||
ElMessage.error("复制失败!");
|
||||
});
|
||||
});
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
checkSession()
|
||||
.then(() => {
|
||||
isLogin.value = true;
|
||||
httpGet("/api/invite/code")
|
||||
isLogin.value = true
|
||||
httpGet('/api/invite/code')
|
||||
.then((res) => {
|
||||
const text = `${location.protocol}//${location.host}/register?invite_code=${res.data.code}`;
|
||||
hits.value = res.data["hits"];
|
||||
regNum.value = res.data["reg_num"];
|
||||
const text = `${location.protocol}//${location.host}/register?invite_code=${res.data.code}`
|
||||
hits.value = res.data['hits']
|
||||
regNum.value = res.data['reg_num']
|
||||
if (hits.value > 0) {
|
||||
rate.value = ((regNum.value / hits.value) * 100).toFixed(2);
|
||||
rate.value = ((regNum.value / hits.value) * 100).toFixed(2)
|
||||
}
|
||||
QRCode.toDataURL(text, { width: 400, height: 400, margin: 2 }, (error, url) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
} else {
|
||||
qrImg.value = url;
|
||||
qrImg.value = url
|
||||
}
|
||||
});
|
||||
inviteURL.value = text;
|
||||
})
|
||||
inviteURL.value = text
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取邀请码失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取邀请码失败:' + e.message)
|
||||
})
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
invitePower.value = res.data["invite_power"];
|
||||
invitePower.value = res.data['invite_power']
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
store.setShowLoginDialog(true);
|
||||
});
|
||||
};
|
||||
store.setShowLoginDialog(true)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import '../assets/css/custom-scroll.styl'
|
||||
.page-invitation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
<el-col :span="8" v-for="item in rates" :key="item.value">
|
||||
<div
|
||||
class="flex-col items-center"
|
||||
:class="item.value === params.aspect_ratio ? 'grid-content active' : 'grid-content'"
|
||||
:class="
|
||||
item.value === params.aspect_ratio ? 'grid-content active' : 'grid-content'
|
||||
"
|
||||
@click="changeRate(item)"
|
||||
>
|
||||
<el-image class="icon proportion" :src="item.img" fit="cover"></el-image>
|
||||
@@ -35,8 +37,17 @@
|
||||
<!-- 模型选择 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="模型选择">
|
||||
<el-select v-model="params.model" placeholder="请选择模型" @change="updateModelPower">
|
||||
<el-option v-for="item in models" :key="item.value" :label="item.text" :value="item.value" />
|
||||
<el-select
|
||||
v-model="params.model"
|
||||
placeholder="请选择模型"
|
||||
@change="updateModelPower"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.value"
|
||||
:label="item.text"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -44,7 +55,11 @@
|
||||
<!-- 视频时长 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="视频时长">
|
||||
<el-select v-model="params.duration" placeholder="请选择时长" @change="updateModelPower">
|
||||
<el-select
|
||||
v-model="params.duration"
|
||||
placeholder="请选择时长"
|
||||
@change="updateModelPower"
|
||||
>
|
||||
<el-option label="5秒" value="5" />
|
||||
<el-option label="10秒" value="10" />
|
||||
</el-select>
|
||||
@@ -54,7 +69,11 @@
|
||||
<!-- 生成模式 -->
|
||||
<div class="param-line">
|
||||
<el-form-item label="生成模式">
|
||||
<el-select v-model="params.mode" placeholder="请选择模式" @change="updateModelPower">
|
||||
<el-select
|
||||
v-model="params.mode"
|
||||
placeholder="请选择模式"
|
||||
@change="updateModelPower"
|
||||
>
|
||||
<el-option label="标准模式" value="std" />
|
||||
<el-option label="专业模式" value="pro" />
|
||||
</el-select>
|
||||
@@ -94,7 +113,11 @@
|
||||
<!-- 仅在simple模式下显示详细配置 -->
|
||||
<div class="camera-control mt-2" v-if="params.camera_control.type === 'simple'">
|
||||
<el-form-item label="水平移动">
|
||||
<el-slider v-model="params.camera_control.config.horizontal" :min="-10" :max="10" />
|
||||
<el-slider
|
||||
v-model="params.camera_control.config.horizontal"
|
||||
:min="-10"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="垂直移动">
|
||||
<el-slider v-model="params.camera_control.config.vertical" :min="-10" :max="10" />
|
||||
@@ -126,7 +149,10 @@
|
||||
<div class="text">使用文字描述想要生成视频的内容</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="图生视频" name="image2video">
|
||||
<div class="text">以某张图片为底稿参考来创作视频,生成类似风格或类型视频,支持 PNG /JPG/JPEG 格式图片;</div>
|
||||
<div class="text">
|
||||
以某张图片为底稿参考来创作视频,生成类似风格或类型视频,支持 PNG /JPG/JPEG
|
||||
格式图片;
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
@@ -142,7 +168,13 @@
|
||||
placeholder="请在此输入视频提示词,您也可以点击下面的提示词助手生成视频提示词"
|
||||
/>
|
||||
<el-row class="text-info">
|
||||
<el-button class="generate-btn" @click="generatePrompt" :loading="isGenerating" size="small" color="#5865f2">
|
||||
<el-button
|
||||
class="generate-btn"
|
||||
@click="generatePrompt"
|
||||
:loading="isGenerating"
|
||||
size="small"
|
||||
color="#5865f2"
|
||||
>
|
||||
<i class="iconfont icon-chuangzuo"></i>
|
||||
生成专业视频提示词
|
||||
</el-button>
|
||||
@@ -152,10 +184,18 @@
|
||||
<div v-else class="image2video">
|
||||
<div class="image-upload img-inline">
|
||||
<div class="upload-box img-uploader video-img-box">
|
||||
<el-icon v-if="params.image" @click="removeImage('start')" class="removeimg"><CircleCloseFilled /></el-icon>
|
||||
<el-icon v-if="params.image" @click="removeImage('start')" class="removeimg"
|
||||
><CircleCloseFilled
|
||||
/></el-icon>
|
||||
|
||||
<h4>起始帧</h4>
|
||||
<el-upload class="uploader img-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadStartImage" accept=".jpg,.png,.jpeg">
|
||||
<el-upload
|
||||
class="uploader img-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadStartImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<img v-if="params.image" :src="params.image" class="preview" />
|
||||
|
||||
<el-icon v-else class="upload-icon"><Plus /></el-icon>
|
||||
@@ -165,9 +205,17 @@
|
||||
<i class="iconfont icon-exchange" @click="switchReverse"></i>
|
||||
</div>
|
||||
<div class="upload-box img-uploader video-img-box">
|
||||
<el-icon v-if="params.image_tail" @click="removeImage('end')" class="removeimg"><CircleCloseFilled /></el-icon>
|
||||
<el-icon v-if="params.image_tail" @click="removeImage('end')" class="removeimg"
|
||||
><CircleCloseFilled
|
||||
/></el-icon>
|
||||
<h4>结束帧</h4>
|
||||
<el-upload class="uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadEndImage" accept=".jpg,.png,.jpeg">
|
||||
<el-upload
|
||||
class="uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadEndImage"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<img v-if="params.image_tail" :src="params.image_tail" class="preview" />
|
||||
<el-icon v-else class="upload-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
@@ -186,7 +234,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-line pt">
|
||||
<el-input v-model="params.prompt" type="textarea" :autosize="{ minRows: 4, maxRows: 6 }" placeholder="描述视频画面细节" />
|
||||
<el-input
|
||||
v-model="params.prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
placeholder="描述视频画面细节"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -238,8 +291,19 @@
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<div v-if="item.progress === 100">
|
||||
<video class="video" :src="item.video_url" preload="auto" loop="loop" muted="muted">您的浏览器不支持视频播放</video>
|
||||
<button class="play flex justify-center items-center" @click="previewVideo(item)">
|
||||
<video
|
||||
class="video"
|
||||
:src="item.video_url"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<button
|
||||
class="play flex justify-center items-center"
|
||||
@click="previewVideo(item)"
|
||||
>
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -255,7 +319,9 @@
|
||||
<el-tag class="mr-1">{{ item.raw_data.duration }}秒</el-tag>
|
||||
<el-tag class="mr-1">{{ item.raw_data.mode }}</el-tag>
|
||||
</div>
|
||||
<div class="failed" v-if="item.progress === 101">任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}</div>
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}
|
||||
</div>
|
||||
<div class="prompt" v-else>
|
||||
{{ substr(item.prompt, 1000) }}
|
||||
</div>
|
||||
@@ -275,9 +341,18 @@
|
||||
</button> -->
|
||||
|
||||
<el-tooltip content="下载视频" placement="top">
|
||||
<button class="btn btn-icon" @click="downloadVideo(item)" :disabled="item.downloading">
|
||||
<button
|
||||
class="btn btn-icon"
|
||||
@click="downloadVideo(item)"
|
||||
:disabled="item.downloading"
|
||||
>
|
||||
<i class="iconfont icon-download" v-if="!item.downloading"></i>
|
||||
<el-image src="/images/loading.gif" class="downloading" fit="cover" v-else />
|
||||
<el-image
|
||||
src="/images/loading.gif"
|
||||
class="downloading"
|
||||
fit="cover"
|
||||
v-else
|
||||
/>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
@@ -298,7 +373,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty :image-size="100" :image="nodata" description="没有任何作品,赶紧去创作吧!" v-else />
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
:image="nodata"
|
||||
description="没有任何作品,赶紧去创作吧!"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
@@ -318,7 +398,13 @@
|
||||
</div>
|
||||
|
||||
<!-- 视频预览对话框 -->
|
||||
<black-dialog v-model:show="previewVisible" title="视频预览" hide-footer @cancal="previewVisible = false" width="auto">
|
||||
<black-dialog
|
||||
v-model:show="previewVisible"
|
||||
title="视频预览"
|
||||
hide-footer
|
||||
@cancal="previewVisible = false"
|
||||
width="auto"
|
||||
>
|
||||
<video
|
||||
v-if="currentVideo"
|
||||
:src="currentVideo"
|
||||
@@ -336,47 +422,44 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import failed from "@/assets/img/failed.png";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
import { ref, reactive, onMounted, onUnmounted, watch } from "vue";
|
||||
import { Plus, Delete, InfoFilled, ChromeFilled, DocumentCopy, Download, WarnTriangleFilled, CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
import { httpGet, httpPost, httpDownload } from "@/utils/http";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import Clipboard from "clipboard";
|
||||
import BlackDialog from "@/components/ui/BlackDialog.vue";
|
||||
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
|
||||
import { replaceImg, substr } from "@/utils/libs";
|
||||
import Generating from "@/components/ui/Generating.vue";
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import BlackDialog from '@/components/ui/BlackDialog.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg, substr } from '@/utils/libs'
|
||||
import { CircleCloseFilled, InfoFilled, Plus } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
|
||||
const models = ref([
|
||||
{
|
||||
text: "可灵 1.6",
|
||||
value: "kling-v1-6",
|
||||
text: '可灵 1.6',
|
||||
value: 'kling-v1-6',
|
||||
},
|
||||
{
|
||||
text: "可灵 1.5",
|
||||
value: "kling-v1-5",
|
||||
text: '可灵 1.5',
|
||||
value: 'kling-v1-5',
|
||||
},
|
||||
{
|
||||
text: "可灵 1.0",
|
||||
value: "kling-v1",
|
||||
text: '可灵 1.0',
|
||||
value: 'kling-v1',
|
||||
},
|
||||
]);
|
||||
])
|
||||
// 参数设置
|
||||
const params = reactive({
|
||||
task_type: "text2video",
|
||||
task_type: 'text2video',
|
||||
model: models.value[0].value,
|
||||
prompt: "",
|
||||
negative_prompt: "",
|
||||
prompt: '',
|
||||
negative_prompt: '',
|
||||
cfg_scale: 0.7,
|
||||
mode: "std",
|
||||
aspect_ratio: "16:9",
|
||||
duration: "5",
|
||||
mode: 'std',
|
||||
aspect_ratio: '16:9',
|
||||
duration: '5',
|
||||
camera_control: {
|
||||
type: "",
|
||||
type: '',
|
||||
config: {
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
@@ -386,142 +469,142 @@ const params = reactive({
|
||||
zoom: 0,
|
||||
},
|
||||
},
|
||||
image: "",
|
||||
image_tail: "",
|
||||
});
|
||||
image: '',
|
||||
image_tail: '',
|
||||
})
|
||||
const rates = [
|
||||
{ css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png" },
|
||||
{ css: 'square', value: '1:1', text: '1:1', img: '/images/mj/rate_1_1.png' },
|
||||
|
||||
{
|
||||
css: "size16-9",
|
||||
value: "16:9",
|
||||
text: "16:9",
|
||||
img: "/images/mj/rate_16_9.png",
|
||||
css: 'size16-9',
|
||||
value: '16:9',
|
||||
text: '16:9',
|
||||
img: '/images/mj/rate_16_9.png',
|
||||
},
|
||||
{
|
||||
css: "size9-16",
|
||||
value: "9:16",
|
||||
text: "9:16",
|
||||
img: "/images/mj/rate_9_16.png",
|
||||
css: 'size9-16',
|
||||
value: '9:16',
|
||||
text: '9:16',
|
||||
img: '/images/mj/rate_9_16.png',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
// 切换图片比例
|
||||
const changeRate = (item) => {
|
||||
params.aspect_ratio = item.value;
|
||||
};
|
||||
params.aspect_ratio = item.value
|
||||
}
|
||||
|
||||
const generating = ref(false);
|
||||
const isGenerating = ref(false);
|
||||
const powerCost = ref(10);
|
||||
const availablePower = ref(100);
|
||||
const taskFilter = ref("all");
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const noData = ref(true);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const taskPulling = ref(true);
|
||||
const pullHandler = ref(null);
|
||||
const previewVisible = ref(false);
|
||||
const currentVideo = ref("");
|
||||
const showCameraControl = ref(false);
|
||||
const keLingPowers = ref({});
|
||||
const isLogin = ref(false);
|
||||
const generating = ref(false)
|
||||
const isGenerating = ref(false)
|
||||
const powerCost = ref(10)
|
||||
const availablePower = ref(100)
|
||||
const taskFilter = ref('all')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(true)
|
||||
const pullHandler = ref(null)
|
||||
const previewVisible = ref(false)
|
||||
const currentVideo = ref('')
|
||||
const showCameraControl = ref(false)
|
||||
const keLingPowers = ref({})
|
||||
const isLogin = ref(false)
|
||||
// 动态更新模型消耗的算力
|
||||
const updateModelPower = () => {
|
||||
showCameraControl.value = params.model === "kling-v1-5" && params.mode === "pro";
|
||||
powerCost.value = keLingPowers.value[`${params.model}_${params.mode}_${params.duration}`] || {};
|
||||
};
|
||||
showCameraControl.value = params.model === 'kling-v1-5' && params.mode === 'pro'
|
||||
powerCost.value = keLingPowers.value[`${params.model}_${params.mode}_${params.duration}`] || {}
|
||||
}
|
||||
|
||||
// tab切换
|
||||
const tabChange = (tab) => {
|
||||
params.task_type = tab;
|
||||
};
|
||||
params.task_type = tab
|
||||
}
|
||||
|
||||
const uploadStartImage = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file);
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
try {
|
||||
showLoading("图片上传中...");
|
||||
const res = await httpPost("/api/upload", formData);
|
||||
params.image = res.data.url;
|
||||
ElMessage.success("上传成功");
|
||||
closeLoading();
|
||||
showLoading('图片上传中...')
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
params.image = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
closeLoading()
|
||||
} catch (e) {
|
||||
showMessageError("上传失败: " + e.message);
|
||||
closeLoading();
|
||||
showMessageError('上传失败: ' + e.message)
|
||||
closeLoading()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//移除图片
|
||||
const removeImage = (type) => {
|
||||
if (type === "start") {
|
||||
params.image = "";
|
||||
} else if (type === "end") {
|
||||
params.image_tail = "";
|
||||
if (type === 'start') {
|
||||
params.image = ''
|
||||
} else if (type === 'end') {
|
||||
params.image_tail = ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//图片交换方法
|
||||
const switchReverse = () => {
|
||||
[params.image, params.image_tail] = [params.image_tail, params.image];
|
||||
};
|
||||
;[params.image, params.image_tail] = [params.image_tail, params.image]
|
||||
}
|
||||
const uploadEndImage = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file);
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file)
|
||||
try {
|
||||
const res = await httpPost("/api/upload", formData);
|
||||
params.image_tail = res.data.url;
|
||||
ElMessage.success("上传成功");
|
||||
const res = await httpPost('/api/upload', formData)
|
||||
params.image_tail = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} catch (e) {
|
||||
showMessageError("上传失败: " + e.message);
|
||||
showMessageError('上传失败: ' + e.message)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const generatePrompt = async () => {
|
||||
if (isGenerating.value) return;
|
||||
if (isGenerating.value) return
|
||||
if (!params.prompt) {
|
||||
return showMessageError("请输入视频描述");
|
||||
return showMessageError('请输入视频描述')
|
||||
}
|
||||
isGenerating.value = true;
|
||||
isGenerating.value = true
|
||||
try {
|
||||
const res = await httpPost("/api/prompt/video", { prompt: params.prompt });
|
||||
params.prompt = res.data;
|
||||
const res = await httpPost('/api/prompt/video', { prompt: params.prompt })
|
||||
params.prompt = res.data
|
||||
} catch (e) {
|
||||
showMessageError("生成失败: " + e.message);
|
||||
showMessageError('生成失败: ' + e.message)
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
isGenerating.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const generate = async () => {
|
||||
//增加防抖
|
||||
if (generating.value) return;
|
||||
if (generating.value) return
|
||||
if (!params.prompt?.trim()) {
|
||||
return ElMessage.error("请输入视频描述");
|
||||
return ElMessage.error('请输入视频描述')
|
||||
}
|
||||
// 提示词长度不能超过 500
|
||||
if (params.prompt.length > 500) {
|
||||
return ElMessage.error("视频描述不能超过 500 个字符");
|
||||
return ElMessage.error('视频描述不能超过 500 个字符')
|
||||
}
|
||||
if (params.task_type === "image2video" && !params.image) {
|
||||
return ElMessage.error("请上传起始帧图片");
|
||||
if (params.task_type === 'image2video' && !params.image) {
|
||||
return ElMessage.error('请上传起始帧图片')
|
||||
}
|
||||
generating.value = true;
|
||||
generating.value = true
|
||||
// 处理图片链接
|
||||
if (params.image) {
|
||||
params.image = replaceImg(params.image);
|
||||
params.image = replaceImg(params.image)
|
||||
}
|
||||
if (params.image_tail) {
|
||||
params.image_tail = replaceImg(params.image_tail);
|
||||
params.image_tail = replaceImg(params.image_tail)
|
||||
}
|
||||
try {
|
||||
await httpPost("/api/video/keling/create", params);
|
||||
showMessageOK("任务创建成功");
|
||||
await httpPost('/api/video/keling/create', params)
|
||||
showMessageOK('任务创建成功')
|
||||
// 新增重置
|
||||
page.value = 1;
|
||||
page.value = 1
|
||||
list.value.unshift({
|
||||
progress: 0,
|
||||
prompt: params.prompt,
|
||||
@@ -531,132 +614,132 @@ const generate = async () => {
|
||||
duration: params.duration,
|
||||
mode: params.mode,
|
||||
},
|
||||
});
|
||||
taskPulling.value = true;
|
||||
})
|
||||
taskPulling.value = true
|
||||
} catch (e) {
|
||||
showMessageError("创建失败: " + e.message);
|
||||
showMessageError('创建失败: ' + e.message)
|
||||
} finally {
|
||||
generating.value = false;
|
||||
generating.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page;
|
||||
page.value = _page
|
||||
}
|
||||
|
||||
httpGet("/api/video/list", {
|
||||
httpGet('/api/video/list', {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
type: "keling",
|
||||
task_type: taskFilter.value === "all" ? "" : taskFilter.value,
|
||||
type: 'keling',
|
||||
task_type: taskFilter.value === 'all' ? '' : taskFilter.value,
|
||||
})
|
||||
.then((res) => {
|
||||
total.value = res.data.total;
|
||||
let needPull = false;
|
||||
const items = [];
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const items = []
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true;
|
||||
needPull = true
|
||||
}
|
||||
items.push({
|
||||
...v,
|
||||
downloading: false,
|
||||
});
|
||||
})
|
||||
}
|
||||
loading.value = false;
|
||||
taskPulling.value = needPull;
|
||||
loading.value = false
|
||||
taskPulling.value = needPull
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items;
|
||||
list.value = items
|
||||
}
|
||||
noData.value = list.value.length === 0;
|
||||
noData.value = list.value.length === 0
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
noData.value = true;
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
noData.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const previewVideo = (task) => {
|
||||
currentVideo.value = task.video_url;
|
||||
previewVisible.value = true;
|
||||
};
|
||||
currentVideo.value = task.video_url
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
const downloadVideo = async (task) => {
|
||||
try {
|
||||
const res = await httpDownload(`/api/download?url=${replaceImg(task.video_url)}`);
|
||||
const blob = new Blob([res.data]);
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `video_${task.id}.mp4`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
const res = await httpDownload(`/api/download?url=${replaceImg(task.video_url)}`)
|
||||
const blob = new Blob([res.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = `video_${task.id}.mp4`
|
||||
link.click()
|
||||
URL.revokeObjectURL(link.href)
|
||||
} catch (e) {
|
||||
showMessageError("下载失败: " + e.message);
|
||||
showMessageError('下载失败: ' + e.message)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/video/remove", { id: item.id })
|
||||
httpGet('/api/video/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success("任务删除成功");
|
||||
fetchData(page.value);
|
||||
ElMessage.success('任务删除成功')
|
||||
fetchData(page.value)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("任务删除失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const clipboard = ref(null);
|
||||
const clipboard = ref(null)
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then((u) => {
|
||||
isLogin.value = true;
|
||||
availablePower.value = u.power;
|
||||
fetchData(1);
|
||||
isLogin.value = true
|
||||
availablePower.value = u.power
|
||||
fetchData(1)
|
||||
// 设置轮询
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(page.value);
|
||||
fetchData(page.value)
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
clipboard.value = new Clipboard(".copy-prompt");
|
||||
clipboard.value.on("success", () => {
|
||||
ElMessage.success("复制成功!");
|
||||
});
|
||||
clipboard.value.on("error", () => {
|
||||
ElMessage.error("复制失败!");
|
||||
});
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
|
||||
getSystemInfo().then((res) => {
|
||||
keLingPowers.value = res.data.keling_powers;
|
||||
updateModelPower();
|
||||
});
|
||||
});
|
||||
keLingPowers.value = res.data.keling_powers
|
||||
updateModelPower()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
clipboard.value.destroy()
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value);
|
||||
clearInterval(pullHandler.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/keling.styl"
|
||||
@import '../assets/css/keling.styl'
|
||||
</style>
|
||||
|
||||
@@ -40,6 +40,18 @@
|
||||
@keyup="handleKeyup"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="" prop="agreement" :class="{ 'agreement-error': agreementError }">
|
||||
<div class="agreement-box" :class="{ shake: isShaking }">
|
||||
<el-checkbox v-model="ruleForm.agreement" @change="handleAgreementChange">
|
||||
我已阅读并同意
|
||||
<span class="agreement-link" @click.stop.prevent="openAgreement"
|
||||
>《用户协议》</span
|
||||
>
|
||||
和
|
||||
<span class="agreement-link" @click.stop.prevent="openPrivacy">《隐私政策》</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="login-btn" size="large" type="primary" @click="login"
|
||||
>登录</el-button
|
||||
@@ -63,6 +75,8 @@ import { useSharedStore } from '@/store/sharedata'
|
||||
import { setRoute } from '@/store/system'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -71,7 +85,6 @@ import Captcha from '@/components/Captcha.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const title = ref('')
|
||||
|
||||
const logo = ref('')
|
||||
const licenseConfig = ref({})
|
||||
const wechatLoginURL = ref('')
|
||||
@@ -79,13 +92,24 @@ const enableVerify = ref(false)
|
||||
const captchaRef = ref(null)
|
||||
const ruleFormRef = ref(null)
|
||||
const ruleForm = reactive({
|
||||
username: process.env.VUE_APP_USER,
|
||||
password: process.env.VUE_APP_PASS,
|
||||
username: import.meta.env.VITE_USER,
|
||||
password: import.meta.env.VITE_PASS,
|
||||
agreement: false,
|
||||
})
|
||||
const rules = {
|
||||
username: [{ required: true, trigger: 'blur', message: '请输入账号' }],
|
||||
password: [{ required: true, trigger: 'blur', message: '请输入密码' }],
|
||||
agreement: [{ required: true, trigger: 'change', message: '请同意用户协议' }],
|
||||
}
|
||||
const agreementContent = ref('')
|
||||
const privacyContent = ref('')
|
||||
|
||||
// 初始化markdown解析器
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 检查URL中是否存在token参数
|
||||
@@ -110,6 +134,36 @@ onMounted(() => {
|
||||
title.value = 'Geek-AI'
|
||||
})
|
||||
|
||||
// 获取用户协议
|
||||
httpGet('/api/config/get?key=agreement')
|
||||
.then((res) => {
|
||||
if (res.data && res.data.content) {
|
||||
agreementContent.value = res.data.content
|
||||
} else {
|
||||
agreementContent.value =
|
||||
'用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
agreementContent.value =
|
||||
'用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'
|
||||
})
|
||||
|
||||
// 获取隐私政策
|
||||
httpGet('/api/config/get?key=privacy')
|
||||
.then((res) => {
|
||||
if (res.data && res.data.content) {
|
||||
privacyContent.value = res.data.content
|
||||
} else {
|
||||
privacyContent.value =
|
||||
'我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
privacyContent.value =
|
||||
'我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
licenseConfig.value = res.data
|
||||
@@ -141,6 +195,16 @@ const handleKeyup = (e) => {
|
||||
}
|
||||
|
||||
const login = async function () {
|
||||
if (!ruleForm.agreement) {
|
||||
agreementError.value = true
|
||||
isShaking.value = true
|
||||
setTimeout(() => {
|
||||
isShaking.value = false
|
||||
}, 500)
|
||||
showMessageError('请先阅读并同意用户协议')
|
||||
return
|
||||
}
|
||||
|
||||
await ruleFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (enableVerify.value) {
|
||||
@@ -170,8 +234,127 @@ const doLogin = (verifyData) => {
|
||||
showMessageError('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const agreementError = ref(false)
|
||||
const isShaking = ref(false)
|
||||
|
||||
const handleAgreementChange = () => {
|
||||
agreementError.value = !ruleForm.agreement
|
||||
if (agreementError.value) {
|
||||
isShaking.value = true
|
||||
setTimeout(() => {
|
||||
isShaking.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
const openAgreement = () => {
|
||||
// 使用弹窗显示用户协议内容,支持Markdown格式
|
||||
ElMessageBox.alert(
|
||||
`<div class="markdown-content">${md.render(agreementContent.value)}</div>`,
|
||||
'用户协议',
|
||||
{
|
||||
confirmButtonText: '我已阅读',
|
||||
dangerouslyUseHTMLString: true,
|
||||
callback: () => {},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const openPrivacy = () => {
|
||||
// 使用弹窗显示隐私政策内容,支持Markdown格式
|
||||
ElMessageBox.alert(
|
||||
`<div class="markdown-content">${md.render(privacyContent.value)}</div>`,
|
||||
'隐私政策',
|
||||
{
|
||||
confirmButtonText: '我已阅读',
|
||||
dangerouslyUseHTMLString: true,
|
||||
callback: () => {},
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/login.styl"
|
||||
@import '../assets/css/login.styl'
|
||||
|
||||
.agreement-box
|
||||
margin-bottom: 10px
|
||||
transition: all 0.3s
|
||||
|
||||
.agreement-link
|
||||
color: var(--el-color-primary)
|
||||
cursor: pointer
|
||||
|
||||
.agreement-error
|
||||
.el-checkbox
|
||||
.el-checkbox__input
|
||||
.el-checkbox__inner
|
||||
border-color: #F56C6C !important
|
||||
|
||||
.shake
|
||||
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both
|
||||
|
||||
@keyframes shake
|
||||
10%, 90%
|
||||
transform: translate3d(-1px, 0, 0)
|
||||
20%, 80%
|
||||
transform: translate3d(2px, 0, 0)
|
||||
30%, 50%, 70%
|
||||
transform: translate3d(-4px, 0, 0)
|
||||
40%, 60%
|
||||
transform: translate3d(4px, 0, 0)
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局样式,用于Markdown内容显示 */
|
||||
.markdown-content {
|
||||
text-align: left;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 15px 0 10px;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid #ccc;
|
||||
padding-left: 10px;
|
||||
color: #666;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,11 +15,25 @@
|
||||
<div class="prompt-container">
|
||||
<div class="input-container">
|
||||
<div class="upload-icon" v-if="images.length < 2">
|
||||
<el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="upload" accept=".jpg,.png,.jpeg">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="upload"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
>
|
||||
<i class="iconfont icon-image"></i>
|
||||
</el-upload>
|
||||
</div>
|
||||
<textarea class="prompt-input" :rows="row" v-model="formData.prompt" maxlength="2000" placeholder="请输入提示词或者上传图片" autofocus> </textarea>
|
||||
<textarea
|
||||
class="prompt-input"
|
||||
:rows="row"
|
||||
v-model="formData.prompt"
|
||||
maxlength="2000"
|
||||
placeholder="请输入提示词或者上传图片"
|
||||
autofocus
|
||||
>
|
||||
</textarea>
|
||||
<div class="send-icon" @click="create">
|
||||
<i class="iconfont icon-send"></i>
|
||||
</div>
|
||||
@@ -44,7 +58,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-container class="video-container" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
|
||||
<el-container
|
||||
class="video-container"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(100,100,100,0.3)"
|
||||
>
|
||||
<h2 class="h-title text-2xl mb-5 mt-2">你的作品</h2>
|
||||
|
||||
<div class="list-box" v-if="!noData">
|
||||
@@ -53,7 +71,15 @@
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<div v-if="item.progress === 100">
|
||||
<video class="video" :src="replaceImg(item.video_url)" preload="auto" loop="loop" muted="muted">您的浏览器不支持视频播放</video>
|
||||
<video
|
||||
class="video"
|
||||
:src="replaceImg(item.video_url)"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<button class="play flex justify-center items-center" @click="play(item)">
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
@@ -63,7 +89,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div class="failed" v-if="item.progress === 101">任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}</div>
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
任务执行失败:{{ item.err_msg }},任务提示词:{{ item.prompt }}
|
||||
</div>
|
||||
<div class="prompt" v-else>{{ item.prompt }}</div>
|
||||
</div>
|
||||
<div class="right" v-if="item.progress === 100">
|
||||
@@ -100,7 +128,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" :image="nodata" description="没有任何作品,赶紧去创作吧!" v-else />
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
:image="nodata"
|
||||
description="没有任何作品,赶紧去创作吧!"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
@@ -116,8 +149,22 @@
|
||||
/>
|
||||
</div>
|
||||
</el-container>
|
||||
<black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto">
|
||||
<video style="max-width: 90vw; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
|
||||
<black-dialog
|
||||
v-model:show="showDialog"
|
||||
title="预览视频"
|
||||
hide-footer
|
||||
@cancal="showDialog = false"
|
||||
width="auto"
|
||||
>
|
||||
<video
|
||||
style="max-width: 90vw; max-height: 90vh"
|
||||
:src="currentVideoUrl"
|
||||
preload="auto"
|
||||
:autoplay="true"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
v-show="showDialog"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</black-dialog>
|
||||
@@ -125,217 +172,216 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
import { httpDownload, httpPost, httpGet } from "@/utils/http";
|
||||
import { checkSession } from "@/store/cache";
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
|
||||
import { replaceImg } from "@/utils/libs";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
|
||||
import Generating from "@/components/ui/Generating.vue";
|
||||
import BlackDialog from "@/components/ui/BlackDialog.vue";
|
||||
import Clipboard from "clipboard";
|
||||
const showDialog = ref(false);
|
||||
const currentVideoUrl = ref("");
|
||||
const row = ref(1);
|
||||
const images = ref([]);
|
||||
import BlackDialog from '@/components/ui/BlackDialog.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { replaceImg } from '@/utils/libs'
|
||||
import { CircleCloseFilled } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
const showDialog = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
const row = ref(1)
|
||||
const images = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
prompt: "",
|
||||
prompt: '',
|
||||
expand_prompt: false,
|
||||
loop: false,
|
||||
first_frame_img: "",
|
||||
end_frame_img: "",
|
||||
});
|
||||
first_frame_img: '',
|
||||
end_frame_img: '',
|
||||
})
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const noData = ref(true);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const taskPulling = ref(true);
|
||||
const clipboard = ref(null);
|
||||
const pullHandler = ref(null);
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const noData = ref(true)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const taskPulling = ref(true)
|
||||
const clipboard = ref(null)
|
||||
const pullHandler = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
checkSession().then(() => {
|
||||
fetchData(1);
|
||||
fetchData(1)
|
||||
// 设置轮询
|
||||
pullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(1);
|
||||
fetchData(1)
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
clipboard.value = new Clipboard(".copy-prompt");
|
||||
clipboard.value.on("success", () => {
|
||||
ElMessage.success("复制成功!");
|
||||
});
|
||||
});
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
clipboard.value.destroy()
|
||||
if (pullHandler.value) {
|
||||
clearInterval(pullHandler.value);
|
||||
clearInterval(pullHandler.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const download = (item) => {
|
||||
const url = replaceImg(item.video_url);
|
||||
const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${url}`;
|
||||
const urlObj = new URL(url);
|
||||
const fileName = urlObj.pathname.split("/").pop();
|
||||
item.downloading = true;
|
||||
const url = replaceImg(item.video_url)
|
||||
const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
item.downloading = true
|
||||
httpDownload(downloadURL)
|
||||
.then((response) => {
|
||||
const blob = new Blob([response.data]);
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(link.href);
|
||||
item.downloading = false;
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
item.downloading = false
|
||||
})
|
||||
.catch(() => {
|
||||
showMessageError("下载失败");
|
||||
item.downloading = false;
|
||||
});
|
||||
};
|
||||
showMessageError('下载失败')
|
||||
item.downloading = false
|
||||
})
|
||||
}
|
||||
|
||||
const play = (item) => {
|
||||
currentVideoUrl.value = replaceImg(item.video_url);
|
||||
showDialog.value = true;
|
||||
};
|
||||
currentVideoUrl.value = replaceImg(item.video_url)
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/video/remove", { id: item.id })
|
||||
httpGet('/api/video/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success("任务删除成功");
|
||||
fetchData();
|
||||
ElMessage.success('任务删除成功')
|
||||
fetchData()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("任务删除失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const publishJob = (item) => {
|
||||
httpGet("/api/video/publish", { id: item.id, publish: item.publish })
|
||||
httpGet('/api/video/publish', { id: item.id, publish: item.publish })
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功");
|
||||
ElMessage.success('操作成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const upload = (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file, file.name);
|
||||
showLoading("正在上传文件...");
|
||||
httpPost("/api/upload", formData)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file, file.name)
|
||||
showLoading('正在上传文件...')
|
||||
httpPost('/api/upload', formData)
|
||||
.then((res) => {
|
||||
images.value.push(res.data.url);
|
||||
ElMessage.success({ message: "上传成功", duration: 500 });
|
||||
closeLoading();
|
||||
images.value.push(res.data.url)
|
||||
ElMessage.success({ message: '上传成功', duration: 500 })
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("图片上传失败:" + e.message);
|
||||
closeLoading();
|
||||
});
|
||||
};
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
|
||||
const remove = (img) => {
|
||||
images.value = images.value.filter((item) => item !== img);
|
||||
};
|
||||
images.value = images.value.filter((item) => item !== img)
|
||||
}
|
||||
|
||||
const switchReverse = () => {
|
||||
images.value = images.value.reverse();
|
||||
};
|
||||
images.value = images.value.reverse()
|
||||
}
|
||||
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page;
|
||||
page.value = _page
|
||||
}
|
||||
httpGet("/api/video/list", {
|
||||
httpGet('/api/video/list', {
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
type: "luma",
|
||||
type: 'luma',
|
||||
})
|
||||
.then((res) => {
|
||||
total.value = res.data.total;
|
||||
let needPull = false;
|
||||
const items = [];
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const items = []
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true;
|
||||
needPull = true
|
||||
}
|
||||
items.push(v);
|
||||
items.push(v)
|
||||
}
|
||||
loading.value = false;
|
||||
taskPulling.value = needPull;
|
||||
loading.value = false
|
||||
taskPulling.value = needPull
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items;
|
||||
list.value = items
|
||||
}
|
||||
noData.value = list.value.length === 0;
|
||||
noData.value = list.value.length === 0
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
noData.value = true;
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
noData.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const create = () => {
|
||||
const len = images.value.length;
|
||||
const len = images.value.length
|
||||
if (len) {
|
||||
formData.first_frame_img = replaceImg(images.value[0]);
|
||||
formData.first_frame_img = replaceImg(images.value[0])
|
||||
if (len === 2) {
|
||||
formData.end_frame_img = replaceImg(images.value[1]);
|
||||
formData.end_frame_img = replaceImg(images.value[1])
|
||||
}
|
||||
}
|
||||
|
||||
httpPost("/api/video/luma/create", formData)
|
||||
httpPost('/api/video/luma/create', formData)
|
||||
.then(() => {
|
||||
fetchData(1);
|
||||
taskPulling.value = true;
|
||||
showMessageOK("创建任务成功");
|
||||
fetchData(1)
|
||||
taskPulling.value = true
|
||||
showMessageOK('创建任务成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("创建任务失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showMessageError('创建任务失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const generatePrompt = () => {
|
||||
if (formData.prompt === "") {
|
||||
return showMessageError("请输入原始提示词");
|
||||
if (formData.prompt === '') {
|
||||
return showMessageError('请输入原始提示词')
|
||||
}
|
||||
showLoading("正在生成视频脚本...");
|
||||
httpPost("/api/prompt/video", { prompt: formData.prompt })
|
||||
showLoading('正在生成视频脚本...')
|
||||
httpPost('/api/prompt/video', { prompt: formData.prompt })
|
||||
.then((res) => {
|
||||
formData.prompt = res.data;
|
||||
closeLoading();
|
||||
formData.prompt = res.data
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("生成提示词失败:" + e.message);
|
||||
closeLoading();
|
||||
});
|
||||
};
|
||||
showMessageError('生成提示词失败:' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/luma.styl"
|
||||
@import "../assets/css/luma.styl"
|
||||
</style>
|
||||
|
||||
@@ -109,15 +109,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { Markmap } from 'markmap-view'
|
||||
import { Transformer } from 'markmap-lib'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Download } from '@element-plus/icons-vue'
|
||||
import { Toolbar } from 'markmap-toolbar'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { Download } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Transformer } from 'markmap-lib'
|
||||
import { Toolbar } from 'markmap-toolbar'
|
||||
import { Markmap } from 'markmap-view'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
const leftBoxHeight = ref(window.innerHeight - 105)
|
||||
//const rightBoxHeight = ref(window.innerHeight - 115);
|
||||
@@ -263,9 +263,9 @@ const getModelById = (modelId) => {
|
||||
}
|
||||
|
||||
// download SVG to png file
|
||||
const downloadImage = async() => {
|
||||
const downloadImage = async () => {
|
||||
// 先自适应思维导图到可视化区域
|
||||
await markMap.value.fit()
|
||||
await markMap.value.fit()
|
||||
|
||||
const svgElement = document.getElementById('markmap')
|
||||
const serializer = new XMLSerializer()
|
||||
@@ -275,7 +275,7 @@ const downloadImage = async() => {
|
||||
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)
|
||||
|
||||
// 分辨率倍数,越高图片越清晰,但文件越大
|
||||
const scale = 4
|
||||
const scale = 4
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = svgElement.offsetWidth * scale
|
||||
canvas.height = svgElement.offsetHeight * scale
|
||||
@@ -298,6 +298,6 @@ const downloadImage = async() => {
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mark-map.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import '../assets/css/mark-map.styl'
|
||||
@import '../assets/css/custom-scroll.styl'
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="member custom-scroll" v-loading="loading" element-loading-background="rgba(255,255,255,.3)" :element-loading-text="loadingText">
|
||||
<div
|
||||
class="member custom-scroll"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(255,255,255,.3)"
|
||||
:element-loading-text="loadingText"
|
||||
>
|
||||
<div class="inner">
|
||||
<div class="user-profile">
|
||||
<user-profile :key="profileKey" />
|
||||
@@ -26,7 +31,9 @@
|
||||
|
||||
<div class="product-box">
|
||||
<div class="info" v-if="orderPayInfoText !== ''">
|
||||
<el-alert type="success" show-icon :closable="false" effect="dark"> <strong>说明:</strong> {{ vipInfoText }} </el-alert>
|
||||
<el-alert type="success" show-icon :closable="false" effect="dark">
|
||||
<strong>说明:</strong> {{ vipInfoText }}
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<el-row v-if="list.length > 0" :gutter="20" class="list-box">
|
||||
@@ -61,7 +68,12 @@
|
||||
</div>
|
||||
|
||||
<div class="pay-way">
|
||||
<span type="primary" v-for="payWay in payWays" @click="pay(item, payWay)" :key="payWay">
|
||||
<span
|
||||
type="primary"
|
||||
v-for="payWay in payWays"
|
||||
@click="pay(item, payWay)"
|
||||
:key="payWay"
|
||||
>
|
||||
<el-button v-if="payWay.pay_type === 'alipay'" color="#15A6E8" circle>
|
||||
<i class="iconfont icon-alipay"></i>
|
||||
</el-button>
|
||||
@@ -97,14 +109,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false" />
|
||||
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" @hide="showBindMobileDialog = false" />
|
||||
<password-dialog
|
||||
v-if="isLogin"
|
||||
:show="showPasswordDialog"
|
||||
@hide="showPasswordDialog = false"
|
||||
/>
|
||||
<bind-mobile
|
||||
v-if="isLogin"
|
||||
:show="showBindMobileDialog"
|
||||
@hide="showBindMobileDialog = false"
|
||||
/>
|
||||
<bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false" />
|
||||
<third-login v-if="isLogin" :show="showThirdLoginDialog" @hide="showThirdLoginDialog = false" />
|
||||
<third-login
|
||||
v-if="isLogin"
|
||||
:show="showThirdLoginDialog"
|
||||
@hide="showThirdLoginDialog = false"
|
||||
/>
|
||||
<redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback" />
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showDialog" :show-close="false" :close-on-click-modal="false" hide-footer width="auto" class="pay-dialog">
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:show-close="false"
|
||||
:close-on-click-modal="false"
|
||||
hide-footer
|
||||
width="auto"
|
||||
class="pay-dialog"
|
||||
>
|
||||
<div v-if="qrImg !== ''">
|
||||
<div class="product-info">
|
||||
请使用微信扫码支付:<span class="price">¥{{ price }}</span>
|
||||
@@ -120,148 +151,148 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import UserProfile from "@/components/UserProfile.vue";
|
||||
import PasswordDialog from "@/components/PasswordDialog.vue";
|
||||
import BindMobile from "@/components/BindMobile.vue";
|
||||
import RedeemVerify from "@/components/RedeemVerify.vue";
|
||||
import UserOrder from "@/components/UserOrder.vue";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import BindEmail from "@/components/BindEmail.vue";
|
||||
import ThirdLogin from "@/components/ThirdLogin.vue";
|
||||
import QRCode from "qrcode";
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
import BindEmail from '@/components/BindEmail.vue'
|
||||
import BindMobile from '@/components/BindMobile.vue'
|
||||
import PasswordDialog from '@/components/PasswordDialog.vue'
|
||||
import RedeemVerify from '@/components/RedeemVerify.vue'
|
||||
import ThirdLogin from '@/components/ThirdLogin.vue'
|
||||
import UserOrder from '@/components/UserOrder.vue'
|
||||
import UserProfile from '@/components/UserProfile.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const list = ref([]);
|
||||
const vipImg = ref("/images/menu/member.png");
|
||||
const enableReward = ref(false); // 是否启用众筹功能
|
||||
const rewardImg = ref("/images/reward.png");
|
||||
const showPasswordDialog = ref(false);
|
||||
const showBindMobileDialog = ref(false);
|
||||
const showBindEmailDialog = ref(false);
|
||||
const showRedeemVerifyDialog = ref(false);
|
||||
const showThirdLoginDialog = ref(false);
|
||||
const user = ref(null);
|
||||
const isLogin = ref(false);
|
||||
const orderTimeout = ref(1800);
|
||||
const loading = ref(true);
|
||||
const loadingText = ref("加载中...");
|
||||
const orderPayInfoText = ref("");
|
||||
const list = ref([])
|
||||
const vipImg = ref('/images/menu/member.png')
|
||||
const enableReward = ref(false) // 是否启用众筹功能
|
||||
const rewardImg = ref('/images/reward.png')
|
||||
const showPasswordDialog = ref(false)
|
||||
const showBindMobileDialog = ref(false)
|
||||
const showBindEmailDialog = ref(false)
|
||||
const showRedeemVerifyDialog = ref(false)
|
||||
const showThirdLoginDialog = ref(false)
|
||||
const user = ref(null)
|
||||
const isLogin = ref(false)
|
||||
const orderTimeout = ref(1800)
|
||||
const loading = ref(true)
|
||||
const loadingText = ref('加载中...')
|
||||
const orderPayInfoText = ref('')
|
||||
|
||||
const payWays = ref([]);
|
||||
const vipInfoText = ref("");
|
||||
const store = useSharedStore();
|
||||
const profileKey = ref(0);
|
||||
const userOrderKey = ref(0);
|
||||
const showDialog = ref(false);
|
||||
const qrImg = ref("");
|
||||
const price = ref(0);
|
||||
const payWays = ref([])
|
||||
const vipInfoText = ref('')
|
||||
const store = useSharedStore()
|
||||
const profileKey = ref(0)
|
||||
const userOrderKey = ref(0)
|
||||
const showDialog = ref(false)
|
||||
const qrImg = ref('')
|
||||
const price = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then((_user) => {
|
||||
user.value = _user;
|
||||
isLogin.value = true;
|
||||
user.value = _user
|
||||
isLogin.value = true
|
||||
})
|
||||
.catch(() => {
|
||||
store.setShowLoginDialog(true);
|
||||
});
|
||||
store.setShowLoginDialog(true)
|
||||
})
|
||||
|
||||
httpGet("/api/product/list")
|
||||
httpGet('/api/product/list')
|
||||
.then((res) => {
|
||||
list.value = res.data;
|
||||
loading.value = false;
|
||||
list.value = res.data
|
||||
loading.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取产品套餐失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取产品套餐失败:' + e.message)
|
||||
})
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
rewardImg.value = res.data["reward_img"];
|
||||
enableReward.value = res.data["enabled_reward"];
|
||||
orderPayInfoText.value = res.data["order_pay_info_text"];
|
||||
if (res.data["order_pay_timeout"] > 0) {
|
||||
orderTimeout.value = res.data["order_pay_timeout"];
|
||||
rewardImg.value = res.data['reward_img']
|
||||
enableReward.value = res.data['enabled_reward']
|
||||
orderPayInfoText.value = res.data['order_pay_info_text']
|
||||
if (res.data['order_pay_timeout'] > 0) {
|
||||
orderTimeout.value = res.data['order_pay_timeout']
|
||||
}
|
||||
vipInfoText.value = res.data["vip_info_text"];
|
||||
vipInfoText.value = res.data['vip_info_text']
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/payment/payWays")
|
||||
httpGet('/api/payment/payWays')
|
||||
.then((res) => {
|
||||
payWays.value = res.data;
|
||||
payWays.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取支付方式失败:" + e.message);
|
||||
});
|
||||
});
|
||||
ElMessage.error('获取支付方式失败:' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
const pay = (product, payWay) => {
|
||||
if (!isLogin.value) {
|
||||
store.setShowLoginDialog(true);
|
||||
return;
|
||||
store.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
loading.value = true;
|
||||
loadingText.value = "正在生成支付订单...";
|
||||
let host = process.env.VUE_APP_API_HOST;
|
||||
if (host === "") {
|
||||
host = `${location.protocol}//${location.host}`;
|
||||
loading.value = true
|
||||
loadingText.value = '正在生成支付订单...'
|
||||
let host = import.meta.env.VITE_API_HOST
|
||||
if (host === '') {
|
||||
host = `${location.protocol}//${location.host}`
|
||||
}
|
||||
httpPost(`${process.env.VUE_APP_API_HOST}/api/payment/doPay`, {
|
||||
httpPost(`${import.meta.env.VITE_API_HOST}/api/payment/doPay`, {
|
||||
product_id: product.id,
|
||||
pay_way: payWay.pay_way,
|
||||
pay_type: payWay.pay_type,
|
||||
user_id: user.value.id,
|
||||
host: host,
|
||||
device: "jump",
|
||||
device: 'jump',
|
||||
})
|
||||
.then((res) => {
|
||||
showDialog.value = true;
|
||||
loading.value = false;
|
||||
if (payWay.pay_way === "wechat") {
|
||||
price.value = Number(product.discount);
|
||||
showDialog.value = true
|
||||
loading.value = false
|
||||
if (payWay.pay_way === 'wechat') {
|
||||
price.value = Number(product.discount)
|
||||
QRCode.toDataURL(res.data, { width: 300, height: 300, margin: 2 }, (error, url) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
console.error(error)
|
||||
} else {
|
||||
qrImg.value = url;
|
||||
qrImg.value = url
|
||||
}
|
||||
});
|
||||
})
|
||||
} else {
|
||||
window.open(res.data, "_blank");
|
||||
window.open(res.data, '_blank')
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
setTimeout(() => {
|
||||
ElMessage.error("生成支付订单失败:" + e.message);
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
});
|
||||
};
|
||||
ElMessage.error('生成支付订单失败:' + e.message)
|
||||
loading.value = false
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
const redeemCallback = (success) => {
|
||||
showRedeemVerifyDialog.value = false;
|
||||
showRedeemVerifyDialog.value = false
|
||||
if (success) {
|
||||
profileKey.value += 1;
|
||||
profileKey.value += 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const payCallback = (success) => {
|
||||
showDialog.value = false;
|
||||
showDialog.value = false
|
||||
if (success) {
|
||||
profileKey.value += 1;
|
||||
userOrderKey.value += 1;
|
||||
profileKey.value += 1
|
||||
userOrderKey.value += 1
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import "@/assets/css/member.styl"
|
||||
<style lang="stylus" scoped>
|
||||
@import "../assets/css/custom-scroll.styl"
|
||||
@import "../assets/css/member.styl"
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
<div class="inner">
|
||||
<div class="list-box">
|
||||
<div class="handle-box">
|
||||
<el-input v-model="query.model" placeholder="模型" class="handle-input mr10" clearable></el-input>
|
||||
<el-input
|
||||
v-model="query.model"
|
||||
placeholder="模型"
|
||||
class="handle-input mr10"
|
||||
clearable
|
||||
></el-input>
|
||||
<el-date-picker
|
||||
v-model="query.date"
|
||||
type="daterange"
|
||||
@@ -23,21 +28,27 @@
|
||||
<el-table-column prop="model" label="模型" width="130px" />
|
||||
<el-table-column prop="type" label="类型">
|
||||
<template #default="scope">
|
||||
<el-tag size="small" :type="tagColors[scope.row.type]">{{ scope.row.type_str }}</el-tag>
|
||||
<el-tag size="small" :type="tagColors[scope.row.type]">{{
|
||||
scope.row.type_str
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数额">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-text type="success" v-if="scope.row.mark === 1">+{{ scope.row.amount }}</el-text>
|
||||
<el-text type="danger" v-if="scope.row.mark === 0">-{{ scope.row.amount }}</el-text>
|
||||
<el-text type="success" v-if="scope.row.mark === 1"
|
||||
>+{{ scope.row.amount }}</el-text
|
||||
>
|
||||
<el-text type="danger" v-if="scope.row.mark === 0"
|
||||
>-{{ scope.row.amount }}</el-text
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="balance" label="余额" />
|
||||
<el-table-column label="发生时间" width="160px">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" />
|
||||
@@ -64,48 +75,48 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import { onMounted, ref } from "vue";
|
||||
import { dateFormat } from "@/utils/libs";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import Clipboard from "clipboard";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { httpPost } from "@/utils/http";
|
||||
import { checkSession } from "@/store/cache";
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { dateFormat } from '@/utils/libs'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const items = ref([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const loading = ref(false);
|
||||
const listBoxHeight = window.innerHeight - 87;
|
||||
const items = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const loading = ref(false)
|
||||
const listBoxHeight = window.innerHeight - 87
|
||||
const query = ref({
|
||||
model: "",
|
||||
model: '',
|
||||
date: [],
|
||||
});
|
||||
const tagColors = ref(["primary", "success", "primary", "danger", "info", "warning"]);
|
||||
})
|
||||
const tagColors = ref(['primary', 'success', 'primary', 'danger', 'info', 'warning'])
|
||||
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then(() => {
|
||||
fetchData();
|
||||
fetchData()
|
||||
})
|
||||
.catch(() => {});
|
||||
const clipboard = new Clipboard(".copy-order-no");
|
||||
clipboard.on("success", () => {
|
||||
ElMessage.success("复制成功!");
|
||||
});
|
||||
.catch(() => {})
|
||||
const clipboard = new Clipboard('.copy-order-no')
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
|
||||
clipboard.on("error", () => {
|
||||
ElMessage.error("复制失败!");
|
||||
});
|
||||
});
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
loading.value = true;
|
||||
httpPost("/api/powerLog/list", {
|
||||
loading.value = true
|
||||
httpPost('/api/powerLog/list', {
|
||||
model: query.value.model,
|
||||
date: query.value.date,
|
||||
page: page.value,
|
||||
@@ -113,22 +124,22 @@ const fetchData = () => {
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
items.value = res.data.items;
|
||||
total.value = res.data.total;
|
||||
page.value = res.data.page;
|
||||
pageSize.value = res.data.page_size;
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.page_size
|
||||
}
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import "../assets/css/custom-scroll.styl"
|
||||
.power-log {
|
||||
color #ffffff
|
||||
.inner {
|
||||
|
||||
@@ -10,12 +10,27 @@
|
||||
<el-tab-pane label="手机注册" name="mobile" v-if="enableMobile">
|
||||
<el-form-item>
|
||||
<div class="form-title">手机号码</div>
|
||||
<el-input placeholder="请输入手机号码" size="large" v-model="data.mobile" maxlength="11" autocomplete="off"> </el-input>
|
||||
<el-input
|
||||
placeholder="请输入手机号码"
|
||||
size="large"
|
||||
v-model="data.mobile"
|
||||
maxlength="11"
|
||||
autocomplete="off"
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="form-title">验证码</div>
|
||||
<div class="flex w100">
|
||||
<el-input placeholder="请输入验证码" size="large" maxlength="30" class="code-input" v-model="data.code" autocomplete="off"> </el-input>
|
||||
<el-input
|
||||
placeholder="请输入验证码"
|
||||
size="large"
|
||||
maxlength="30"
|
||||
class="code-input"
|
||||
v-model="data.code"
|
||||
autocomplete="off"
|
||||
>
|
||||
</el-input>
|
||||
|
||||
<send-msg size="large" :receiver="data.mobile" type="mobile" />
|
||||
</div>
|
||||
@@ -24,12 +39,26 @@
|
||||
<el-tab-pane label="邮箱注册" name="email" v-if="enableEmail">
|
||||
<el-form-item class="block">
|
||||
<div class="form-title">邮箱</div>
|
||||
<el-input placeholder="请输入邮箱地址" size="large" v-model="data.email" autocomplete="off"> </el-input>
|
||||
<el-input
|
||||
placeholder="请输入邮箱地址"
|
||||
size="large"
|
||||
v-model="data.email"
|
||||
autocomplete="off"
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="block">
|
||||
<div class="form-title">验证码</div>
|
||||
<div class="flex w100">
|
||||
<el-input placeholder="请输入验证码" size="large" maxlength="30" class="code-input" v-model="data.code" autocomplete="off"> </el-input>
|
||||
<el-input
|
||||
placeholder="请输入验证码"
|
||||
size="large"
|
||||
maxlength="30"
|
||||
class="code-input"
|
||||
v-model="data.code"
|
||||
autocomplete="off"
|
||||
>
|
||||
</el-input>
|
||||
|
||||
<send-msg size="large" :receiver="data.email" type="email" />
|
||||
</div>
|
||||
@@ -39,7 +68,13 @@
|
||||
<el-form-item class="block">
|
||||
<div class="form-title">用户名</div>
|
||||
|
||||
<el-input placeholder="请输入用户名" size="large" v-model="data.username" autocomplete="off"> </el-input>
|
||||
<el-input
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
v-model="data.username"
|
||||
autocomplete="off"
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -47,24 +82,67 @@
|
||||
<el-form-item class="block">
|
||||
<div class="form-title">密码</div>
|
||||
|
||||
<el-input placeholder="请输入密码(8-16位)" maxlength="16" size="large" v-model="data.password" show-password autocomplete="off"> </el-input>
|
||||
<el-input
|
||||
placeholder="请输入密码(8-16位)"
|
||||
maxlength="16"
|
||||
size="large"
|
||||
v-model="data.password"
|
||||
show-password
|
||||
autocomplete="off"
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="block">
|
||||
<div class="form-title">重复密码</div>
|
||||
|
||||
<el-input placeholder="请再次输入密码(8-16位)" size="large" maxlength="16" v-model="data.repass" show-password autocomplete="off"> </el-input>
|
||||
<el-input
|
||||
placeholder="请再次输入密码(8-16位)"
|
||||
size="large"
|
||||
maxlength="16"
|
||||
v-model="data.repass"
|
||||
show-password
|
||||
autocomplete="off"
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="block">
|
||||
<div class="form-title">邀请码</div>
|
||||
|
||||
<el-input placeholder="请输入邀请码(可选)" size="large" v-model="data.invite_code" autocomplete="off"> </el-input>
|
||||
<el-input
|
||||
placeholder="请输入邀请码(可选)"
|
||||
size="large"
|
||||
v-model="data.invite_code"
|
||||
autocomplete="off"
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label=""
|
||||
prop="agreement"
|
||||
:class="{ 'agreement-error': agreementError }"
|
||||
>
|
||||
<div class="agreement-box" :class="{ shake: isShaking }">
|
||||
<el-checkbox v-model="data.agreement" @change="handleAgreementChange">
|
||||
我已阅读并同意
|
||||
<span class="agreement-link" @click.stop.prevent="openAgreement"
|
||||
>《用户协议》</span
|
||||
>
|
||||
和
|
||||
<span class="agreement-link" @click.stop.prevent="openPrivacy"
|
||||
>《隐私政策》</span
|
||||
>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-row class="btn-row" :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-button class="login-btn" type="primary" size="large" @click="submitRegister">注册</el-button>
|
||||
<el-button class="login-btn" type="primary" size="large" @click="submitRegister"
|
||||
>注册</el-button
|
||||
>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
@@ -80,7 +158,9 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<el-button type="primary" @click="router.push('/')"><i class="iconfont icon-home mr-1"></i> 返回首页</el-button>
|
||||
<el-button type="primary" @click="router.push('/')"
|
||||
><i class="iconfont icon-home mr-1"></i> 返回首页</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
@@ -92,146 +172,232 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import AccountTop from "@/components/AccountTop.vue";
|
||||
import AccountBg from "@/components/AccountBg.vue";
|
||||
import AccountBg from '@/components/AccountBg.vue'
|
||||
import AccountTop from '@/components/AccountTop.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useRouter } from "vue-router";
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import { arrayContains, isMobile } from "@/utils/libs";
|
||||
import { setUserToken } from "@/store/session";
|
||||
import { validateEmail, validateMobile } from "@/utils/validate";
|
||||
import { showMessageError, showMessageOK } from "@/utils/dialog";
|
||||
import { getLicenseInfo, getSystemInfo } from "@/store/cache";
|
||||
import Captcha from "@/components/Captcha.vue";
|
||||
import Captcha from '@/components/Captcha.vue'
|
||||
import SendMsg from '@/components/SendMsg.vue'
|
||||
import { getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { setUserToken } from '@/store/session'
|
||||
import { showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { arrayContains, isMobile } from '@/utils/libs'
|
||||
import { validateEmail, validateMobile } from '@/utils/validate'
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref("");
|
||||
const logo = ref("");
|
||||
const router = useRouter()
|
||||
const title = ref('')
|
||||
const logo = ref('')
|
||||
const data = ref({
|
||||
username: "",
|
||||
mobile: "",
|
||||
email: "",
|
||||
password: "",
|
||||
code: "",
|
||||
repass: "",
|
||||
invite_code: router.currentRoute.value.query["invite_code"],
|
||||
});
|
||||
username: '',
|
||||
mobile: '',
|
||||
email: '',
|
||||
password: '',
|
||||
code: '',
|
||||
repass: '',
|
||||
invite_code: router.currentRoute.value.query['invite_code'],
|
||||
agreement: false,
|
||||
})
|
||||
|
||||
const enableMobile = ref(false);
|
||||
const enableEmail = ref(false);
|
||||
const enableUser = ref(false);
|
||||
const enableRegister = ref(true);
|
||||
const activeName = ref("mobile");
|
||||
const wxImg = ref("/images/wx.png");
|
||||
const licenseConfig = ref({});
|
||||
const enableVerify = ref(false);
|
||||
const captchaRef = ref(null);
|
||||
const enableMobile = ref(false)
|
||||
const enableEmail = ref(false)
|
||||
const enableUser = ref(false)
|
||||
const enableRegister = ref(true)
|
||||
const activeName = ref('mobile')
|
||||
const wxImg = ref('/images/wx.png')
|
||||
const licenseConfig = ref({})
|
||||
const enableVerify = ref(false)
|
||||
const captchaRef = ref(null)
|
||||
const agreementError = ref(false)
|
||||
const isShaking = ref(false)
|
||||
|
||||
// 初始化markdown解析器
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
})
|
||||
|
||||
const agreementContent = ref('')
|
||||
const privacyContent = ref('')
|
||||
|
||||
// 记录邀请码点击次数
|
||||
if (data.value.invite_code) {
|
||||
httpGet("/api/invite/hits", { code: data.value.invite_code });
|
||||
httpGet('/api/invite/hits', { code: data.value.invite_code })
|
||||
}
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
title.value = res.data.title;
|
||||
logo.value = res.data.logo;
|
||||
const registerWays = res.data["register_ways"];
|
||||
title.value = res.data.title
|
||||
logo.value = res.data.logo
|
||||
const registerWays = res.data['register_ways']
|
||||
|
||||
if (arrayContains(registerWays, "username")) {
|
||||
enableUser.value = true;
|
||||
activeName.value = "username";
|
||||
if (arrayContains(registerWays, 'username')) {
|
||||
enableUser.value = true
|
||||
activeName.value = 'username'
|
||||
}
|
||||
if (arrayContains(registerWays, "email")) {
|
||||
enableEmail.value = true;
|
||||
activeName.value = "email";
|
||||
if (arrayContains(registerWays, 'email')) {
|
||||
enableEmail.value = true
|
||||
activeName.value = 'email'
|
||||
}
|
||||
if (arrayContains(registerWays, "mobile")) {
|
||||
enableMobile.value = true;
|
||||
activeName.value = "mobile";
|
||||
if (arrayContains(registerWays, 'mobile')) {
|
||||
enableMobile.value = true
|
||||
activeName.value = 'mobile'
|
||||
}
|
||||
// 是否启用注册
|
||||
enableRegister.value = res.data["enabled_register"];
|
||||
enableRegister.value = res.data['enabled_register']
|
||||
// 使用后台上传的客服微信二维码
|
||||
if (res.data["wechat_card_url"] !== "") {
|
||||
wxImg.value = res.data["wechat_card_url"];
|
||||
if (res.data['wechat_card_url'] !== '') {
|
||||
wxImg.value = res.data['wechat_card_url']
|
||||
}
|
||||
enableVerify.value = res.data["enabled_verify"];
|
||||
enableVerify.value = res.data['enabled_verify']
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
// 获取用户协议
|
||||
httpGet('/api/config/get?key=agreement')
|
||||
.then((res) => {
|
||||
if (res.data && res.data.content) {
|
||||
agreementContent.value = res.data.content
|
||||
} else {
|
||||
agreementContent.value =
|
||||
'# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(e)
|
||||
agreementContent.value =
|
||||
'# 用户协议\n\n用户在使用本服务前应当阅读并同意本协议。本协议内容包括协议正文及所有本平台已经发布的或将来可能发布的各类规则。所有规则为本协议不可分割的组成部分,与协议正文具有同等法律效力。'
|
||||
})
|
||||
|
||||
// 获取隐私政策
|
||||
httpGet('/api/config/get?key=privacy')
|
||||
.then((res) => {
|
||||
if (res.data && res.data.content) {
|
||||
privacyContent.value = res.data.content
|
||||
} else {
|
||||
privacyContent.value =
|
||||
'# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(e)
|
||||
privacyContent.value =
|
||||
'# 隐私政策\n\n我们非常重视用户的隐私和个人信息保护。您在使用我们的产品与服务时,我们可能会收集和使用您的相关信息。我们希望通过本《隐私政策》向您说明我们在收集和使用您相关信息时对应的处理规则。'
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
licenseConfig.value = res.data;
|
||||
licenseConfig.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取 License 配置:" + e.message);
|
||||
});
|
||||
showMessageError('获取 License 配置:' + e.message)
|
||||
})
|
||||
|
||||
// 注册操作
|
||||
const submitRegister = () => {
|
||||
if (activeName.value === "username" && data.value.username === "") {
|
||||
return showMessageError("请输入用户名");
|
||||
if (activeName.value === 'username' && data.value.username === '') {
|
||||
return showMessageError('请输入用户名')
|
||||
}
|
||||
|
||||
if (activeName.value === "mobile" && !validateMobile(data.value.mobile)) {
|
||||
return showMessageError("请输入合法的手机号");
|
||||
if (activeName.value === 'mobile' && !validateMobile(data.value.mobile)) {
|
||||
return showMessageError('请输入合法的手机号')
|
||||
}
|
||||
|
||||
if (activeName.value === "email" && !validateEmail(data.value.email)) {
|
||||
return showMessageError("请输入合法的邮箱地址");
|
||||
if (activeName.value === 'email' && !validateEmail(data.value.email)) {
|
||||
return showMessageError('请输入合法的邮箱地址')
|
||||
}
|
||||
|
||||
if (data.value.password.length < 8) {
|
||||
return showMessageError("密码的长度为8-16个字符");
|
||||
return showMessageError('密码的长度为8-16个字符')
|
||||
}
|
||||
if (data.value.repass !== data.value.password) {
|
||||
return showMessageError("两次输入密码不一致");
|
||||
return showMessageError('两次输入密码不一致')
|
||||
}
|
||||
|
||||
if ((activeName.value === "mobile" || activeName.value === "email") && data.value.code === "") {
|
||||
return showMessageError("请输入验证码");
|
||||
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
|
||||
return showMessageError('请输入验证码')
|
||||
}
|
||||
|
||||
if (!data.value.agreement) {
|
||||
agreementError.value = true
|
||||
isShaking.value = true
|
||||
setTimeout(() => {
|
||||
isShaking.value = false
|
||||
}, 500)
|
||||
showMessageError('请先阅读并同意用户协议和隐私政策')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是用户名和密码登录,那么需要加载验证码
|
||||
if (enableVerify.value && activeName.value === "username") {
|
||||
captchaRef.value.loadCaptcha();
|
||||
if (enableVerify.value && activeName.value === 'username') {
|
||||
captchaRef.value.loadCaptcha()
|
||||
} else {
|
||||
doSubmitRegister({});
|
||||
doSubmitRegister({})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const doSubmitRegister = (verifyData) => {
|
||||
data.value.key = verifyData.key;
|
||||
data.value.dots = verifyData.dots;
|
||||
data.value.x = verifyData.x;
|
||||
data.value.reg_way = activeName.value;
|
||||
httpPost("/api/user/register", data.value)
|
||||
data.value.key = verifyData.key
|
||||
data.value.dots = verifyData.dots
|
||||
data.value.x = verifyData.x
|
||||
data.value.reg_way = activeName.value
|
||||
httpPost('/api/user/register', data.value)
|
||||
.then((res) => {
|
||||
setUserToken(res.data.token);
|
||||
showMessageOK("注册成功,即将跳转到对话主界面...");
|
||||
setUserToken(res.data.token)
|
||||
showMessageOK('注册成功,即将跳转到对话主界面...')
|
||||
if (isMobile()) {
|
||||
router.push("/mobile/index");
|
||||
router.push('/mobile/index')
|
||||
} else {
|
||||
router.push("/chat");
|
||||
router.push('/chat')
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("注册失败," + e.message);
|
||||
});
|
||||
};
|
||||
showMessageError('注册失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const handleAgreementChange = () => {
|
||||
agreementError.value = !data.value.agreement
|
||||
}
|
||||
|
||||
const openAgreement = () => {
|
||||
// 使用弹窗显示用户协议内容,支持Markdown格式
|
||||
ElMessageBox.alert(
|
||||
`<div class="markdown-content">${md.render(agreementContent.value)}</div>`,
|
||||
'用户协议',
|
||||
{
|
||||
confirmButtonText: '我已阅读',
|
||||
dangerouslyUseHTMLString: true,
|
||||
callback: () => {},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const openPrivacy = () => {
|
||||
// 使用弹窗显示隐私政策内容,支持Markdown格式
|
||||
ElMessageBox.alert(
|
||||
`<div class="markdown-content">${md.render(privacyContent.value)}</div>`,
|
||||
'隐私政策',
|
||||
{
|
||||
confirmButtonText: '我已阅读',
|
||||
dangerouslyUseHTMLString: true,
|
||||
callback: () => {},
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/login.styl"
|
||||
@import '../assets/css/login.styl'
|
||||
:deep(.back){
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -243,4 +409,84 @@ const doSubmitRegister = (verifyData) => {
|
||||
margin-top: 20px
|
||||
|
||||
}
|
||||
|
||||
.agreement-box
|
||||
margin-bottom: 10px
|
||||
transition: all 0.3s
|
||||
|
||||
.agreement-link
|
||||
color: var(--el-color-primary)
|
||||
cursor: pointer
|
||||
|
||||
.agreement-error
|
||||
.el-checkbox
|
||||
.el-checkbox__input
|
||||
.el-checkbox__inner
|
||||
border-color: #F56C6C !important
|
||||
|
||||
.shake
|
||||
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both
|
||||
|
||||
@keyframes shake
|
||||
10%, 90%
|
||||
transform: translate3d(-1px, 0, 0)
|
||||
20%, 80%
|
||||
transform: translate3d(2px, 0, 0)
|
||||
30%, 50%, 70%
|
||||
transform: translate3d(-4px, 0, 0)
|
||||
40%, 60%
|
||||
transform: translate3d(4px, 0, 0)
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 全局样式,用于Markdown内容显示 */
|
||||
.markdown-content {
|
||||
text-align: left;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.markdown-content h1 {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.markdown-content h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 15px 0 10px;
|
||||
}
|
||||
|
||||
.markdown-content p {
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid #ccc;
|
||||
padding-left: 10px;
|
||||
color: #666;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,11 +10,7 @@
|
||||
<el-tab-pane label="手机号验证" name="mobile">
|
||||
<el-form-item>
|
||||
<div class="form-title">手机号码</div>
|
||||
<el-input
|
||||
v-model="form.mobile"
|
||||
size="large"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
<el-input v-model="form.mobile" size="large" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="form-title">验证码</div>
|
||||
@@ -26,11 +22,7 @@
|
||||
placeholder="请输入验证码"
|
||||
class="code-input"
|
||||
/>
|
||||
<send-msg
|
||||
size="large"
|
||||
:receiver="form.mobile"
|
||||
type="mobile"
|
||||
/>
|
||||
<send-msg size="large" :receiver="form.mobile" type="mobile" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
@@ -38,11 +30,7 @@
|
||||
<el-form-item>
|
||||
<div class="form-title">邮箱</div>
|
||||
|
||||
<el-input
|
||||
v-model="form.email"
|
||||
placeholder="请输入邮箱"
|
||||
size="large"
|
||||
/>
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="form-title">验证码</div>
|
||||
@@ -54,11 +42,7 @@
|
||||
placeholder="请输入验证码"
|
||||
class="code-input"
|
||||
/>
|
||||
<send-msg
|
||||
size="large"
|
||||
:receiver="form.email"
|
||||
type="email"
|
||||
/>
|
||||
<send-msg size="large" :receiver="form.email" type="email" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
@@ -85,12 +69,7 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
class="login-btn"
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="save"
|
||||
>
|
||||
<el-button class="login-btn" size="large" type="primary" @click="save">
|
||||
重置密码
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
@@ -103,50 +82,49 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import AccountTop from "@/components/AccountTop.vue";
|
||||
import AccountTop from '@/components/AccountTop.vue'
|
||||
import SendMsg from '@/components/SendMsg.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { ElMessage } from "element-plus";
|
||||
import { httpPost } from "@/utils/http";
|
||||
import AccountBg from "@/components/AccountBg.vue";
|
||||
import { validateEmail, validateMobile } from "@/utils/validate";
|
||||
import AccountBg from '@/components/AccountBg.vue'
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const form = ref({
|
||||
mobile: "",
|
||||
email: "",
|
||||
type: "mobile",
|
||||
code: "",
|
||||
password: "",
|
||||
repass: ""
|
||||
});
|
||||
mobile: '',
|
||||
email: '',
|
||||
type: 'mobile',
|
||||
code: '',
|
||||
password: '',
|
||||
repass: '',
|
||||
})
|
||||
|
||||
const save = () => {
|
||||
if (form.value.code === "") {
|
||||
return ElMessage.error("请输入验证码");
|
||||
if (form.value.code === '') {
|
||||
return ElMessage.error('请输入验证码')
|
||||
}
|
||||
if (form.value.password.length < 8) {
|
||||
return ElMessage.error("密码长度必须大于8位");
|
||||
return ElMessage.error('密码长度必须大于8位')
|
||||
}
|
||||
if (form.value.repass !== form.value.password) {
|
||||
return ElMessage.error("两次输入密码不一致");
|
||||
return ElMessage.error('两次输入密码不一致')
|
||||
}
|
||||
|
||||
httpPost("/api/user/resetPass", form.value)
|
||||
httpPost('/api/user/resetPass', form.value)
|
||||
.then(() => {
|
||||
ElMessage.success({
|
||||
message: "重置密码成功",
|
||||
duration: 1000
|
||||
});
|
||||
message: '重置密码成功',
|
||||
duration: 1000,
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("重置密码失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('重置密码失败:' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/login.styl"
|
||||
<style lang="stylus" scoped>
|
||||
@import "../assets/css/login.styl"
|
||||
::v-deep(.el-tabs__item.is-active, .el-tabs__item:hover){
|
||||
color: var(--common-text-color) !important;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
<el-avatar :size="32" :src="song.user?.avatar" />
|
||||
</span>
|
||||
<span class="nickname">{{ song.user?.nickname }}</span>
|
||||
<button class="btn btn-icon" @click="play"><i class="iconfont icon-play"></i> {{ song.play_times }}</button>
|
||||
<button class="btn btn-icon" @click="play">
|
||||
<i class="iconfont icon-play"></i> {{ song.play_times }}
|
||||
</button>
|
||||
|
||||
<el-tooltip content="复制歌曲链接" placement="top">
|
||||
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(song)">
|
||||
@@ -37,58 +39,58 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { httpGet } from "@/utils/http";
|
||||
import { showMessageError } from "@/utils/dialog";
|
||||
import { dateFormat } from "@/utils/libs";
|
||||
import Clipboard from "clipboard";
|
||||
import { ElMessage } from "element-plus";
|
||||
import MusicPlayer from "@/components/MusicPlayer.vue";
|
||||
import MusicPlayer from '@/components/MusicPlayer.vue'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { dateFormat } from '@/utils/libs'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter();
|
||||
const id = router.currentRoute.value.params.id;
|
||||
const song = ref({ title: "" });
|
||||
const playList = ref([]);
|
||||
const playerRef = ref(null);
|
||||
const router = useRouter()
|
||||
const id = router.currentRoute.value.params.id
|
||||
const song = ref({ title: '' })
|
||||
const playList = ref([])
|
||||
const playerRef = ref(null)
|
||||
|
||||
httpGet("/api/suno/detail", { song_id: id })
|
||||
httpGet('/api/suno/detail', { song_id: id })
|
||||
.then((res) => {
|
||||
song.value = res.data;
|
||||
playList.value = [song.value];
|
||||
document.title = song.value?.title + " | By " + song.value?.user.nickname + " | Suno音乐";
|
||||
song.value = res.data
|
||||
playList.value = [song.value]
|
||||
document.title = song.value?.title + ' | By ' + song.value?.user.nickname + ' | Suno音乐'
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取歌曲详情失败:" + e.message);
|
||||
});
|
||||
showMessageError('获取歌曲详情失败:' + e.message)
|
||||
})
|
||||
|
||||
const clipboard = ref(null);
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
clipboard.value = new Clipboard(".copy-link");
|
||||
clipboard.value.on("success", () => {
|
||||
ElMessage.success("复制歌曲链接成功!");
|
||||
});
|
||||
clipboard.value = new Clipboard('.copy-link')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制歌曲链接成功!')
|
||||
})
|
||||
|
||||
clipboard.value.on("error", () => {
|
||||
ElMessage.error("复制失败!");
|
||||
});
|
||||
});
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
});
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
// 播放歌曲
|
||||
const play = () => {
|
||||
playerRef.value.play();
|
||||
};
|
||||
playerRef.value.play()
|
||||
}
|
||||
|
||||
const winHeight = ref(window.innerHeight - 50);
|
||||
const winHeight = ref(window.innerHeight - 50)
|
||||
const getShareURL = (item) => {
|
||||
return `${location.protocol}//${location.host}/song/${item.id}`;
|
||||
};
|
||||
return `${location.protocol}//${location.host}/song/${item.id}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/song.styl"
|
||||
@import '../assets/css/song.styl'
|
||||
</style>
|
||||
|
||||
@@ -5,27 +5,48 @@
|
||||
<el-tooltip content="定义模式" placement="top">
|
||||
<black-switch v-model:value="custom" size="large" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="请上传6-60秒的原始音频,检测到人声的音频将仅设为私人音频。" placement="bottom-end">
|
||||
<el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadAudio" accept=".wav,.mp3">
|
||||
<el-tooltip
|
||||
content="请上传6-60秒的原始音频,检测到人声的音频将仅设为私人音频。"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadAudio"
|
||||
accept=".wav,.mp3"
|
||||
>
|
||||
<el-button class="upload-music" round type="primary">
|
||||
<i class="iconfont icon-upload"></i>
|
||||
<span>上传音乐</span>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-tooltip>
|
||||
<black-select v-model:value="data.model" :options="models" placeholder="请选择模型" style="width: 100px" />
|
||||
<black-select
|
||||
v-model:value="data.model"
|
||||
:options="models"
|
||||
placeholder="请选择模型"
|
||||
style="width: 100px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="params">
|
||||
<div class="pure-music">
|
||||
<span class="switch"><black-switch v-model:value="data.instrumental" size="default" /></span>
|
||||
<span class="switch"
|
||||
><black-switch v-model:value="data.instrumental" size="default"
|
||||
/></span>
|
||||
<span class="text">纯音乐</span>
|
||||
</div>
|
||||
<div v-if="custom">
|
||||
<div class="item-group" v-if="!data.instrumental">
|
||||
<div class="label">
|
||||
<span class="text">歌词</span>
|
||||
<el-popover placement="right" :width="200" trigger="hover" content="自己写歌词或寻求 AI 的帮助。使用两节歌词(8 行)可获得最佳效果。">
|
||||
<el-popover
|
||||
placement="right"
|
||||
:width="200"
|
||||
trigger="hover"
|
||||
content="自己写歌词或寻求 AI 的帮助。使用两节歌词(8 行)可获得最佳效果。"
|
||||
>
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
@@ -34,7 +55,12 @@
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="item" v-loading="isGenerating" element-loading-text="正在生成歌词...">
|
||||
<black-input v-model:value="data.lyrics" type="textarea" :rows="10" :placeholder="promptPlaceholder" />
|
||||
<black-input
|
||||
v-model:value="data.lyrics"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
:placeholder="promptPlaceholder"
|
||||
/>
|
||||
<button class="btn btn-lyric" @click="createLyric">生成歌词</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,7 +72,7 @@
|
||||
placement="right"
|
||||
:width="200"
|
||||
trigger="hover"
|
||||
content="描述您想要的音乐风格(例如“原声流行音乐”)。Sunos 模特无法识别艺术家的名字,但能够理解音乐流派和氛围。"
|
||||
content="描述您想要的音乐风格(例如:原声流行音乐)。Sunos 模特无法识别艺术家的名字,但能够理解音乐流派和氛围。"
|
||||
>
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
@@ -56,12 +82,20 @@
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="item">
|
||||
<black-input v-model:value="data.tags" type="textarea" :maxlength="120" :rows="3" placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..." />
|
||||
<black-input
|
||||
v-model:value="data.tags"
|
||||
type="textarea"
|
||||
:maxlength="120"
|
||||
:rows="3"
|
||||
placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tag-select">
|
||||
<div class="inner">
|
||||
<span class="tag" @click="selectTag(tag)" v-for="tag in tags" :key="tag.value">{{ tag.label }}</span>
|
||||
<span class="tag" @click="selectTag(tag)" v-for="tag in tags" :key="tag.value">{{
|
||||
tag.label
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +103,12 @@
|
||||
<div class="item-group">
|
||||
<div class="label">
|
||||
<span class="text">歌曲名称</span>
|
||||
<el-popover placement="right" :width="200" trigger="hover" content="给你的歌曲起一个标题,以便于分享、发现和组织。">
|
||||
<el-popover
|
||||
placement="right"
|
||||
:width="200"
|
||||
trigger="hover"
|
||||
content="给你的歌曲起一个标题,以便于分享、发现和组织。"
|
||||
>
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
@@ -78,7 +117,12 @@
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="item">
|
||||
<black-input v-model:value="data.title" type="textarea" :rows="1" placeholder="请输入歌曲名称..." />
|
||||
<black-input
|
||||
v-model:value="data.title"
|
||||
type="textarea"
|
||||
:rows="1"
|
||||
placeholder="请输入歌曲名称..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,14 +144,24 @@
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="item">
|
||||
<black-input v-model:value="data.prompt" type="textarea" :rows="10" placeholder="例如:一首关于爱情的摇滚歌曲..." />
|
||||
<black-input
|
||||
v-model:value="data.prompt"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="例如:一首关于爱情的摇滚歌曲..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ref-song" v-if="refSong">
|
||||
<div class="label">
|
||||
<span class="text">续写</span>
|
||||
<el-popover placement="right" :width="200" trigger="hover" content="输入额外的歌词,根据您之前的歌词来扩展歌曲。">
|
||||
<el-popover
|
||||
placement="right"
|
||||
:width="200"
|
||||
trigger="hover"
|
||||
content="输入额外的歌词,根据您之前的歌词来扩展歌曲。"
|
||||
>
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
@@ -122,7 +176,9 @@
|
||||
<span class="title">{{ refSong.title }}</span>
|
||||
<el-button type="info" @click="removeRefSong" size="small" :icon="Delete" circle />
|
||||
</div>
|
||||
<div class="extend-secs">从 <input v-model="refSong.extend_secs" type="text" /> 秒开始续写</div>
|
||||
<div class="extend-secs">
|
||||
从 <input v-model="refSong.extend_secs" type="text" /> 秒开始续写
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,7 +190,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-box h-dvh" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
|
||||
<div
|
||||
class="right-box h-dvh"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(100,100,100,0.3)"
|
||||
>
|
||||
<div class="list-box" v-if="!noData">
|
||||
<div v-for="item in list" :key="item.id">
|
||||
<div class="item" v-if="item.progress === 100">
|
||||
@@ -150,7 +210,9 @@
|
||||
<div class="center">
|
||||
<div class="title">
|
||||
<a :href="'/song/' + item.song_id" target="_blank">{{ item.title }}</a>
|
||||
<span class="model" v-if="item.major_model_version">{{ item.major_model_version }}</span>
|
||||
<span class="model" v-if="item.major_model_version">{{
|
||||
item.major_model_version
|
||||
}}</span>
|
||||
<span class="model" v-if="item.type === 4">用户上传</span>
|
||||
<span class="model" v-if="item.type === 3">
|
||||
<i class="iconfont icon-mp3"></i>
|
||||
@@ -171,7 +233,11 @@
|
||||
|
||||
<button class="btn btn-publish">
|
||||
<span class="text">发布</span>
|
||||
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
|
||||
<black-switch
|
||||
v-model:value="item.publish"
|
||||
@change="publishJob(item)"
|
||||
size="small"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<el-tooltip content="下载歌曲" placement="top">
|
||||
@@ -228,7 +294,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" :image="nodata" description="没有任何作品,赶紧去创作吧!" v-else />
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
:image="nodata"
|
||||
description="没有任何作品,赶紧去创作吧!"
|
||||
v-else
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
@@ -245,11 +316,22 @@
|
||||
</div>
|
||||
|
||||
<div class="music-player" v-if="showPlayer">
|
||||
<music-player :songs="playList" ref="playerRef" :show-close="true" @close="showPlayer = false" />
|
||||
<music-player
|
||||
:songs="playList"
|
||||
ref="playerRef"
|
||||
:show-close="true"
|
||||
@close="showPlayer = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<black-dialog v-model:show="showDialog" title="修改歌曲" @cancal="showDialog = false" @confirm="updateSong" :width="500 + 'px'">
|
||||
<black-dialog
|
||||
v-model:show="showDialog"
|
||||
title="修改歌曲"
|
||||
@cancal="showDialog = false"
|
||||
@confirm="updateSong"
|
||||
:width="500 + 'px'"
|
||||
>
|
||||
<form class="form">
|
||||
<div class="form-item">
|
||||
<div class="label">歌曲名称</div>
|
||||
@@ -258,7 +340,13 @@
|
||||
|
||||
<div class="form-item">
|
||||
<div class="label">封面图片</div>
|
||||
<el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" :http-request="uploadCover" accept=".png,.jpg,.jpeg,.bmp">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadCover"
|
||||
accept=".png,.jpg,.jpeg,.bmp"
|
||||
>
|
||||
<el-avatar :src="editData.cover" shape="square" :size="100" />
|
||||
</el-upload>
|
||||
</div>
|
||||
@@ -268,385 +356,386 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { Delete, InfoFilled } from "@element-plus/icons-vue";
|
||||
import BlackSelect from "@/components/ui/BlackSelect.vue";
|
||||
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
|
||||
import BlackInput from "@/components/ui/BlackInput.vue";
|
||||
import MusicPlayer from "@/components/MusicPlayer.vue";
|
||||
import { compact } from "lodash";
|
||||
import { httpDownload, httpGet, httpPost } from "@/utils/http";
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from "@/utils/dialog";
|
||||
import { checkSession } from "@/store/cache";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { formatTime, replaceImg } from "@/utils/libs";
|
||||
import Clipboard from "clipboard";
|
||||
import BlackDialog from "@/components/ui/BlackDialog.vue";
|
||||
import Compressor from "compressorjs";
|
||||
import Generating from "@/components/ui/Generating.vue";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import MusicPlayer from '@/components/MusicPlayer.vue'
|
||||
import BlackDialog from '@/components/ui/BlackDialog.vue'
|
||||
import BlackInput from '@/components/ui/BlackInput.vue'
|
||||
import BlackSelect from '@/components/ui/BlackSelect.vue'
|
||||
import BlackSwitch from '@/components/ui/BlackSwitch.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
import { formatTime, replaceImg } from '@/utils/libs'
|
||||
import { Delete, InfoFilled } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import Compressor from 'compressorjs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { compact } from 'lodash'
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const custom = ref(false);
|
||||
const custom = ref(false)
|
||||
const models = ref([
|
||||
{ label: "v3.0", value: "chirp-v3-0" },
|
||||
{ label: "v3.5", value: "chirp-v3-5" },
|
||||
{ label: "v4.0", value: "chirp-v4" },
|
||||
]);
|
||||
{ label: 'v3.0', value: 'chirp-v3-0' },
|
||||
{ label: 'v3.5', value: 'chirp-v3-5' },
|
||||
{ label: 'v4.0', value: 'chirp-v4' },
|
||||
{ label: 'v4.5', value: 'chirp-auk' },
|
||||
])
|
||||
const tags = ref([
|
||||
{ label: "女声", value: "female vocals" },
|
||||
{ label: "男声", value: "male vocals" },
|
||||
{ label: "流行", value: "pop" },
|
||||
{ label: "摇滚", value: "rock" },
|
||||
{ label: "硬摇滚", value: "hard rock" },
|
||||
{ label: "电音", value: "electronic" },
|
||||
{ label: "金属", value: "metal" },
|
||||
{ label: "重金属", value: "heavy metal" },
|
||||
{ label: "节拍", value: "beat" },
|
||||
{ label: "弱拍", value: "upbeat" },
|
||||
{ label: "合成器", value: "synth" },
|
||||
{ label: "吉他", value: "guitar" },
|
||||
{ label: "钢琴", value: "piano" },
|
||||
{ label: "小提琴", value: "violin" },
|
||||
{ label: "贝斯", value: "bass" },
|
||||
{ label: "嘻哈", value: "hip hop" },
|
||||
]);
|
||||
{ label: '女声', value: 'female vocals' },
|
||||
{ label: '男声', value: 'male vocals' },
|
||||
{ label: '流行', value: 'pop' },
|
||||
{ label: '摇滚', value: 'rock' },
|
||||
{ label: '硬摇滚', value: 'hard rock' },
|
||||
{ label: '电音', value: 'electronic' },
|
||||
{ label: '金属', value: 'metal' },
|
||||
{ label: '重金属', value: 'heavy metal' },
|
||||
{ label: '节拍', value: 'beat' },
|
||||
{ label: '弱拍', value: 'upbeat' },
|
||||
{ label: '合成器', value: 'synth' },
|
||||
{ label: '吉他', value: 'guitar' },
|
||||
{ label: '钢琴', value: 'piano' },
|
||||
{ label: '小提琴', value: 'violin' },
|
||||
{ label: '贝斯', value: 'bass' },
|
||||
{ label: '嘻哈', value: 'hip hop' },
|
||||
])
|
||||
const data = ref({
|
||||
model: "chirp-v3-0",
|
||||
tags: "",
|
||||
lyrics: "",
|
||||
prompt: "",
|
||||
title: "",
|
||||
model: 'chirp-auk',
|
||||
tags: '',
|
||||
lyrics: '',
|
||||
prompt: '',
|
||||
title: '',
|
||||
instrumental: false,
|
||||
ref_task_id: "",
|
||||
ref_task_id: '',
|
||||
extend_secs: 0,
|
||||
ref_song_id: "",
|
||||
});
|
||||
const loading = ref(false);
|
||||
const noData = ref(true);
|
||||
const playList = ref([]);
|
||||
const playerRef = ref(null);
|
||||
const showPlayer = ref(false);
|
||||
const list = ref([]);
|
||||
const taskPulling = ref(true);
|
||||
const tastPullHandler = ref(null);
|
||||
const btnText = ref("开始创作");
|
||||
const refSong = ref(null);
|
||||
const showDialog = ref(false);
|
||||
const editData = ref({ title: "", cover: "", id: 0 });
|
||||
const promptPlaceholder = ref("请在这里输入你自己写的歌词...");
|
||||
const store = useSharedStore();
|
||||
const clipboard = ref(null);
|
||||
ref_song_id: '',
|
||||
})
|
||||
const loading = ref(false)
|
||||
const noData = ref(true)
|
||||
const playList = ref([])
|
||||
const playerRef = ref(null)
|
||||
const showPlayer = ref(false)
|
||||
const list = ref([])
|
||||
const taskPulling = ref(true)
|
||||
const tastPullHandler = ref(null)
|
||||
const btnText = ref('开始创作')
|
||||
const refSong = ref(null)
|
||||
const showDialog = ref(false)
|
||||
const editData = ref({ title: '', cover: '', id: 0 })
|
||||
const promptPlaceholder = ref('请在这里输入你自己写的歌词...')
|
||||
const store = useSharedStore()
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
clipboard.value = new Clipboard(".copy-link");
|
||||
clipboard.value.on("success", () => {
|
||||
ElMessage.success("复制歌曲链接成功!");
|
||||
});
|
||||
clipboard.value = new Clipboard('.copy-link')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制歌曲链接成功!')
|
||||
})
|
||||
|
||||
clipboard.value.on("error", () => {
|
||||
ElMessage.error("复制失败!");
|
||||
});
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
|
||||
checkSession()
|
||||
.then(() => {
|
||||
fetchData(1);
|
||||
fetchData(1)
|
||||
tastPullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchData(1);
|
||||
fetchData(1)
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
.catch(() => {})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
clipboard.value.destroy()
|
||||
if (tastPullHandler.value) {
|
||||
clearInterval(tastPullHandler.value);
|
||||
clearInterval(tastPullHandler.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page;
|
||||
page.value = _page
|
||||
}
|
||||
loading.value = true;
|
||||
httpGet("/api/suno/list", { page: page.value, page_size: pageSize.value })
|
||||
loading.value = true
|
||||
httpGet('/api/suno/list', { page: page.value, page_size: pageSize.value })
|
||||
.then((res) => {
|
||||
total.value = res.data.total;
|
||||
let needPull = false;
|
||||
const items = [];
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const items = []
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 100) {
|
||||
v.major_model_version = v["raw_data"]["major_model_version"];
|
||||
v.major_model_version = v['raw_data']['major_model_version']
|
||||
}
|
||||
if (v.progress === 0 || v.progress === 102) {
|
||||
needPull = true;
|
||||
needPull = true
|
||||
}
|
||||
items.push(v);
|
||||
items.push(v)
|
||||
}
|
||||
loading.value = false;
|
||||
taskPulling.value = needPull;
|
||||
loading.value = false
|
||||
taskPulling.value = needPull
|
||||
// 如果任务有变化,则刷新任务列表
|
||||
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
|
||||
list.value = items;
|
||||
list.value = items
|
||||
}
|
||||
noData.value = list.value.length === 0;
|
||||
noData.value = list.value.length === 0
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
noData.value = true;
|
||||
showMessageError("获取作品列表失败:" + e.message);
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
noData.value = true
|
||||
showMessageError('获取作品列表失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 创建新的歌曲
|
||||
const create = () => {
|
||||
data.value.type = custom.value ? 2 : 1;
|
||||
data.value.ref_task_id = refSong.value ? refSong.value.task_id : "";
|
||||
data.value.ref_song_id = refSong.value ? refSong.value.song_id : "";
|
||||
data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0;
|
||||
data.value.type = custom.value ? 2 : 1
|
||||
data.value.ref_task_id = refSong.value ? refSong.value.task_id : ''
|
||||
data.value.ref_song_id = refSong.value ? refSong.value.song_id : ''
|
||||
data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0
|
||||
if (refSong.value) {
|
||||
if (data.value.extend_secs > refSong.value.duration) {
|
||||
return showMessageError("续写开始时间不能超过原歌曲长度");
|
||||
return showMessageError('续写开始时间不能超过原歌曲长度')
|
||||
}
|
||||
} else if (custom.value) {
|
||||
if (data.value.lyrics === "") {
|
||||
return showMessageError("请输入歌词");
|
||||
if (data.value.lyrics === '') {
|
||||
return showMessageError('请输入歌词')
|
||||
}
|
||||
if (data.value.title === "") {
|
||||
return showMessageError("请输入歌曲标题");
|
||||
if (data.value.title === '') {
|
||||
return showMessageError('请输入歌曲标题')
|
||||
}
|
||||
} else {
|
||||
if (data.value.prompt === "") {
|
||||
return showMessageError("请输入歌曲描述");
|
||||
if (data.value.prompt === '') {
|
||||
return showMessageError('请输入歌曲描述')
|
||||
}
|
||||
}
|
||||
|
||||
httpPost("/api/suno/create", data.value)
|
||||
httpPost('/api/suno/create', data.value)
|
||||
.then(() => {
|
||||
fetchData(1);
|
||||
taskPulling.value = true;
|
||||
showMessageOK("创建任务成功");
|
||||
fetchData(1)
|
||||
taskPulling.value = true
|
||||
showMessageOK('创建任务成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("创建任务失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showMessageError('创建任务失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 拼接歌曲
|
||||
const merge = (item) => {
|
||||
httpPost("/api/suno/create", { song_id: item.song_id, type: 3 })
|
||||
httpPost('/api/suno/create', { song_id: item.song_id, type: 3 })
|
||||
.then(() => {
|
||||
fetchData(1);
|
||||
taskPulling.value = true;
|
||||
showMessageOK("创建任务成功");
|
||||
fetchData(1)
|
||||
taskPulling.value = true
|
||||
showMessageOK('创建任务成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("合并歌曲失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showMessageError('合并歌曲失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 下载歌曲
|
||||
const download = (item) => {
|
||||
const url = replaceImg(item.audio_url);
|
||||
const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${url}`;
|
||||
const url = replaceImg(item.audio_url)
|
||||
const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}`
|
||||
// parse filename
|
||||
const urlObj = new URL(url);
|
||||
const fileName = urlObj.pathname.split("/").pop();
|
||||
item.downloading = true;
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
item.downloading = true
|
||||
httpDownload(downloadURL)
|
||||
.then((response) => {
|
||||
const blob = new Blob([response.data]);
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(link.href);
|
||||
item.downloading = false;
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
item.downloading = false
|
||||
})
|
||||
.catch(() => {
|
||||
showMessageError("下载失败");
|
||||
item.downloading = false;
|
||||
});
|
||||
};
|
||||
showMessageError('下载失败')
|
||||
item.downloading = false
|
||||
})
|
||||
}
|
||||
|
||||
const uploadAudio = (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file.file, file.name);
|
||||
showLoading("正在上传文件...");
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file, file.name)
|
||||
showLoading('正在上传文件...')
|
||||
// 执行上传操作
|
||||
httpPost("/api/upload", formData)
|
||||
httpPost('/api/upload', formData)
|
||||
.then((res) => {
|
||||
httpPost("/api/suno/create", {
|
||||
httpPost('/api/suno/create', {
|
||||
audio_url: res.data.url,
|
||||
title: res.data.name,
|
||||
type: 4,
|
||||
})
|
||||
.then(() => {
|
||||
fetchData(1);
|
||||
showMessageOK("歌曲上传成功");
|
||||
closeLoading();
|
||||
fetchData(1)
|
||||
showMessageOK('歌曲上传成功')
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("歌曲上传失败:" + e.message);
|
||||
closeLoading();
|
||||
});
|
||||
removeRefSong();
|
||||
ElMessage.success({ message: "上传成功", duration: 500 });
|
||||
showMessageError('歌曲上传失败:' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
removeRefSong()
|
||||
ElMessage.success({ message: '上传成功', duration: 500 })
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("文件传失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('文件传失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 续写歌曲
|
||||
const extend = (item) => {
|
||||
refSong.value = item;
|
||||
refSong.value.extend_secs = item.duration;
|
||||
data.value.title = item.title;
|
||||
custom.value = true;
|
||||
btnText.value = "续写歌曲";
|
||||
promptPlaceholder.value = "输入额外的歌词,根据您之前的歌词来扩展歌曲...";
|
||||
};
|
||||
refSong.value = item
|
||||
refSong.value.extend_secs = item.duration
|
||||
data.value.title = item.title
|
||||
custom.value = true
|
||||
btnText.value = '续写歌曲'
|
||||
promptPlaceholder.value = '输入额外的歌词,根据您之前的歌词来扩展歌曲...'
|
||||
}
|
||||
|
||||
// 更细歌曲
|
||||
const update = (item) => {
|
||||
showDialog.value = true;
|
||||
editData.value.title = item.title;
|
||||
editData.value.cover = item.cover_url;
|
||||
editData.value.id = item.id;
|
||||
};
|
||||
showDialog.value = true
|
||||
editData.value.title = item.title
|
||||
editData.value.cover = item.cover_url
|
||||
editData.value.id = item.id
|
||||
}
|
||||
|
||||
const updateSong = () => {
|
||||
if (editData.value.title === "" || editData.value.cover === "") {
|
||||
return showMessageError("歌曲标题和封面不能为空");
|
||||
if (editData.value.title === '' || editData.value.cover === '') {
|
||||
return showMessageError('歌曲标题和封面不能为空')
|
||||
}
|
||||
httpPost("/api/suno/update", editData.value)
|
||||
httpPost('/api/suno/update', editData.value)
|
||||
.then(() => {
|
||||
showMessageOK("更新歌曲成功");
|
||||
showDialog.value = false;
|
||||
fetchData();
|
||||
showMessageOK('更新歌曲成功')
|
||||
showDialog.value = false
|
||||
fetchData()
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("更新歌曲失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showMessageError('更新歌曲失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => custom.value,
|
||||
(newValue) => {
|
||||
if (!newValue) {
|
||||
removeRefSong();
|
||||
removeRefSong()
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const removeRefSong = () => {
|
||||
refSong.value = null;
|
||||
btnText.value = "开始创作";
|
||||
promptPlaceholder.value = "请在这里输入你自己写的歌词...";
|
||||
};
|
||||
refSong.value = null
|
||||
btnText.value = '开始创作'
|
||||
promptPlaceholder.value = '请在这里输入你自己写的歌词...'
|
||||
}
|
||||
|
||||
const play = (item) => {
|
||||
playList.value = [item];
|
||||
showPlayer.value = true;
|
||||
nextTick(() => playerRef.value.play());
|
||||
};
|
||||
playList.value = [item]
|
||||
showPlayer.value = true
|
||||
nextTick(() => playerRef.value.play())
|
||||
}
|
||||
|
||||
const selectTag = (tag) => {
|
||||
if (data.value.tags.length + tag.value.length >= 119) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
data.value.tags = compact([...data.value.tags.split(","), tag.value]).join(",");
|
||||
};
|
||||
data.value.tags = compact([...data.value.tags.split(','), tag.value]).join(',')
|
||||
}
|
||||
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm("此操作将会删除任务相关文件,继续操作码?", "删除提示", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/suno/remove", { id: item.id })
|
||||
httpGet('/api/suno/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success("任务删除成功");
|
||||
fetchData();
|
||||
ElMessage.success('任务删除成功')
|
||||
fetchData()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("任务删除失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const publishJob = (item) => {
|
||||
httpGet("/api/suno/publish", { id: item.id, publish: item.publish })
|
||||
httpGet('/api/suno/publish', { id: item.id, publish: item.publish })
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功");
|
||||
ElMessage.success('操作成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const getShareURL = (item) => {
|
||||
return `${location.protocol}//${location.host}/song/${item.song_id}`;
|
||||
};
|
||||
return `${location.protocol}//${location.host}/song/${item.song_id}`
|
||||
}
|
||||
|
||||
const uploadCover = (file) => {
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", result, result.name);
|
||||
showLoading("图片上传中...");
|
||||
const formData = new FormData()
|
||||
formData.append('file', result, result.name)
|
||||
showLoading('图片上传中...')
|
||||
// 执行上传操作
|
||||
httpPost("/api/upload", formData)
|
||||
httpPost('/api/upload', formData)
|
||||
.then((res) => {
|
||||
editData.value.cover = res.data.url;
|
||||
ElMessage.success({ message: "上传成功", duration: 500 });
|
||||
closeLoading();
|
||||
editData.value.cover = res.data.url
|
||||
ElMessage.success({ message: '上传成功', duration: 500 })
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("图片上传失败:" + e.message);
|
||||
closeLoading();
|
||||
});
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
},
|
||||
error(err) {
|
||||
console.log(err.message);
|
||||
console.log(err.message)
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const isGenerating = ref(false);
|
||||
const isGenerating = ref(false)
|
||||
const createLyric = () => {
|
||||
if (data.value.lyrics === "") {
|
||||
return showMessageError("请输入歌词描述");
|
||||
if (data.value.lyrics === '') {
|
||||
return showMessageError('请输入歌词描述')
|
||||
}
|
||||
isGenerating.value = true;
|
||||
httpPost("/api/prompt/lyric", { prompt: data.value.lyrics })
|
||||
isGenerating.value = true
|
||||
httpPost('/api/prompt/lyric', { prompt: data.value.lyrics })
|
||||
.then((res) => {
|
||||
const lines = res.data.split("\n");
|
||||
data.value.title = lines.shift().replace(/\*/g, "");
|
||||
lines.shift();
|
||||
data.value.lyrics = lines.join("\n");
|
||||
isGenerating.value = false;
|
||||
const lines = res.data.split('\n')
|
||||
data.value.title = lines.shift().replace(/\*/g, '')
|
||||
lines.shift()
|
||||
data.value.lyrics = lines.join('\n')
|
||||
isGenerating.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("歌词生成失败:" + e.message);
|
||||
isGenerating.value = false;
|
||||
});
|
||||
};
|
||||
showMessageError('歌词生成失败:' + e.message)
|
||||
isGenerating.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/suno.styl"
|
||||
@import '../assets/css/suno.styl'
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
<div class="handle-box">
|
||||
<el-input v-model="query.name" placeholder="模型名称" class="handle-input" />
|
||||
|
||||
<el-select v-model="query.type" placeholder="模型类型" class="handle-input" clearable>
|
||||
<el-option v-for="v in modelTypes" :value="v.value" :label="v.label" :key="v.value">
|
||||
{{ v.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-button :icon="Search" @click="fetchData">搜索</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||
</div>
|
||||
@@ -25,7 +31,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="category" label="标签" />
|
||||
<el-table-column prop="tag" label="标签" />
|
||||
<el-table-column prop="value" label="模型值">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.value }}</span>
|
||||
@@ -34,13 +40,6 @@
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="模型描述" width="180">
|
||||
<template #default="scope">
|
||||
<el-tooltip :content="scope.row.description || ''" placement="top" :show-after="200">
|
||||
<div class="description-cell">{{ scope.row.description }}</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="power" label="费率" />
|
||||
<el-table-column prop="max_tokens" label="最大响应长度" />
|
||||
<el-table-column prop="max_context" label="最大上下文长度" />
|
||||
@@ -79,7 +78,7 @@
|
||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="模型类型:" prop="type">
|
||||
<el-select v-model="item.type" placeholder="请选择模型类型">
|
||||
<el-option v-for="v in type" :value="v.value" :label="v.label" :key="v.value">
|
||||
<el-option v-for="v in modelTypes" :value="v.value" :label="v.label" :key="v.value">
|
||||
{{ v.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
@@ -92,8 +91,8 @@
|
||||
<el-input v-model="item.value" autocomplete="off" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型标签" prop="category">
|
||||
<el-input v-model="item.category" autocomplete="off" />
|
||||
<el-form-item label="模型标签" prop="tag">
|
||||
<el-input v-model="item.tag" autocomplete="off" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="消耗算力:" prop="power">
|
||||
@@ -137,8 +136,8 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型描述" prop="description">
|
||||
<el-input v-model="item.description" autocomplete="off" />
|
||||
<el-form-item label="模型简介" prop="desc">
|
||||
<el-input v-model="item.desc" type="textarea" :rows="3" autocomplete="off" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="创意度:" prop="temperature">
|
||||
@@ -237,7 +236,7 @@ const rules = reactive({
|
||||
})
|
||||
const loading = ref(true)
|
||||
const formRef = ref(null)
|
||||
const type = ref([
|
||||
const modelTypes = ref([
|
||||
{ label: '聊天', value: 'chat' },
|
||||
{ label: '绘图', value: 'img' },
|
||||
{ label: '语音', value: 'tts' },
|
||||
@@ -266,16 +265,18 @@ const fetchData = () => {
|
||||
httpGet('/api/admin/model/list', query.value)
|
||||
.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
|
||||
res.data.forEach((item) => {
|
||||
if (!item.options) {
|
||||
item.options = {}
|
||||
}
|
||||
item.last_used_at = dateFormat(item.last_used_at)
|
||||
})
|
||||
items.value = res.data
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
ElMessage.error('获取数据失败')
|
||||
})
|
||||
}
|
||||
@@ -344,7 +345,7 @@ const add = function () {
|
||||
const edit = function (row) {
|
||||
title.value = '修改模型'
|
||||
showDialog.value = true
|
||||
item.value = row
|
||||
item.value = Object.assign({}, row)
|
||||
}
|
||||
|
||||
const save = function () {
|
||||
@@ -392,7 +393,7 @@ const remove = function (row) {
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/admin/form.styl";
|
||||
@import "../../assets/css/admin/form.styl";
|
||||
.model-list {
|
||||
|
||||
.handle-box {
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
|
||||
<admin-header />
|
||||
<admin-tags />
|
||||
<div
|
||||
:class="'content ' + theme"
|
||||
:style="{ height: contentHeight + 'px' }"
|
||||
>
|
||||
<div :class="'content ' + theme" :style="{ height: contentHeight + 'px' }">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="move" mode="out-in">
|
||||
<keep-alive :include="tags.nameList">
|
||||
@@ -20,41 +17,41 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {useSidebarStore} from "@/store/sidebar";
|
||||
import {useTagsStore} from "@/store/tags";
|
||||
import AdminHeader from "@/components/admin/AdminHeader.vue";
|
||||
import AdminSidebar from "@/components/admin/AdminSidebar.vue";
|
||||
import AdminTags from "@/components/admin/AdminTags.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {checkAdminSession} from "@/store/cache";
|
||||
import {ref, watch} from "vue";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import AdminHeader from '@/components/admin/AdminHeader.vue'
|
||||
import AdminSidebar from '@/components/admin/AdminSidebar.vue'
|
||||
import AdminTags from '@/components/admin/AdminTags.vue'
|
||||
import { checkAdminSession } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { useSidebarStore } from '@/store/sidebar'
|
||||
import { useTagsStore } from '@/store/tags'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const sidebar = useSidebarStore();
|
||||
const tags = useTagsStore();
|
||||
const isLogin = ref(false);
|
||||
const contentHeight = window.innerHeight - 80;
|
||||
const store = useSharedStore();
|
||||
const theme = ref(store.theme);
|
||||
const sidebar = useSidebarStore()
|
||||
const tags = useTagsStore()
|
||||
const isLogin = ref(false)
|
||||
const contentHeight = window.innerHeight - 80
|
||||
const store = useSharedStore()
|
||||
const theme = ref(store.theme)
|
||||
|
||||
// 获取会话信息
|
||||
const router = useRouter();
|
||||
const router = useRouter()
|
||||
checkAdminSession()
|
||||
.then(() => {
|
||||
isLogin.value = true;
|
||||
isLogin.value = true
|
||||
})
|
||||
.catch(() => {
|
||||
router.replace("/admin/login");
|
||||
});
|
||||
router.replace('/admin/login')
|
||||
})
|
||||
|
||||
watch(
|
||||
() => store.theme,
|
||||
() => store.theme,
|
||||
(val) => {
|
||||
theme.value = val;
|
||||
theme.value = val
|
||||
}
|
||||
);
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@import '@/assets/css/main.styl';
|
||||
<style lang="stylus" scoped>
|
||||
@import "../../assets/css/main.styl";
|
||||
</style>
|
||||
|
||||
@@ -68,8 +68,8 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const title = ref('Geek-AI Console')
|
||||
const username = ref(process.env.VUE_APP_ADMIN_USER)
|
||||
const password = ref(process.env.VUE_APP_ADMIN_PASS)
|
||||
const username = ref(import.meta.env.VITE_ADMIN_USER)
|
||||
const password = ref(import.meta.env.VITE_ADMIN_PASS)
|
||||
const logo = ref('')
|
||||
const enableVerify = ref(false)
|
||||
const captchaRef = ref(null)
|
||||
@@ -133,7 +133,7 @@ const doLogin = function (verifyData) {
|
||||
background #8d4bbb
|
||||
// background-image url("~@/assets/img/transparent-bg.png")
|
||||
// background-repeat:repeat;
|
||||
background-image url("~@/assets/img/admin-login-bg.jpg")
|
||||
background-image url("@/assets/img/admin-login-bg.jpg")
|
||||
background-size cover
|
||||
background-position center
|
||||
background-repeat no-repeat
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-table :data="items" :row-key="(row) => row.id" table-layout="auto" border style="width: 100%">
|
||||
<el-table
|
||||
:data="items"
|
||||
:row-key="(row) => row.id"
|
||||
table-layout="auto"
|
||||
border
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="name" label="菜单名称">
|
||||
<template #default="scope">
|
||||
<span class="sort" :data-id="scope.row.id">
|
||||
@@ -51,7 +57,12 @@
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
开放注册
|
||||
<el-tooltip effect="dark" content="可以填写 iconfont 图标名称也可以自己上传图片" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="可以填写 iconfont 图标名称也可以自己上传图片"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -89,49 +100,49 @@
|
||||
</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 { InfoFilled, Plus, UploadFilled } from "@element-plus/icons-vue";
|
||||
import { Sortable } from "sortablejs";
|
||||
import Compressor from "compressorjs";
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { dateFormat, removeArrayItem } from '@/utils/libs'
|
||||
import { InfoFilled, Plus, UploadFilled } from '@element-plus/icons-vue'
|
||||
import Compressor from 'compressorjs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Sortable } from 'sortablejs'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
// 变量定义
|
||||
const items = ref([]);
|
||||
const item = ref({});
|
||||
const showDialog = ref(false);
|
||||
const title = ref("");
|
||||
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);
|
||||
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")
|
||||
httpGet('/api/admin/menu/list')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
// 初始化数据
|
||||
const arr = 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);
|
||||
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
|
||||
}
|
||||
items.value = arr;
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const drawBodyWrapper = document.querySelector(".el-table__body tbody");
|
||||
fetchData();
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
fetchData()
|
||||
|
||||
// 初始化拖动排序插件
|
||||
Sortable.create(drawBodyWrapper, {
|
||||
@@ -139,80 +150,82 @@ onMounted(() => {
|
||||
animation: 500,
|
||||
onEnd({ newIndex, oldIndex, from }) {
|
||||
if (oldIndex === newIndex) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const sortedData = Array.from(from.children).map((row) => row.querySelector(".sort").getAttribute("data-id"));
|
||||
const ids = [];
|
||||
const sorts = [];
|
||||
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 + 1);
|
||||
items.value[index].sort_num = index + 1;
|
||||
});
|
||||
ids.push(parseInt(id))
|
||||
sorts.push(index + 1)
|
||||
items.value[index].sort_num = index + 1
|
||||
})
|
||||
|
||||
httpPost("/api/admin/menu/sort", { ids: ids, sorts: sorts }).catch((e) => {
|
||||
ElMessage.error("排序失败:" + e.message);
|
||||
});
|
||||
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 = {};
|
||||
};
|
||||
title.value = '新增菜单'
|
||||
showDialog.value = true
|
||||
item.value = {}
|
||||
}
|
||||
|
||||
const edit = function (row) {
|
||||
title.value = "修改菜单";
|
||||
showDialog.value = true;
|
||||
item.value = row;
|
||||
};
|
||||
title.value = '修改菜单'
|
||||
showDialog.value = true
|
||||
item.value = row
|
||||
}
|
||||
|
||||
const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showDialog.value = false;
|
||||
showDialog.value = false
|
||||
if (!item.value.id) {
|
||||
item.value.sort_num = items.value.length + 1;
|
||||
item.value.sort_num = items.value.length + 1
|
||||
}
|
||||
httpPost("/api/admin/menu/save", item.value)
|
||||
httpPost('/api/admin/menu/save', item.value)
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功!");
|
||||
fetchData();
|
||||
ElMessage.success('操作成功!')
|
||||
fetchData()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败," + e.message);
|
||||
});
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
} else {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const enable = (row) => {
|
||||
httpPost("/api/admin/menu/enable", { id: row.id, enabled: row.enabled })
|
||||
httpPost('/api/admin/menu/enable', { id: row.id, enabled: row.enabled })
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功!");
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const remove = function (row) {
|
||||
httpGet("/api/admin/menu/remove?id=" + row.id)
|
||||
httpGet('/api/admin/menu/remove?id=' + row.id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功!");
|
||||
ElMessage.success('删除成功!')
|
||||
items.value = removeArrayItem(items.value, row, (v1, v2) => {
|
||||
return v1.id === v2.id;
|
||||
});
|
||||
return v1.id === v2.id
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('删除失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 图片上传
|
||||
const uploadImg = (file) => {
|
||||
@@ -220,28 +233,28 @@ const uploadImg = (file) => {
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", result, result.name);
|
||||
const formData = new FormData()
|
||||
formData.append('file', result, result.name)
|
||||
// 执行上传操作
|
||||
httpPost("/api/admin/upload", formData)
|
||||
httpPost('/api/admin/upload', formData)
|
||||
.then((res) => {
|
||||
item.value.icon = res.data.url;
|
||||
ElMessage.success("上传成功");
|
||||
item.value.icon = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("上传失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(e) {
|
||||
ElMessage.error("上传失败:" + e.message);
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/admin/form.styl"
|
||||
@import "@/assets/css/main.styl"
|
||||
<style lang="stylus" scoped>
|
||||
@import "../../assets/css/admin/form.styl"
|
||||
@import "../../assets/css/main.styl"
|
||||
.menu {
|
||||
|
||||
.handle-box {
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
<el-tabs v-model="activeName" class="sys-tabs">
|
||||
<el-tab-pane label="系统配置" name="basic">
|
||||
<div class="container">
|
||||
<el-form :model="system" label-width="150px" label-position="right" ref="systemFormRef" :rules="rules">
|
||||
<el-form
|
||||
:model="system"
|
||||
label-width="150px"
|
||||
label-position="right"
|
||||
ref="systemFormRef"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="基础配置">
|
||||
<el-form-item label="网站标题" prop="title">
|
||||
@@ -18,7 +24,12 @@
|
||||
<el-form-item label="圆形 LOGO" prop="logo">
|
||||
<el-input v-model="system['logo']" placeholder="正方形或者圆形 Logo">
|
||||
<template #append>
|
||||
<el-upload :auto-upload="true" :show-file-list="false" @click="beforeUpload('logo')" :http-request="uploadImg">
|
||||
<el-upload
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
@click="beforeUpload('logo')"
|
||||
:http-request="uploadImg"
|
||||
>
|
||||
<el-icon class="uploader-icon">
|
||||
<UploadFilled />
|
||||
</el-icon>
|
||||
@@ -29,7 +40,12 @@
|
||||
<el-form-item label="条形 LOGO" prop="logo">
|
||||
<el-input v-model="system['bar_logo']" placeholder="长方形 Logo">
|
||||
<template #append>
|
||||
<el-upload :auto-upload="true" :show-file-list="false" @click="beforeUpload('bar_logo')" :http-request="uploadImg">
|
||||
<el-upload
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
@click="beforeUpload('bar_logo')"
|
||||
:http-request="uploadImg"
|
||||
>
|
||||
<el-icon class="uploader-icon">
|
||||
<UploadFilled />
|
||||
</el-icon>
|
||||
@@ -41,20 +57,43 @@
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
首页导航菜单
|
||||
<el-tooltip effect="dark" content="被选中的菜单将会在首页导航栏显示" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="被选中的菜单将会在首页导航栏显示"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="system['index_navs']" multiple :filterable="true" placeholder="请选择菜单,多选" style="width: 100%">
|
||||
<el-option v-for="item in menus" :key="item.id" :label="item.name" :value="item.id" />
|
||||
<el-select
|
||||
v-model="system['index_navs']"
|
||||
multiple
|
||||
:filterable="true"
|
||||
placeholder="请选择菜单,多选"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in menus"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="版权信息" prop="copyright">
|
||||
<el-input v-model="system['copyright']" placeholder="更改此选项需要获取 License 授权" />
|
||||
<el-input
|
||||
v-model="system['copyright']"
|
||||
placeholder="更改此选项需要获取 License 授权"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="默认昵称" prop="default_nickname">
|
||||
<el-input v-model="system['default_nickname']" placeholder="默认昵称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="ICP 备案号" prop="icp">
|
||||
@@ -65,7 +104,12 @@
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
开放注册
|
||||
<el-tooltip effect="dark" content="关闭注册之后只能通过管理后台添加用户" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="关闭注册之后只能通过管理后台添加用户"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -109,7 +153,12 @@
|
||||
<el-form-item label="微信客服二维码" prop="wechat_card_url">
|
||||
<el-input v-model="system['wechat_card_url']" placeholder="微信客服二维码">
|
||||
<template #append>
|
||||
<el-upload :auto-upload="true" :show-file-list="false" @click="beforeUpload('wechat_card_url')" :http-request="uploadImg">
|
||||
<el-upload
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
@click="beforeUpload('wechat_card_url')"
|
||||
:http-request="uploadImg"
|
||||
>
|
||||
<el-icon class="uploader-icon">
|
||||
<UploadFilled />
|
||||
</el-icon>
|
||||
@@ -121,15 +170,30 @@
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
默认翻译模型
|
||||
<el-tooltip effect="dark" content="选择一个默认模型来翻译提示词" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="选择一个默认模型来翻译提示词"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model.number="system['translate_model_id']" :filterable="true" placeholder="选择一个默认模型来翻译提示词" style="width: 100%">
|
||||
<el-option v-for="item in models" :key="item.id" :label="item.name" :value="item.id" />
|
||||
<el-select
|
||||
v-model.number="system['translate_model_id']"
|
||||
:filterable="true"
|
||||
placeholder="选择一个默认模型来翻译提示词"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -140,8 +204,8 @@
|
||||
<div class="tip-input-line">
|
||||
<el-input-number v-model="system['context_deep']" :min="0" :max="10" />
|
||||
<div class="tip">
|
||||
会话上下文深度:在老会话中继续会话,默认加载多少条聊天记录作为上下文。如果设置为 0
|
||||
则不加载聊天记录,仅仅使用当前角色的上下文。该配置参数必须设置需要为偶数。
|
||||
会话上下文深度:在老会话中继续会话,默认加载多少条聊天记录作为上下文。如果设置为
|
||||
0 则不加载聊天记录,仅仅使用当前角色的上下文。该配置参数必须设置需要为偶数。
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -150,55 +214,98 @@
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
SD反向提示词
|
||||
<el-tooltip effect="dark" content="Stable-Diffusion 绘画默认反向提示词" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="Stable-Diffusion 绘画默认反向提示词"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input type="textarea" :rows="2" v-model="system['sd_neg_prompt']" placeholder="" />
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
v-model="system['sd_neg_prompt']"
|
||||
placeholder=""
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="会员充值说明" prop="order_pay_timeout">
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
会员充值说明
|
||||
<el-tooltip effect="dark" content="会员充值页面的充值说明文字" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="会员充值页面的充值说明文字"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input type="textarea" :rows="2" v-model="system['vip_info_text']" placeholder="" />
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
v-model="system['vip_info_text']"
|
||||
placeholder=""
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="MJ默认API模式" prop="mj_mode">
|
||||
<el-select v-model="system['mj_mode']" placeholder="请选择模式">
|
||||
<el-option v-for="item in mjModels" :value="item.value" :label="item.name" :key="item.value">{{ item.name }} </el-option>
|
||||
<el-option
|
||||
v-for="item in mjModels"
|
||||
:value="item.value"
|
||||
:label="item.name"
|
||||
:key="item.value"
|
||||
>{{ item.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传文件限制" prop="max_file_size">
|
||||
<el-input v-model.number="system['max_file_size']" placeholder="最大上传文件大小,单位:MB" />
|
||||
<el-input
|
||||
v-model.number="system['max_file_size']"
|
||||
placeholder="最大上传文件大小,单位:MB"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="算力配置">
|
||||
<el-form-item label="注册赠送算力" prop="init_power">
|
||||
<el-input v-model.number="system['init_power']" placeholder="新用户注册赠送算力" />
|
||||
<el-input
|
||||
v-model.number="system['init_power']"
|
||||
placeholder="新用户注册赠送算力"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="邀请赠送算力" prop="invite_power">
|
||||
<el-input v-model.number="system['invite_power']" placeholder="邀请新用户注册赠送算力" />
|
||||
<el-input
|
||||
v-model.number="system['invite_power']"
|
||||
placeholder="邀请新用户注册赠送算力"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="VIP每月赠送算力" prop="vip_month_power">
|
||||
<el-input v-model.number="system['vip_month_power']" placeholder="VIP用户每月赠送算力" />
|
||||
<el-input
|
||||
v-model.number="system['vip_month_power']"
|
||||
placeholder="VIP用户每月赠送算力"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
签到赠送算力
|
||||
<el-tooltip effect="dark" content="每日签到赠送算力" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="每日签到赠送算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -211,7 +318,12 @@
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
MJ绘图算力
|
||||
<el-tooltip effect="dark" content="使用MidJourney画一张图消耗算力" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="使用MidJourney画一张图消耗算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -222,32 +334,54 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Stable-Diffusion算力" prop="sd_power">
|
||||
<el-input v-model.number="system['sd_power']" placeholder="使用Stable-Diffusion画一张图消耗算力" />
|
||||
<el-input
|
||||
v-model.number="system['sd_power']"
|
||||
placeholder="使用Stable-Diffusion画一张图消耗算力"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
DALL-E-3算力
|
||||
<el-tooltip effect="dark" content="使用DALL-E-3画一张图消耗算力" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="使用DALL-E-3画一张图消耗算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model.number="system['dall_power']" placeholder="使用DALL-E-3画一张图消耗算力" />
|
||||
<el-input
|
||||
v-model.number="system['dall_power']"
|
||||
placeholder="使用DALL-E-3画一张图消耗算力"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Suno 算力" prop="suno_power">
|
||||
<el-input v-model.number="system['suno_power']" placeholder="使用 Suno 生成一首音乐消耗算力" />
|
||||
<el-input
|
||||
v-model.number="system['suno_power']"
|
||||
placeholder="使用 Suno 生成一首音乐消耗算力"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Luma 算力" prop="luma_power">
|
||||
<el-input v-model.number="system['luma_power']" placeholder="使用 Luma 生成一段视频消耗算力" />
|
||||
<el-input
|
||||
v-model.number="system['luma_power']"
|
||||
placeholder="使用 Luma 生成一段视频消耗算力"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
可灵算力
|
||||
<el-tooltip effect="dark" content="可灵每个模型价格不一样,具体请参考:https://api.geekai.pro/models" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="可灵每个模型价格不一样,具体请参考:https://api.geekai.pro/models"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -255,7 +389,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<el-row :gutter="20" v-if="system['keling_powers']">
|
||||
<el-col :span="6" v-for="[key] in Object.entries(system['keling_powers'])" :key="key">
|
||||
<el-col
|
||||
:span="6"
|
||||
v-for="[key] in Object.entries(system['keling_powers'])"
|
||||
:key="key"
|
||||
>
|
||||
<el-form-item :label="key" label-position="left">
|
||||
<el-input v-model.number="system['keling_powers'][key]" size="small" />
|
||||
</el-form-item>
|
||||
@@ -266,7 +404,12 @@
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
高级语音算力
|
||||
<el-tooltip effect="dark" content="使用一次 OpenAI 高级语音对话消耗的算力" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="使用一次 OpenAI 高级语音对话消耗的算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -279,7 +422,12 @@
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
提示词算力
|
||||
<el-tooltip effect="dark" content="生成AI绘图提示词,歌词,视频描述消耗的算力" raw-content placement="right">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
content="生成AI绘图提示词,歌词,视频描述消耗的算力"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -300,15 +448,54 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="公告配置" name="notice">
|
||||
<md-editor class="mgb20" v-model="notice" :theme="store.theme" @on-upload-img="onUploadImg" />
|
||||
<md-editor
|
||||
class="mgb20"
|
||||
v-model="notice"
|
||||
:theme="store.theme"
|
||||
@on-upload-img="onUploadImg"
|
||||
/>
|
||||
<el-form-item>
|
||||
<div style="padding-top: 10px; margin-left: 150px">
|
||||
<el-button type="primary" @click="save('notice')">保存</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="用户协议" name="agreement">
|
||||
<md-editor
|
||||
class="mgb20"
|
||||
v-model="agreement"
|
||||
:theme="store.theme"
|
||||
@on-upload-img="onUploadImg"
|
||||
/>
|
||||
<el-form-item>
|
||||
<div style="padding-top: 10px; margin-left: 150px">
|
||||
<el-button type="primary" @click="save('agreement')">保存</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="隐私声明" name="privacy">
|
||||
<md-editor
|
||||
class="mgb20"
|
||||
v-model="privacy"
|
||||
:theme="store.theme"
|
||||
@on-upload-img="onUploadImg"
|
||||
/>
|
||||
<el-form-item>
|
||||
<div style="padding-top: 10px; margin-left: 150px">
|
||||
<el-button type="primary" @click="save('privacy')">保存</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="思维导图" name="mark_map">
|
||||
<md-editor class="mgb20" :theme="store.theme" v-model="system['mark_map_text']" @on-upload-img="onUploadImg" />
|
||||
<md-editor
|
||||
class="mgb20"
|
||||
:theme="store.theme"
|
||||
v-model="system['mark_map_text']"
|
||||
@on-upload-img="onUploadImg"
|
||||
/>
|
||||
<el-form-item>
|
||||
<div style="padding-top: 10px; margin-left: 150px">
|
||||
<el-button type="primary" @click="save('system')">保存</el-button>
|
||||
@@ -321,7 +508,13 @@
|
||||
|
||||
<el-tab-pane label="授权激活" name="license">
|
||||
<div class="container">
|
||||
<el-descriptions v-if="license.is_active" class="margin-top" title="已授权信息" :column="1" border>
|
||||
<el-descriptions
|
||||
v-if="license.is_active"
|
||||
class="margin-top"
|
||||
title="已授权信息"
|
||||
:column="1"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">License Key</div>
|
||||
@@ -374,168 +567,213 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="修复数据" name="fixData">
|
||||
<!-- <el-tab-pane label="修复数据" name="fixData">
|
||||
<div class="container">
|
||||
<p class="text">
|
||||
有些版本升级的时候更新了数据库的结构,比如字段名字改了,需要把之前的字段的值转移到其他字段,这些无法通过简单的 SQL
|
||||
语句可以实现的,需要手动写程序修正数据。
|
||||
有些版本升级的时候更新了数据库的结构,比如字段名字改了,需要把之前的字段的值转移到其他字段,这些无法通过简单的
|
||||
SQL 语句可以实现的,需要手动写程序修正数据。
|
||||
</p>
|
||||
|
||||
<!-- <p class="text">当前版本 v4.1.4 需要修正用户数据,增加了 mobile 和 email 字段,需要把之前用手机号或者邮箱注册的用户的 username 字段数据初始化到 mobile 或者 email 字段。另外,需要把订单的支付渠道从名字称修正为 key。</p>-->
|
||||
|
||||
<!-- <el-text type="danger">请注意:在修复数据前,请先备份好数据库,以免数据丢失!</el-text>-->
|
||||
|
||||
<div class="mt-3">
|
||||
<el-button type="primary" @click="fixData">立即修复</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import Compressor from "compressorjs";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { CloseBold, InfoFilled, Select, UploadFilled } from "@element-plus/icons-vue";
|
||||
import MdEditor from "md-editor-v3";
|
||||
import "md-editor-v3/lib/style.css";
|
||||
import Menu from "@/views/admin/Menu.vue";
|
||||
import { copyObj, dateFormat } from "@/utils/libs";
|
||||
import ItemsInput from "@/components/ui/ItemsInput.vue";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import ItemsInput from '@/components/ui/ItemsInput.vue'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { copyObj, dateFormat } from '@/utils/libs'
|
||||
import Menu from '@/views/admin/Menu.vue'
|
||||
import { CloseBold, InfoFilled, Select, UploadFilled } from '@element-plus/icons-vue'
|
||||
import Compressor from 'compressorjs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MdEditor from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/style.css'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
const activeName = ref("basic");
|
||||
const system = ref({ models: [] });
|
||||
const configBak = ref({});
|
||||
const loading = ref(true);
|
||||
const systemFormRef = ref(null);
|
||||
const models = ref([]);
|
||||
const notice = ref("");
|
||||
const license = ref({ is_active: false });
|
||||
const menus = ref([]);
|
||||
const activeName = ref('basic')
|
||||
const system = ref({ models: [] })
|
||||
const configBak = ref({})
|
||||
const loading = ref(true)
|
||||
const systemFormRef = ref(null)
|
||||
const models = ref([])
|
||||
const notice = ref('')
|
||||
const agreement = ref('')
|
||||
const privacy = ref('')
|
||||
const license = ref({ is_active: false })
|
||||
const menus = ref([])
|
||||
const mjModels = ref([
|
||||
{ name: "慢速(Relax)", value: "relax" },
|
||||
{ name: "快速(Fast)", value: "fast" },
|
||||
{ name: "急速(Turbo)", value: "turbo" },
|
||||
]);
|
||||
const store = useSharedStore();
|
||||
{ name: '慢速(Relax)', value: 'relax' },
|
||||
{ name: '快速(Fast)', value: 'fast' },
|
||||
{ name: '急速(Turbo)', value: 'turbo' },
|
||||
])
|
||||
const store = useSharedStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 加载系统配置
|
||||
httpGet("/api/admin/config/get?key=system")
|
||||
httpGet('/api/admin/config/get?key=system')
|
||||
.then((res) => {
|
||||
system.value = res.data;
|
||||
system.value = res.data
|
||||
system.value.keling_powers = system.value.keling_powers || {
|
||||
"kling-v1-6_std_5": 240,
|
||||
"kling-v1-6_std_10": 480,
|
||||
"kling-v1-6_pro_5": 420,
|
||||
"kling-v1-6_pro_10": 840,
|
||||
"kling-v1-5_std_5": 240,
|
||||
"kling-v1-5_std_10": 480,
|
||||
"kling-v1-5_pro_5": 420,
|
||||
"kling-v1-5_pro_10": 840,
|
||||
"kling-v1_std_5": 120,
|
||||
"kling-v1_std_10": 240,
|
||||
"kling-v1_pro_5": 420,
|
||||
"kling-v1_pro_10": 840,
|
||||
};
|
||||
configBak.value = copyObj(system.value);
|
||||
'kling-v1-6_std_5': 240,
|
||||
'kling-v1-6_std_10': 480,
|
||||
'kling-v1-6_pro_5': 420,
|
||||
'kling-v1-6_pro_10': 840,
|
||||
'kling-v1-5_std_5': 240,
|
||||
'kling-v1-5_std_10': 480,
|
||||
'kling-v1-5_pro_5': 420,
|
||||
'kling-v1-5_pro_10': 840,
|
||||
'kling-v1_std_5': 120,
|
||||
'kling-v1_std_10': 240,
|
||||
'kling-v1_pro_5': 420,
|
||||
'kling-v1_pro_10': 840,
|
||||
}
|
||||
configBak.value = copyObj(system.value)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("加载系统配置失败: " + e.message);
|
||||
});
|
||||
ElMessage.error('加载系统配置失败: ' + e.message)
|
||||
})
|
||||
// 加载聊天配置
|
||||
httpGet("/api/admin/config/get?key=notice")
|
||||
httpGet('/api/admin/config/get?key=notice')
|
||||
.then((res) => {
|
||||
notice.value = res.data["content"];
|
||||
notice.value = res.data['content']
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("公告信息失败: " + e.message);
|
||||
});
|
||||
ElMessage.error('公告信息失败: ' + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/admin/model/list")
|
||||
// 加载用户协议
|
||||
httpGet('/api/admin/config/get?key=agreement')
|
||||
.then((res) => {
|
||||
models.value = res.data;
|
||||
loading.value = false;
|
||||
agreement.value = res.data['content'] || ''
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取模型失败:" + e.message);
|
||||
});
|
||||
console.warn('加载用户协议失败: ' + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/admin/menu/list")
|
||||
// 加载隐私政策
|
||||
httpGet('/api/admin/config/get?key=privacy')
|
||||
.then((res) => {
|
||||
menus.value = res.data;
|
||||
privacy.value = res.data['content'] || ''
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取模型失败:" + e.message);
|
||||
});
|
||||
console.warn('加载隐私政策失败: ' + e.message)
|
||||
})
|
||||
|
||||
fetchLicense();
|
||||
});
|
||||
httpGet('/api/admin/model/list')
|
||||
.then((res) => {
|
||||
models.value = res.data
|
||||
loading.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取模型失败:' + e.message)
|
||||
})
|
||||
|
||||
httpGet('/api/admin/menu/list')
|
||||
.then((res) => {
|
||||
menus.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取模型失败:' + e.message)
|
||||
})
|
||||
|
||||
fetchLicense()
|
||||
})
|
||||
|
||||
const fetchLicense = () => {
|
||||
httpGet("/api/admin/config/license")
|
||||
httpGet('/api/admin/config/license')
|
||||
.then((res) => {
|
||||
license.value = res.data;
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取 License 失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取 License 失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
title: [{ required: true, message: "请输入网站标题", trigger: "blur" }],
|
||||
admin_title: [{ required: true, message: "请输入控制台标题", trigger: "blur" }],
|
||||
init_chat_calls: [{ required: true, message: "请输入赠送对话次数", trigger: "blur" }],
|
||||
user_img_calls: [{ required: true, message: "请输入赠送绘图次数", trigger: "blur" }],
|
||||
});
|
||||
title: [{ required: true, message: '请输入网站标题', trigger: 'blur' }],
|
||||
admin_title: [{ required: true, message: '请输入控制台标题', trigger: 'blur' }],
|
||||
init_chat_calls: [{ required: true, message: '请输入赠送对话次数', trigger: 'blur' }],
|
||||
user_img_calls: [{ required: true, message: '请输入赠送绘图次数', trigger: 'blur' }],
|
||||
})
|
||||
const save = function (key) {
|
||||
if (key === "system") {
|
||||
if (key === 'system') {
|
||||
systemFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
httpPost("/api/admin/config/update", { key: key, config: system.value, config_bak: configBak.value })
|
||||
httpPost('/api/admin/config/update', {
|
||||
key: key,
|
||||
config: system.value,
|
||||
config_bak: configBak.value,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功!");
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
}
|
||||
});
|
||||
} else if (key === "notice") {
|
||||
httpPost("/api/admin/config/update", { key: key, config: { content: notice.value, updated: true } })
|
||||
})
|
||||
} else if (key === 'notice') {
|
||||
httpPost('/api/admin/config/update', {
|
||||
key: key,
|
||||
config: { content: notice.value, updated: true },
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功!");
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
} else if (key === 'agreement') {
|
||||
httpPost('/api/admin/config/update', {
|
||||
key: key,
|
||||
config: { content: agreement.value, updated: true },
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
} else if (key === 'privacy') {
|
||||
httpPost('/api/admin/config/update', {
|
||||
key: key,
|
||||
config: { content: privacy.value, updated: true },
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('操作失败:' + e.message)
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 激活授权
|
||||
const licenseKey = ref("");
|
||||
const licenseKey = ref('')
|
||||
const active = () => {
|
||||
if (licenseKey.value === "") {
|
||||
return ElMessage.error("请输入授权码");
|
||||
if (licenseKey.value === '') {
|
||||
return ElMessage.error('请输入授权码')
|
||||
}
|
||||
httpPost("/api/admin/config/active", { license: licenseKey.value })
|
||||
httpPost('/api/admin/config/active', { license: licenseKey.value })
|
||||
.then((res) => {
|
||||
ElMessage.success("授权成功,机器编码为:" + res.data);
|
||||
fetchLicense();
|
||||
ElMessage.success('授权成功,机器编码为:' + res.data)
|
||||
fetchLicense()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error(e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const configKey = ref("");
|
||||
const configKey = ref('')
|
||||
const beforeUpload = (key) => {
|
||||
configKey.value = key;
|
||||
};
|
||||
configKey.value = key
|
||||
}
|
||||
|
||||
// 图片上传
|
||||
const uploadImg = (file) => {
|
||||
@@ -543,70 +781,70 @@ const uploadImg = (file) => {
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", result, result.name);
|
||||
const formData = new FormData()
|
||||
formData.append('file', result, result.name)
|
||||
// 执行上传操作
|
||||
httpPost("/api/admin/upload", formData)
|
||||
httpPost('/api/admin/upload', formData)
|
||||
.then((res) => {
|
||||
system.value[configKey.value] = res.data.url;
|
||||
ElMessage.success("上传成功");
|
||||
system.value[configKey.value] = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("上传失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(e) {
|
||||
ElMessage.error("上传失败:" + e.message);
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑期文件上传处理
|
||||
const onUploadImg = (files, callback) => {
|
||||
Promise.all(
|
||||
files.map((file) => {
|
||||
return new Promise((rev, rej) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file, file.name);
|
||||
const formData = new FormData()
|
||||
formData.append('file', file, file.name)
|
||||
// 执行上传操作
|
||||
httpPost("/api/admin/upload", formData)
|
||||
httpPost('/api/admin/upload', formData)
|
||||
.then((res) => rev(res))
|
||||
.catch((error) => rej(error));
|
||||
});
|
||||
.catch((error) => rej(error))
|
||||
})
|
||||
})
|
||||
)
|
||||
.then((res) => {
|
||||
ElMessage.success({ message: "上传成功", duration: 500 });
|
||||
callback(res.map((item) => item.data.url));
|
||||
ElMessage.success({ message: '上传成功', duration: 500 })
|
||||
callback(res.map((item) => item.data.url))
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("图片上传失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const fixData = () => {
|
||||
ElMessageBox.confirm("在修复数据前,请先备份好数据库,以免数据丢失!是否继续操作?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(() => {
|
||||
loading.value = true;
|
||||
httpGet("/api/admin/config/fixData")
|
||||
.then(() => {
|
||||
ElMessage.success("数据修复成功");
|
||||
loading.value = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
ElMessage.error("数据修复失败:" + e.message);
|
||||
});
|
||||
});
|
||||
};
|
||||
// const fixData = () => {
|
||||
// ElMessageBox.confirm('在修复数据前,请先备份好数据库,以免数据丢失!是否继续操作?', '警告', {
|
||||
// confirmButtonText: '确定',
|
||||
// cancelButtonText: '取消',
|
||||
// type: 'warning',
|
||||
// }).then(() => {
|
||||
// loading.value = true
|
||||
// httpGet('/api/admin/config/fixData')
|
||||
// .then(() => {
|
||||
// ElMessage.success('数据修复成功')
|
||||
// loading.value = false
|
||||
// })
|
||||
// .catch((e) => {
|
||||
// loading.value = false
|
||||
// ElMessage.error('数据修复失败:' + e.message)
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/admin/form.styl"
|
||||
@import "@/assets/css/main.styl"
|
||||
@import '../../assets/css/admin/form.styl'
|
||||
@import '../../assets/css/main.styl'
|
||||
.system-config {
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
class="chat-dialog"
|
||||
style="--el-dialog-width: 60%"
|
||||
>
|
||||
<div class="chat-box chat-page">
|
||||
<div class="chat-box chat-page p-2">
|
||||
<div v-for="item in messages" :key="item.id">
|
||||
<chat-prompt v-if="item.type === 'prompt'" :data="item" />
|
||||
<chat-reply v-else-if="item.type === 'reply'" :read-only="true" :data="item" />
|
||||
@@ -206,6 +206,8 @@ import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import hl from 'highlight.js'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
// 变量定义
|
||||
@@ -316,8 +318,7 @@ const removeMessage = function (row) {
|
||||
})
|
||||
}
|
||||
|
||||
const mathjaxPlugin = require('markdown-it-mathjax3')
|
||||
const md = require('markdown-it')({
|
||||
const md = MarkdownIt({
|
||||
breaks: true,
|
||||
html: true,
|
||||
linkify: true,
|
||||
@@ -325,7 +326,7 @@ const md = require('markdown-it')({
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
// 处理代码高亮
|
||||
const preCode = hl.highlight(lang, str, true).value
|
||||
const preCode = hl.highlight(str, { language: lang }).value
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
|
||||
}
|
||||
|
||||
@@ -3,8 +3,20 @@
|
||||
<el-tabs v-model="activeName" @tab-change="handleChange">
|
||||
<el-tab-pane label="Midjourney" name="mj" v-loading="data.mj.loading">
|
||||
<div class="handle-box">
|
||||
<el-input v-model="data.mj.query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, 'mj')" clearable />
|
||||
<el-input v-model="data.mj.query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, 'mj')" clearable />
|
||||
<el-input
|
||||
v-model="data.mj.query.username"
|
||||
placeholder="用户名"
|
||||
class="handle-input mr10"
|
||||
@keyup="search($event, 'mj')"
|
||||
clearable
|
||||
/>
|
||||
<el-input
|
||||
v-model="data.mj.query.prompt"
|
||||
placeholder="提示词"
|
||||
class="handle-input mr10"
|
||||
@keyup="search($event, 'mj')"
|
||||
clearable
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="data.mj.query.created_at"
|
||||
type="daterange"
|
||||
@@ -23,7 +35,9 @@
|
||||
<el-table-column prop="user_id" label="用户ID" />
|
||||
<el-table-column label="任务类型">
|
||||
<template #default="scope">
|
||||
<el-button :color="taskTypeTheme[scope.row.type].color" size="small" plain>{{ taskTypeTheme[scope.row.type].text }}</el-button>
|
||||
<el-button :color="taskTypeTheme[scope.row.type].color" size="small" plain>{{
|
||||
taskTypeTheme[scope.row.type].text
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="progress" label="任务进度">
|
||||
@@ -35,12 +49,25 @@
|
||||
<el-table-column prop="power" label="消耗算力" />
|
||||
<el-table-column label="结果图片">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
@click="showImage(scope.row.img_url)"
|
||||
v-if="scope.row.img_url !== ''"
|
||||
plain
|
||||
>预览图片</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提示词">
|
||||
<template #default="scope">
|
||||
<el-popover placement="top-start" title="绘画提示词" :width="300" trigger="hover" :content="scope.row.prompt">
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
title="绘画提示词"
|
||||
:width="300"
|
||||
trigger="hover"
|
||||
:content="scope.row.prompt"
|
||||
>
|
||||
<template #reference>
|
||||
<span>{{ substr(scope.row.prompt, 20) }}</span>
|
||||
</template>
|
||||
@@ -49,7 +76,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败原因">
|
||||
@@ -98,8 +125,20 @@
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Stable-Diffusion" name="sd" v-loading="data.sd.loading">
|
||||
<div class="handle-box">
|
||||
<el-input v-model="data.sd.query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, 'sd')" clearable />
|
||||
<el-input v-model="data.sd.query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, 'sd')" clearable />
|
||||
<el-input
|
||||
v-model="data.sd.query.username"
|
||||
placeholder="用户名"
|
||||
class="handle-input mr10"
|
||||
@keyup="search($event, 'sd')"
|
||||
clearable
|
||||
/>
|
||||
<el-input
|
||||
v-model="data.sd.query.prompt"
|
||||
placeholder="提示词"
|
||||
class="handle-input mr10"
|
||||
@keyup="search($event, 'sd')"
|
||||
clearable
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="data.sd.query.created_at"
|
||||
type="daterange"
|
||||
@@ -125,12 +164,25 @@
|
||||
<el-table-column prop="power" label="消耗算力" />
|
||||
<el-table-column label="结果图片">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
@click="showImage(scope.row.img_url)"
|
||||
v-if="scope.row.img_url !== ''"
|
||||
plain
|
||||
>预览图片</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提示词">
|
||||
<template #default="scope">
|
||||
<el-popover placement="top-start" title="绘画提示词" :width="300" trigger="hover" :content="scope.row.prompt">
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
title="绘画提示词"
|
||||
:width="300"
|
||||
trigger="hover"
|
||||
:content="scope.row.prompt"
|
||||
>
|
||||
<template #reference>
|
||||
<span>{{ substr(scope.row.prompt, 20) }}</span>
|
||||
</template>
|
||||
@@ -139,7 +191,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败原因">
|
||||
@@ -188,8 +240,20 @@
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="DALL-E" name="dall">
|
||||
<div class="handle-box">
|
||||
<el-input v-model="data.dall.query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, 'dall')" clearable />
|
||||
<el-input v-model="data.dall.query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, 'dall')" clearable />
|
||||
<el-input
|
||||
v-model="data.dall.query.username"
|
||||
placeholder="用户名"
|
||||
class="handle-input mr10"
|
||||
@keyup="search($event, 'dall')"
|
||||
clearable
|
||||
/>
|
||||
<el-input
|
||||
v-model="data.dall.query.prompt"
|
||||
placeholder="提示词"
|
||||
class="handle-input mr10"
|
||||
@keyup="search($event, 'dall')"
|
||||
clearable
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="data.dall.query.created_at"
|
||||
type="daterange"
|
||||
@@ -215,12 +279,25 @@
|
||||
<el-table-column prop="power" label="消耗算力" />
|
||||
<el-table-column label="结果图片">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
@click="showImage(scope.row.img_url)"
|
||||
v-if="scope.row.img_url !== ''"
|
||||
plain
|
||||
>预览图片</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提示词">
|
||||
<template #default="scope">
|
||||
<el-popover placement="top-start" title="绘画提示词" :width="300" trigger="hover" :content="scope.row.prompt">
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
title="绘画提示词"
|
||||
:width="300"
|
||||
trigger="hover"
|
||||
:content="scope.row.prompt"
|
||||
>
|
||||
<template #reference>
|
||||
<span>{{ substr(scope.row.prompt, 20) }}</span>
|
||||
</template>
|
||||
@@ -229,7 +306,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败原因">
|
||||
@@ -278,24 +355,32 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="showImageDialog" title="图片预览">
|
||||
<el-image :src="imgURL" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2" :preview-src-list="[imgURL]" :initial-index="0" fit="cover" />
|
||||
<el-dialog v-model="showImageDialog" title="图片预览" style="height: 95vh; overflow: auto">
|
||||
<el-image
|
||||
:src="imgURL"
|
||||
:zoom-rate="1.2"
|
||||
:max-scale="7"
|
||||
:min-scale="0.2"
|
||||
:preview-src-list="[imgURL]"
|
||||
:initial-index="0"
|
||||
fit="cover"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { dateFormat, substr } from "@/utils/libs";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { dateFormat, substr } from '@/utils/libs'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
// 变量定义
|
||||
const data = ref({
|
||||
mj: {
|
||||
items: [],
|
||||
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
|
||||
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
@@ -303,7 +388,7 @@ const data = ref({
|
||||
},
|
||||
sd: {
|
||||
items: [],
|
||||
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
|
||||
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
@@ -311,122 +396,122 @@ const data = ref({
|
||||
},
|
||||
dall: {
|
||||
items: [],
|
||||
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
|
||||
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
loading: true,
|
||||
},
|
||||
});
|
||||
const activeName = ref("mj");
|
||||
})
|
||||
const activeName = ref('mj')
|
||||
const taskTypeTheme = {
|
||||
image: { text: "绘图", color: "#2185d0" },
|
||||
upscale: { text: "放大", color: "#f2711c" },
|
||||
variation: { text: "变换", color: "#00b5ad" },
|
||||
blend: { text: "融图", color: "#21ba45" },
|
||||
swapFace: { text: "换脸", color: "#a333c8" },
|
||||
};
|
||||
image: { text: '绘图', color: '#2185d0' },
|
||||
upscale: { text: '放大', color: '#f2711c' },
|
||||
variation: { text: '变换', color: '#00b5ad' },
|
||||
blend: { text: '融图', color: '#21ba45' },
|
||||
swapFace: { text: '换脸', color: '#a333c8' },
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchMjData();
|
||||
});
|
||||
fetchMjData()
|
||||
})
|
||||
|
||||
const handleChange = (tab) => {
|
||||
switch (tab) {
|
||||
case "mj":
|
||||
fetchMjData();
|
||||
break;
|
||||
case "sd":
|
||||
fetchSdData();
|
||||
break;
|
||||
case "dall":
|
||||
fetchDallData();
|
||||
break;
|
||||
case 'mj':
|
||||
fetchMjData()
|
||||
break
|
||||
case 'sd':
|
||||
fetchSdData()
|
||||
break
|
||||
case 'dall':
|
||||
fetchDallData()
|
||||
break
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 搜索对话
|
||||
const search = (evt, tab) => {
|
||||
if (evt.keyCode === 13) {
|
||||
handleChange(tab);
|
||||
handleChange(tab)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchMjData = () => {
|
||||
const d = data.value.mj;
|
||||
d.query.page = d.page;
|
||||
d.query.page_size = d.pageSize;
|
||||
httpPost("/api/admin/image/list/mj", d.query)
|
||||
const d = data.value.mj
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
httpPost('/api/admin/image/list/mj', d.query)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items;
|
||||
d.total = res.data.total;
|
||||
d.page = res.data.page;
|
||||
d.pageSize = res.data.page_size;
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false;
|
||||
d.loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchSdData = () => {
|
||||
const d = data.value.sd;
|
||||
d.query.page = d.page;
|
||||
d.query.page_size = d.pageSize;
|
||||
httpPost("/api/admin/image/list/sd", d.query)
|
||||
const d = data.value.sd
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
httpPost('/api/admin/image/list/sd', d.query)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items;
|
||||
d.total = res.data.total;
|
||||
d.page = res.data.page;
|
||||
d.pageSize = res.data.page_size;
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false;
|
||||
d.loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchDallData = () => {
|
||||
const d = data.value.dall;
|
||||
d.query.page = d.page;
|
||||
d.query.page_size = d.pageSize;
|
||||
httpPost("/api/admin/image/list/dall", d.query)
|
||||
const d = data.value.dall
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
httpPost('/api/admin/image/list/dall', d.query)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items;
|
||||
d.total = res.data.total;
|
||||
d.page = res.data.page;
|
||||
d.pageSize = res.data.page_size;
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false;
|
||||
d.loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const remove = function (row, tab) {
|
||||
httpGet(`/api/admin/image/remove?id=${row.id}&tab=${tab}`)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功!");
|
||||
handleChange(tab);
|
||||
ElMessage.success('删除成功!')
|
||||
handleChange(tab)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('删除失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showImageDialog = ref(false);
|
||||
const imgURL = ref("");
|
||||
const showImageDialog = ref(false)
|
||||
const imgURL = ref('')
|
||||
const showImage = (url) => {
|
||||
showImageDialog.value = true;
|
||||
imgURL.value = url;
|
||||
};
|
||||
showImageDialog.value = true
|
||||
imgURL.value = url
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
<template>
|
||||
<div class="container media-page">
|
||||
<el-tabs v-model="activeName" @tab-change="handleChange">
|
||||
<el-tab-pane v-for="media in mediaTypes" :key="media.name" :label="media.label" :name="media.name" v-loading="data[media.name].loading">
|
||||
<el-tab-pane
|
||||
v-for="media in mediaTypes"
|
||||
:key="media.name"
|
||||
:label="media.label"
|
||||
:name="media.name"
|
||||
v-loading="data[media.name].loading"
|
||||
>
|
||||
<div class="handle-box">
|
||||
<el-input v-model="data[media.name].query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, media.name)" clearable />
|
||||
<el-input v-model="data[media.name].query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, media.name)" clearable />
|
||||
<el-input
|
||||
v-model="data[media.name].query.username"
|
||||
placeholder="用户名"
|
||||
class="handle-input mr10"
|
||||
@keyup="search($event, media.name)"
|
||||
clearable
|
||||
/>
|
||||
<el-input
|
||||
v-model="data[media.name].query.prompt"
|
||||
placeholder="提示词"
|
||||
class="handle-input mr10"
|
||||
@keyup="search($event, media.name)"
|
||||
clearable
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="data[media.name].query.created_at"
|
||||
type="daterange"
|
||||
@@ -28,7 +46,10 @@
|
||||
<div class="duration">
|
||||
{{ formatTime(scope.row.duration) }}
|
||||
</div>
|
||||
<button class="play" @click="playMusic(scope.row)">
|
||||
<button
|
||||
class="play flex justify-center items-center"
|
||||
@click="playMusic(scope.row)"
|
||||
>
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -37,12 +58,27 @@
|
||||
<template #default="scope" v-if="media.previewComponent === 'VideoPreview'">
|
||||
<div class="container">
|
||||
<div v-if="scope.row.progress === 100">
|
||||
<video class="video" :src="replaceImg(scope.row.video_url)" preload="auto" loop="loop" muted="muted">您的浏览器不支持视频播放</video>
|
||||
<button class="play" @click="playVideo(scope.row)">
|
||||
<video
|
||||
class="video"
|
||||
:src="replaceImg(scope.row.video_url)"
|
||||
preload="auto"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<button
|
||||
class="play flex justify-center items-center"
|
||||
@click="playVideo(scope.row)"
|
||||
>
|
||||
<img src="/images/play.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
<el-image :src="scope.row.cover_url" fit="cover" v-else-if="scope.row.progress > 100" />
|
||||
<el-image
|
||||
:src="scope.row.cover_url"
|
||||
fit="cover"
|
||||
v-else-if="scope.row.progress > 100"
|
||||
/>
|
||||
<generating message="正在生成视频" v-else />
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,13 +95,21 @@
|
||||
<el-table-column prop="play_times" label="播放次数" />
|
||||
<el-table-column label="歌词">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" plain @click="showLyric(scope.row)">查看歌词</el-button>
|
||||
<el-button size="small" type="primary" plain @click="showLyric(scope.row)"
|
||||
>查看歌词</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column label="提示词" v-if="media.previewComponent === 'VideoPreview'">
|
||||
<template #default="scope">
|
||||
<el-popover placement="top-start" title="提示词" :width="300" trigger="hover" :content="scope.row.prompt">
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
title="提示词"
|
||||
:width="300"
|
||||
trigger="hover"
|
||||
:content="scope.row.prompt"
|
||||
>
|
||||
<template #reference>
|
||||
<span>{{ substr(scope.row.prompt, 20) }}</span>
|
||||
</template>
|
||||
@@ -74,7 +118,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败原因">
|
||||
@@ -96,7 +140,10 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row, media.name)">
|
||||
<el-popconfirm
|
||||
title="确定要删除当前记录吗?"
|
||||
@confirm="remove(scope.row, media.name)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
</template>
|
||||
@@ -123,13 +170,25 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-dialog v-model="showVideoDialog" title="视频预览">
|
||||
<video style="width: 100%; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted">
|
||||
<video
|
||||
style="width: 100%; max-height: 90vh"
|
||||
:src="currentVideoUrl"
|
||||
preload="auto"
|
||||
:autoplay="true"
|
||||
loop="loop"
|
||||
muted="muted"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</el-dialog>
|
||||
|
||||
<div class="music-player" v-if="showPlayer">
|
||||
<music-player :songs="playList" ref="playerRef" :show-close="true" @close="showPlayer = false" />
|
||||
<music-player
|
||||
:songs="playList"
|
||||
ref="playerRef"
|
||||
:show-close="true"
|
||||
@close="showPlayer = false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showLyricDialog" title="歌词">
|
||||
@@ -139,18 +198,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { dateFormat, formatTime, replaceImg, substr } from "@/utils/libs";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import MusicPlayer from "@/components/MusicPlayer.vue";
|
||||
import Generating from "@/components/ui/Generating.vue";
|
||||
import MusicPlayer from '@/components/MusicPlayer.vue'
|
||||
import Generating from '@/components/ui/Generating.vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { dateFormat, formatTime, replaceImg, substr } from '@/utils/libs'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
const data = ref({
|
||||
suno: {
|
||||
items: [],
|
||||
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
|
||||
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
@@ -158,7 +218,7 @@ const data = ref({
|
||||
},
|
||||
luma: {
|
||||
items: [],
|
||||
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
|
||||
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
@@ -166,169 +226,169 @@ const data = ref({
|
||||
},
|
||||
keling: {
|
||||
items: [],
|
||||
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
|
||||
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
loading: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const mediaTypes = [
|
||||
{
|
||||
name: "suno",
|
||||
label: "Suno音乐",
|
||||
name: 'suno',
|
||||
label: 'Suno音乐',
|
||||
fetchData: () => fetchSunoData(),
|
||||
previewComponent: "MusicPreview",
|
||||
previewComponent: 'MusicPreview',
|
||||
},
|
||||
{
|
||||
name: "luma",
|
||||
label: "Luma视频",
|
||||
name: 'luma',
|
||||
label: 'Luma视频',
|
||||
fetchData: () => fetchLumaData(),
|
||||
previewComponent: "VideoPreview",
|
||||
previewComponent: 'VideoPreview',
|
||||
},
|
||||
{
|
||||
name: "keling",
|
||||
label: "可灵视频",
|
||||
name: 'keling',
|
||||
label: '可灵视频',
|
||||
fetchData: () => fetchKelingData(),
|
||||
previewComponent: "VideoPreview",
|
||||
previewComponent: 'VideoPreview',
|
||||
},
|
||||
];
|
||||
const activeName = ref("suno");
|
||||
const playList = ref([]);
|
||||
const playerRef = ref(null);
|
||||
const showPlayer = ref(false);
|
||||
const showLyricDialog = ref(false);
|
||||
const lyrics = ref("");
|
||||
const showVideoDialog = ref(false);
|
||||
const currentVideoUrl = ref("");
|
||||
]
|
||||
const activeName = ref('suno')
|
||||
const playList = ref([])
|
||||
const playerRef = ref(null)
|
||||
const showPlayer = ref(false)
|
||||
const showLyricDialog = ref(false)
|
||||
const lyrics = ref('')
|
||||
const showVideoDialog = ref(false)
|
||||
const currentVideoUrl = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
fetchSunoData();
|
||||
});
|
||||
fetchSunoData()
|
||||
})
|
||||
|
||||
const handleChange = (tab) => {
|
||||
switch (tab) {
|
||||
case "suno":
|
||||
fetchSunoData();
|
||||
break;
|
||||
case "luma":
|
||||
fetchLumaData();
|
||||
break;
|
||||
case "keling":
|
||||
fetchKelingData();
|
||||
break;
|
||||
case 'suno':
|
||||
fetchSunoData()
|
||||
break
|
||||
case 'luma':
|
||||
fetchLumaData()
|
||||
break
|
||||
case 'keling':
|
||||
fetchKelingData()
|
||||
break
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 搜索对话
|
||||
const search = (evt, tab) => {
|
||||
if (evt.keyCode === 13) {
|
||||
handleChange(tab);
|
||||
handleChange(tab)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchSunoData = () => {
|
||||
const d = data.value.suno;
|
||||
d.query.page = d.page;
|
||||
d.query.page_size = d.pageSize;
|
||||
httpPost("/api/admin/media/suno", d.query)
|
||||
const d = data.value.suno
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
httpPost('/api/admin/media/suno', d.query)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items;
|
||||
d.total = res.data.total;
|
||||
d.page = res.data.page;
|
||||
d.pageSize = res.data.page_size;
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false;
|
||||
d.loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchLumaData = () => {
|
||||
const d = data.value.luma;
|
||||
d.query.page = d.page;
|
||||
d.query.page_size = d.pageSize;
|
||||
d.query.type = "luma";
|
||||
httpPost("/api/admin/media/videos", d.query)
|
||||
const d = data.value.luma
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
d.query.type = 'luma'
|
||||
httpPost('/api/admin/media/videos', d.query)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items;
|
||||
d.total = res.data.total;
|
||||
d.page = res.data.page;
|
||||
d.pageSize = res.data.page_size;
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false;
|
||||
d.loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
const fetchKelingData = () => {
|
||||
const d = data.value.keling;
|
||||
d.query.page = d.page;
|
||||
d.query.page_size = d.pageSize;
|
||||
d.query.type = "keling";
|
||||
httpPost("/api/admin/media/videos", d.query)
|
||||
const d = data.value.keling
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
d.query.type = 'keling'
|
||||
httpPost('/api/admin/media/videos', d.query)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items;
|
||||
d.total = res.data.total;
|
||||
d.page = res.data.page;
|
||||
d.pageSize = res.data.page_size;
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false;
|
||||
d.loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const remove = function (row, tab) {
|
||||
httpGet(`/api/admin/media/remove?id=${row.id}&tab=${tab}`)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功!");
|
||||
handleChange(tab);
|
||||
ElMessage.success('删除成功!')
|
||||
handleChange(tab)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message);
|
||||
ElMessage.error('删除失败:' + e.message)
|
||||
})
|
||||
.finally(() => {
|
||||
nextTick(() => {
|
||||
// data.value[tab].page = 1;
|
||||
// data.value[tab].pageSize = 10;
|
||||
const mediaType = mediaTypes.find((type) => type.name === tab);
|
||||
const mediaType = mediaTypes.find((type) => type.name === tab)
|
||||
if (mediaType && mediaType.fetchData) {
|
||||
mediaType.fetchData();
|
||||
mediaType.fetchData()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const playMusic = (item) => {
|
||||
playList.value = [item];
|
||||
showPlayer.value = true;
|
||||
nextTick(() => playerRef.value.play());
|
||||
};
|
||||
playList.value = [item]
|
||||
showPlayer.value = true
|
||||
nextTick(() => playerRef.value.play())
|
||||
}
|
||||
|
||||
const playVideo = (item) => {
|
||||
currentVideoUrl.value = replaceImg(item.video_url);
|
||||
showVideoDialog.value = true;
|
||||
};
|
||||
currentVideoUrl.value = replaceImg(item.video_url)
|
||||
showVideoDialog.value = true
|
||||
}
|
||||
|
||||
const md = require("markdown-it")({
|
||||
const md = MarkdownIt({
|
||||
breaks: true,
|
||||
html: true,
|
||||
linkify: true,
|
||||
});
|
||||
})
|
||||
|
||||
const showLyric = (item) => {
|
||||
showLyricDialog.value = true;
|
||||
lyrics.value = md.render(item.prompt);
|
||||
};
|
||||
showLyricDialog.value = true
|
||||
lyrics.value = md.render(item.prompt)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -15,43 +15,47 @@
|
||||
<div class="chat-list-wrapper">
|
||||
<div id="message-list-box" class="message-list-box">
|
||||
<van-list
|
||||
v-model:error="error"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
@load="onLoad"
|
||||
v-model:error="error"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-cell v-for="item in chatData" :key="item" :border="false" class="message-line">
|
||||
<chat-prompt
|
||||
v-if="item.type==='prompt'"
|
||||
:content="item.content"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:icon="item.icon"
|
||||
:tokens="item['tokens']"/>
|
||||
<chat-reply v-else-if="item.type==='reply'"
|
||||
:content="item.content"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:icon="item.icon"
|
||||
:org-content="item.orgContent"
|
||||
:tokens="item['tokens']"/>
|
||||
v-if="item.type === 'prompt'"
|
||||
:content="item.content"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:icon="item.icon"
|
||||
:tokens="item['tokens']"
|
||||
/>
|
||||
<chat-reply
|
||||
v-else-if="item.type === 'reply'"
|
||||
:content="item.content"
|
||||
:created-at="dateFormat(item['created_at'])"
|
||||
:icon="item.icon"
|
||||
:org-content="item.orgContent"
|
||||
:tokens="item['tokens']"
|
||||
/>
|
||||
</van-cell>
|
||||
</van-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end chat box -->
|
||||
</div>
|
||||
<!-- end chat box -->
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import {dateFormat, processContent} from "@/utils/libs";
|
||||
import ChatReply from "@/components/mobile/ChatReply.vue";
|
||||
import ChatPrompt from "@/components/mobile/ChatPrompt.vue";
|
||||
import {nextTick, ref} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import ChatPrompt from '@/components/mobile/ChatPrompt.vue'
|
||||
import ChatReply from '@/components/mobile/ChatReply.vue'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { dateFormat, processContent } from '@/utils/libs'
|
||||
import hl from 'highlight.js'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import hl from "highlight.js";
|
||||
import {showFailToast} from "vant";
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { showFailToast } from 'vant'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const chatData = ref([])
|
||||
const router = useRouter()
|
||||
@@ -62,8 +66,7 @@ const model = ref('')
|
||||
const finished = ref(false)
|
||||
const error = ref(false)
|
||||
|
||||
const mathjaxPlugin = require('markdown-it-mathjax3')
|
||||
const md = require('markdown-it')({
|
||||
const md = new MarkdownIt({
|
||||
breaks: true,
|
||||
html: true,
|
||||
linkify: true,
|
||||
@@ -72,11 +75,14 @@ const md = require('markdown-it')({
|
||||
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
|
||||
// 显示复制代码按钮
|
||||
const copyBtn = `<span class="copy-code-mobile" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '</textarea>')}</textarea>`
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
|
||||
/<\/textarea>/g,
|
||||
'</textarea>'
|
||||
)}</textarea>`
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`
|
||||
// 处理代码高亮
|
||||
const preCode = hl.highlight(lang, str, true).value
|
||||
const preCode = hl.highlight(str, { language: lang }).value
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
|
||||
}
|
||||
@@ -84,50 +90,52 @@ const md = require('markdown-it')({
|
||||
// 处理代码高亮
|
||||
const preCode = md.utils.escapeHtml(str)
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
|
||||
}
|
||||
});
|
||||
return `<pre class="code-container">${code}${copyBtn}</pre>`
|
||||
},
|
||||
})
|
||||
md.use(mathjaxPlugin)
|
||||
|
||||
const onLoad = () => {
|
||||
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
|
||||
// 加载状态结束
|
||||
finished.value = true;
|
||||
const data = res.data
|
||||
if (data && data.length > 0) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].type === "prompt") {
|
||||
chatData.value.push(data[i]);
|
||||
continue;
|
||||
httpGet('/api/chat/history?chat_id=' + chatId)
|
||||
.then((res) => {
|
||||
// 加载状态结束
|
||||
finished.value = true
|
||||
const data = res.data
|
||||
if (data && data.length > 0) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].type === 'prompt') {
|
||||
chatData.value.push(data[i])
|
||||
continue
|
||||
}
|
||||
|
||||
data[i].orgContent = data[i].content
|
||||
data[i].content = md.render(processContent(data[i].content))
|
||||
chatData.value.push(data[i])
|
||||
}
|
||||
|
||||
data[i].orgContent = data[i].content;
|
||||
data[i].content = md.render(processContent(data[i].content))
|
||||
chatData.value.push(data[i]);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
hl.configure({ignoreUnescapedHTML: true})
|
||||
const blocks = document.querySelector("#message-list-box").querySelectorAll('pre code');
|
||||
blocks.forEach((block) => {
|
||||
hl.highlightElement(block)
|
||||
nextTick(() => {
|
||||
hl.configure({ ignoreUnescapedHTML: true })
|
||||
const blocks = document.querySelector('#message-list-box').querySelectorAll('pre code')
|
||||
blocks.forEach((block) => {
|
||||
hl.highlightElement(block)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}).catch(() => {
|
||||
error.value = true
|
||||
})
|
||||
|
||||
httpGet(`/api/chat/detail?chat_id=${chatId}`).then(res => {
|
||||
title.value = res.data.title
|
||||
model.value = res.data.model
|
||||
role.value = res.data.role_name
|
||||
}).catch(e => {
|
||||
showFailToast('加载对话失败:' + e.message)
|
||||
})
|
||||
|
||||
};
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
error.value = true
|
||||
})
|
||||
|
||||
httpGet(`/api/chat/detail?chat_id=${chatId}`)
|
||||
.then((res) => {
|
||||
title.value = res.data.title
|
||||
model.value = res.data.model
|
||||
role.value = res.data.role_name
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('加载对话失败:' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="stylus">
|
||||
.chat-export-mobile {
|
||||
@@ -171,4 +179,4 @@ const onLoad = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -2,46 +2,43 @@
|
||||
<div class="app-background">
|
||||
<div class="container mobile-chat-list">
|
||||
<van-nav-bar
|
||||
:title="title"
|
||||
left-text="新建会话"
|
||||
@click-left="showPicker = true"
|
||||
custom-class="navbar"
|
||||
:title="title"
|
||||
left-text="新建会话"
|
||||
@click-left="showPicker = true"
|
||||
custom-class="navbar"
|
||||
>
|
||||
<template #right>
|
||||
<van-icon name="delete-o" @click="clearAllChatHistory"/>
|
||||
<van-icon name="delete-o" @click="clearAllChatHistory" />
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<div class="content">
|
||||
<van-search
|
||||
v-model="chatName"
|
||||
input-align="center"
|
||||
placeholder="请输入会话标题"
|
||||
custom-class="van-search"
|
||||
@input="search"
|
||||
v-model="chatName"
|
||||
input-align="center"
|
||||
placeholder="请输入会话标题"
|
||||
custom-class="van-search"
|
||||
@input="search"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-swipe-cell v-for="item in chats" :key="item.id">
|
||||
<van-cell @click="changeChat(item)">
|
||||
<div class="chat-list-item">
|
||||
<van-image
|
||||
:src="item.icon"
|
||||
round
|
||||
/>
|
||||
<van-image :src="item.icon" round />
|
||||
<div class="van-ellipsis">{{ item.title }}</div>
|
||||
</div>
|
||||
</van-cell>
|
||||
<template #right>
|
||||
<van-button square text="修改" type="primary" @click="editChat(item)"/>
|
||||
<van-button square text="删除" type="danger" @click="removeChat(item)"/>
|
||||
<van-button square text="修改" type="primary" @click="editChat(item)" />
|
||||
<van-button square text="删除" type="danger" @click="removeChat(item)" />
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</van-list>
|
||||
@@ -50,43 +47,43 @@
|
||||
|
||||
<van-popup v-model:show="showPicker" position="bottom" class="popup">
|
||||
<van-picker
|
||||
:columns="columns"
|
||||
title="选择模型和角色"
|
||||
@change="onChange"
|
||||
@cancel="showPicker = false"
|
||||
@confirm="newChat"
|
||||
:columns="columns"
|
||||
title="选择模型和角色"
|
||||
@change="onChange"
|
||||
@cancel="showPicker = false"
|
||||
@confirm="newChat"
|
||||
>
|
||||
<template #option="item">
|
||||
<div class="picker-option">
|
||||
<van-image
|
||||
v-if="item.icon"
|
||||
:src="item.icon"
|
||||
fit="cover"
|
||||
round
|
||||
/>
|
||||
<van-image v-if="item.icon" :src="item.icon" fit="cover" round />
|
||||
<span>{{ item.text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</van-picker>
|
||||
</van-popup>
|
||||
|
||||
<van-dialog v-model:show="showEditChat" title="修改对话标题" show-cancel-button class="dialog" @confirm="saveTitle">
|
||||
<van-field v-model="tmpChatTitle" label="" placeholder="请输入对话标题" class="field"/>
|
||||
<van-dialog
|
||||
v-model:show="showEditChat"
|
||||
title="修改对话标题"
|
||||
show-cancel-button
|
||||
class="dialog"
|
||||
@confirm="saveTitle"
|
||||
>
|
||||
<van-field v-model="tmpChatTitle" label="" placeholder="请输入对话标题" class="field" />
|
||||
</van-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {showConfirmDialog, showFailToast, showSuccessToast} from "vant";
|
||||
import {checkSession} from "@/store/cache";
|
||||
import {router} from "@/router";
|
||||
import {removeArrayItem, showLoginDialog} from "@/utils/libs";
|
||||
import { router } from '@/router'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { removeArrayItem, showLoginDialog } from '@/utils/libs'
|
||||
import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const title = ref("会话列表")
|
||||
const chatName = ref("")
|
||||
const title = ref('会话列表')
|
||||
const chatName = ref('')
|
||||
const chats = ref([])
|
||||
const allChats = ref([])
|
||||
const loading = ref(false)
|
||||
@@ -100,107 +97,118 @@ const showPicker = ref(false)
|
||||
const columns = ref([roles.value, models.value])
|
||||
const showEditChat = ref(false)
|
||||
const item = ref({})
|
||||
const tmpChatTitle = ref("")
|
||||
const tmpChatTitle = ref('')
|
||||
|
||||
checkSession().then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
// 加载角色列表
|
||||
httpGet(`/api/app/list/user`).then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// console.log(items[i])
|
||||
roles.value.push({
|
||||
text: items[i].name,
|
||||
value: items[i].id,
|
||||
icon: items[i].icon,
|
||||
helloMsg: items[i].hello_msg,
|
||||
model_id: items[i].model_id
|
||||
})
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
showFailToast("加载聊天角色失败")
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
// 加载角色列表
|
||||
httpGet(`/api/app/list/user`)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// console.log(items[i])
|
||||
roles.value.push({
|
||||
text: items[i].name,
|
||||
value: items[i].id,
|
||||
icon: items[i].icon,
|
||||
helloMsg: items[i].hello_msg,
|
||||
model_id: items[i].model_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showFailToast('加载聊天角色失败')
|
||||
})
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list?enable=1')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
models.value.push({ text: items[i].name, value: items[i].id })
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('加载模型失败: ' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list?enable=1').then(res => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
models.value.push({text: items[i].name, value: items[i].id})
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
showFailToast("加载模型失败: " + e.message)
|
||||
// 加载角色列表
|
||||
httpGet('/api/app/list/user')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// console.log(items[i])
|
||||
roles.value.push({
|
||||
text: items[i].name,
|
||||
value: items[i].id,
|
||||
icon: items[i].icon,
|
||||
helloMsg: items[i].hello_msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showFailToast('加载聊天角色失败')
|
||||
})
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
models.value.push({ text: items[i].name, value: items[i].id })
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('加载模型失败: ' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
|
||||
// 加载角色列表
|
||||
httpGet('/api/app/list/user').then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// console.log(items[i])
|
||||
roles.value.push({
|
||||
text: items[i].name,
|
||||
value: items[i].id,
|
||||
icon: items[i].icon,
|
||||
helloMsg: items[i].hello_msg
|
||||
})
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
showFailToast("加载聊天角色失败")
|
||||
})
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list').then(res => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
models.value.push({text: items[i].name, value: items[i].id})
|
||||
}
|
||||
}
|
||||
}).catch(e => {
|
||||
showFailToast("加载模型失败: " + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
const onLoad = () => {
|
||||
checkSession().then((user) => {
|
||||
httpGet("/api/chat/list?user_id=" + user.id).then((res) => {
|
||||
if (res.data) {
|
||||
chats.value = res.data;
|
||||
allChats.value = res.data;
|
||||
finished.value = true
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch(() => {
|
||||
error.value = true
|
||||
showFailToast("加载会话列表失败")
|
||||
checkSession()
|
||||
.then(() => {
|
||||
httpGet('/api/chat/list?user_id=' + loginUser.value.id)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
chats.value = res.data
|
||||
allChats.value = res.data
|
||||
finished.value = true
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
error.value = true
|
||||
showFailToast('加载会话列表失败')
|
||||
})
|
||||
})
|
||||
}).catch(() => {
|
||||
finished.value = true
|
||||
})
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
if (chatName.value === '') {
|
||||
chats.value = allChats.value
|
||||
return
|
||||
}
|
||||
const items = [];
|
||||
const items = []
|
||||
for (let i = 0; i < allChats.value.length; i++) {
|
||||
if (allChats.value[i].title.toLowerCase().indexOf(chatName.value.toLowerCase()) !== -1) {
|
||||
items.push(allChats.value[i]);
|
||||
items.push(allChats.value[i])
|
||||
}
|
||||
}
|
||||
chats.value = items;
|
||||
chats.value = items
|
||||
}
|
||||
|
||||
const clearAllChatHistory = () => {
|
||||
@@ -210,17 +218,21 @@ const clearAllChatHistory = () => {
|
||||
|
||||
showConfirmDialog({
|
||||
title: '操作提示',
|
||||
message: '确定要删除所有的会话记录吗?'
|
||||
}).then(() => {
|
||||
httpGet("/api/chat/clear").then(() => {
|
||||
showSuccessToast('所有聊天记录已清空')
|
||||
chats.value = [];
|
||||
}).catch(e => {
|
||||
showFailToast("操作失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
// on cancel
|
||||
message: '确定要删除所有的会话记录吗?',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet('/api/chat/clear')
|
||||
.then(() => {
|
||||
showSuccessToast('所有聊天记录已清空')
|
||||
chats.value = []
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('操作失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
// on cancel
|
||||
})
|
||||
}
|
||||
|
||||
const newChat = (item) => {
|
||||
@@ -229,7 +241,9 @@ const newChat = (item) => {
|
||||
}
|
||||
showPicker.value = false
|
||||
const options = item.selectedOptions
|
||||
router.push(`/mobile/chat/session?title=新对话&role_id=${options[0].value}&model_id=${options[1].value}`)
|
||||
router.push(
|
||||
`/mobile/chat/session?title=新对话&role_id=${options[0].value}&model_id=${options[1].value}`
|
||||
)
|
||||
}
|
||||
|
||||
const changeChat = (chat) => {
|
||||
@@ -242,40 +256,42 @@ const editChat = (row) => {
|
||||
tmpChatTitle.value = row.title
|
||||
}
|
||||
const saveTitle = () => {
|
||||
httpPost('/api/chat/update', {chat_id: item.value.chat_id, title: tmpChatTitle.value}).then(() => {
|
||||
showSuccessToast("操作成功!");
|
||||
item.value.title = tmpChatTitle.value;
|
||||
}).catch(e => {
|
||||
showFailToast("操作失败:" + e.message);
|
||||
})
|
||||
httpPost('/api/chat/update', { chat_id: item.value.chat_id, title: tmpChatTitle.value })
|
||||
.then(() => {
|
||||
showSuccessToast('操作成功!')
|
||||
item.value.title = tmpChatTitle.value
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('操作失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeChat = (item) => {
|
||||
httpGet('/api/chat/remove?chat_id=' + item.chat_id).then(() => {
|
||||
chats.value = removeArrayItem(chats.value, item, function (e1, e2) {
|
||||
return e1.id === e2.id
|
||||
httpGet('/api/chat/remove?chat_id=' + item.chat_id)
|
||||
.then(() => {
|
||||
chats.value = removeArrayItem(chats.value, item, function (e1, e2) {
|
||||
return e1.id === e2.id
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('操作失败:' + e.message)
|
||||
})
|
||||
}).catch(e => {
|
||||
showFailToast('操作失败:' + e.message);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const onChange = (item) => {
|
||||
const selectedValues = item.selectedOptions
|
||||
if (selectedValues[0].model_id) {
|
||||
for (let i = 0; i < columns.value[1].length; i++) {
|
||||
columns.value[1][i].disabled = columns.value[1][i].value !== selectedValues[0].model_id;
|
||||
columns.value[1][i].disabled = columns.value[1][i].value !== selectedValues[0].model_id
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < columns.value[1].length; i++) {
|
||||
columns.value[1][i].disabled = false;
|
||||
columns.value[1][i].disabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/mobile/chat-list.styl"
|
||||
</style>
|
||||
@import '../../assets/css/mobile/chat-list.styl'
|
||||
</style>
|
||||
|
||||
@@ -120,11 +120,13 @@
|
||||
<script setup>
|
||||
import ChatPrompt from '@/components/mobile/ChatPrompt.vue'
|
||||
import ChatReply from '@/components/mobile/ChatReply.vue'
|
||||
import { checkSession, getClientId } from '@/store/cache'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { getUserToken } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { processContent, randString, renderInputText, UUID } from '@/utils/libs'
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import Clipboard from 'clipboard'
|
||||
import hl from 'highlight.js'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
@@ -134,6 +136,7 @@ import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { showImagePreview, showNotify, showToast } from 'vant'
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const winHeight = ref(0)
|
||||
const navBarRef = ref(null)
|
||||
const bottomBarRef = ref(null)
|
||||
@@ -267,65 +270,10 @@ onMounted(() => {
|
||||
clipboard.on('error', () => {
|
||||
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
|
||||
})
|
||||
|
||||
store.addMessageHandler('chat', (data) => {
|
||||
if (data.channel !== 'chat' || data.clientId !== getClientId()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'error') {
|
||||
showMessageError(data.body)
|
||||
return
|
||||
}
|
||||
|
||||
if (isNewMsg.value) {
|
||||
if (!title.value) {
|
||||
title.value = previousText.value
|
||||
}
|
||||
lineBuffer.value = data.body
|
||||
isNewMsg.value = false
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
if (reply) {
|
||||
reply['content'] = lineBuffer.value
|
||||
}
|
||||
} else if (data.type === 'end') {
|
||||
// 消息接收完毕
|
||||
enableInput()
|
||||
lineBuffer.value = '' // 清空缓冲
|
||||
isNewMsg.value = true
|
||||
} else {
|
||||
lineBuffer.value += data.body
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
reply['orgContent'] = lineBuffer.value
|
||||
reply['content'] = md.render(processContent(lineBuffer.value))
|
||||
|
||||
nextTick(() => {
|
||||
hl.configure({ ignoreUnescapedHTML: true })
|
||||
const lines = document.querySelectorAll('.message-line')
|
||||
const blocks = lines[lines.length - 1].querySelectorAll('pre code')
|
||||
blocks.forEach((block) => {
|
||||
hl.highlightElement(block)
|
||||
})
|
||||
scrollListBox()
|
||||
|
||||
const items = document.querySelectorAll('.message-line')
|
||||
const imgs = items[items.length - 1].querySelectorAll('img')
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
if (!imgs[i].src) {
|
||||
continue
|
||||
}
|
||||
imgs[i].addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
showImagePreview([imgs[i].src])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
store.removeMessageHandler('chat')
|
||||
// Remove WebSocket handler cleanup
|
||||
})
|
||||
|
||||
const newChat = (item) => {
|
||||
@@ -360,7 +308,9 @@ const loadChatHistory = () => {
|
||||
type: 'reply',
|
||||
id: randString(32),
|
||||
icon: role.icon,
|
||||
content: role.hello_msg,
|
||||
content: {
|
||||
text: role.hello_msg,
|
||||
},
|
||||
orgContent: role.hello_msg,
|
||||
})
|
||||
return
|
||||
@@ -373,7 +323,7 @@ const loadChatHistory = () => {
|
||||
}
|
||||
|
||||
data[i].orgContent = data[i].content
|
||||
data[i].content = md.render(processContent(data[i].content))
|
||||
data[i].content.text = md.render(processContent(data[i].content.text))
|
||||
chatData.value.push(data[i])
|
||||
}
|
||||
|
||||
@@ -427,17 +377,106 @@ const scrollListBox = () => {
|
||||
.scrollTo(0, document.getElementById('message-list-box').scrollHeight + 46)
|
||||
}
|
||||
|
||||
// 发送 SSE 请求
|
||||
const sendSSERequest = async (message) => {
|
||||
try {
|
||||
await fetchEventSource('/api/chat/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: getUserToken(),
|
||||
},
|
||||
body: JSON.stringify(message),
|
||||
openWhenHidden: true,
|
||||
onopen(response) {
|
||||
if (response.ok && response.status === 200) {
|
||||
console.log('SSE connection opened')
|
||||
} else {
|
||||
throw new Error(`Failed to open SSE connection: ${response.status}`)
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
try {
|
||||
const data = JSON.parse(msg.data)
|
||||
if (data.type === 'error') {
|
||||
showMessageError(data.body)
|
||||
enableInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'end') {
|
||||
enableInput()
|
||||
lineBuffer.value = '' // 清空缓冲
|
||||
isNewMsg.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'text') {
|
||||
if (isNewMsg.value) {
|
||||
isNewMsg.value = false
|
||||
lineBuffer.value = data.body
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
if (reply) {
|
||||
reply['content']['text'] = lineBuffer.value
|
||||
}
|
||||
} else {
|
||||
lineBuffer.value += data.body
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
reply['orgContent'] = lineBuffer.value
|
||||
reply['content']['text'] = md.render(processContent(lineBuffer.value))
|
||||
|
||||
nextTick(() => {
|
||||
hl.configure({ ignoreUnescapedHTML: true })
|
||||
const lines = document.querySelectorAll('.message-line')
|
||||
const blocks = lines[lines.length - 1].querySelectorAll('pre code')
|
||||
blocks.forEach((block) => {
|
||||
hl.highlightElement(block)
|
||||
})
|
||||
scrollListBox()
|
||||
|
||||
const items = document.querySelectorAll('.message-line')
|
||||
const imgs = items[items.length - 1].querySelectorAll('img')
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
if (!imgs[i].src) {
|
||||
continue
|
||||
}
|
||||
imgs[i].addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
showImagePreview([imgs[i].src])
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error)
|
||||
enableInput()
|
||||
showMessageError('消息处理出错,请重试')
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
console.error('SSE Error:', err)
|
||||
enableInput()
|
||||
showMessageError('连接已断开,请重试')
|
||||
},
|
||||
onclose() {
|
||||
console.log('SSE connection closed')
|
||||
enableInput()
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error)
|
||||
enableInput()
|
||||
showMessageError('发送消息失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = () => {
|
||||
if (canSend.value === false) {
|
||||
showToast('AI 正在作答中,请稍后...')
|
||||
return
|
||||
}
|
||||
|
||||
if (store.socket.conn.readyState !== WebSocket.OPEN) {
|
||||
showToast('连接断开,正在重连...')
|
||||
return
|
||||
}
|
||||
|
||||
if (prompt.value.trim().length === 0) {
|
||||
showToast('请输入需要 AI 回答的问题')
|
||||
return false
|
||||
@@ -448,7 +487,7 @@ const sendMessage = () => {
|
||||
type: 'prompt',
|
||||
id: randString(32),
|
||||
icon: loginUser.value.avatar,
|
||||
content: renderInputText(prompt.value),
|
||||
content: { text: renderInputText(prompt.value) },
|
||||
created_at: new Date().getTime(),
|
||||
})
|
||||
// 添加空回复消息
|
||||
@@ -459,7 +498,9 @@ const sendMessage = () => {
|
||||
type: 'reply',
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
content: '',
|
||||
content: {
|
||||
text: '',
|
||||
},
|
||||
})
|
||||
|
||||
nextTick(() => {
|
||||
@@ -467,31 +508,23 @@ const sendMessage = () => {
|
||||
})
|
||||
|
||||
disableInput(false)
|
||||
store.socket.conn.send(
|
||||
JSON.stringify({
|
||||
channel: 'chat',
|
||||
type: 'text',
|
||||
body: {
|
||||
role_id: roleId.value,
|
||||
model_id: modelId.value,
|
||||
chat_id: chatId.value,
|
||||
content: prompt.value,
|
||||
stream: stream.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// 发送 SSE 请求
|
||||
sendSSERequest({
|
||||
user_id: loginUser.value.id,
|
||||
role_id: roleId.value,
|
||||
model_id: modelId.value,
|
||||
chat_id: chatId.value,
|
||||
prompt: prompt.value,
|
||||
stream: stream.value,
|
||||
})
|
||||
|
||||
previousText.value = prompt.value
|
||||
prompt.value = ''
|
||||
return true
|
||||
}
|
||||
|
||||
const stopGenerate = () => {
|
||||
showStopGenerate.value = false
|
||||
httpGet('/api/chat/stop?session_id=' + getClientId()).then(() => {
|
||||
enableInput()
|
||||
})
|
||||
}
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = () => {
|
||||
disableInput(false)
|
||||
const text = '重新生成上述问题的答案:' + previousText.value
|
||||
@@ -502,19 +535,16 @@ const reGenerate = () => {
|
||||
icon: loginUser.value.avatar,
|
||||
content: renderInputText(text),
|
||||
})
|
||||
store.socket.conn.send(
|
||||
JSON.stringify({
|
||||
channel: 'chat',
|
||||
type: 'text',
|
||||
body: {
|
||||
role_id: roleId.value,
|
||||
model_id: modelId.value,
|
||||
chat_id: chatId.value,
|
||||
content: previousText.value,
|
||||
stream: stream.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// 发送 SSE 请求
|
||||
sendSSERequest({
|
||||
user_id: loginUser.value.id,
|
||||
role_id: roleId.value,
|
||||
model_id: modelId.value,
|
||||
chat_id: chatId.value,
|
||||
prompt: previousText.value,
|
||||
stream: stream.value,
|
||||
})
|
||||
}
|
||||
|
||||
const showShare = ref(false)
|
||||
@@ -601,6 +631,6 @@ const onChange = (item) => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mobile/chat-session.styl"
|
||||
<style lang="stylus" scoped>
|
||||
@import "../../assets/css/mobile/chat-session.styl"
|
||||
</style>
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
<template>
|
||||
<van-config-provider :theme="theme">
|
||||
<div class="mobile-home">
|
||||
<router-view/>
|
||||
<router-view />
|
||||
|
||||
<van-tabbar route v-model="active">
|
||||
<van-tabbar-item to="/mobile/index" name="home" icon="home-o">首页</van-tabbar-item>
|
||||
<van-tabbar-item to="/mobile/chat" name="chat" icon="chat-o">对话</van-tabbar-item>
|
||||
<van-tabbar-item to="/mobile/image" name="image" icon="photo-o">绘图</van-tabbar-item>
|
||||
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的 </van-tabbar-item>
|
||||
</van-tabbar>
|
||||
|
||||
</div>
|
||||
</van-config-provider>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const active = ref('home')
|
||||
const store = useSharedStore()
|
||||
const theme = ref(store.theme)
|
||||
|
||||
watch(() => store.theme, (val) => {
|
||||
theme.value = val
|
||||
})
|
||||
|
||||
watch(
|
||||
() => store.theme,
|
||||
(val) => {
|
||||
theme.value = val
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '@/assets/iconfont/iconfont.css';
|
||||
@import '../../assets/iconfont/iconfont.css';
|
||||
.mobile-home {
|
||||
.container {
|
||||
.van-nav-bar {
|
||||
@@ -53,4 +52,4 @@ watch(() => store.theme, (val) => {
|
||||
position fixed
|
||||
width 100%
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -93,7 +93,7 @@ import { showNotify } from 'vant'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE)
|
||||
const title = ref(import.meta.env.VITE_TITLE)
|
||||
const router = useRouter()
|
||||
const isLogin = ref(false)
|
||||
const apps = ref([])
|
||||
|
||||
@@ -30,28 +30,36 @@
|
||||
<div class="opt" v-if="isLogin">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="8">
|
||||
<van-button round block @click="showPasswordDialog = true" size="small">修改密码</van-button>
|
||||
<van-button round block @click="showPasswordDialog = true" size="small"
|
||||
>修改密码</van-button
|
||||
>
|
||||
</van-col>
|
||||
<van-col :span="8">
|
||||
<van-button round block @click="logout" size="small">退出登录</van-button>
|
||||
</van-col>
|
||||
|
||||
<van-col :span="8">
|
||||
<van-button round block @click="showSettings = true" icon="setting" size="small">设置</van-button>
|
||||
<van-button round block @click="showSettings = true" icon="setting" size="small"
|
||||
>设置</van-button
|
||||
>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
|
||||
<div class="product-list">
|
||||
<h3>充值套餐</h3>
|
||||
<div class="product-list" v-if="menuList['/member']">
|
||||
<h3 class="py-3">充值套餐</h3>
|
||||
<div class="item" v-for="item in products" :key="item.id">
|
||||
<div class="title">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<div class="pay-btn">
|
||||
<div v-for="payWay in payWays" @click="pay(item, payWay)" :key="payWay">
|
||||
<span>
|
||||
<van-button type="primary" size="small" v-if="payWay.pay_type === 'alipay'"> <i class="iconfont icon-alipay"></i> 支付宝 </van-button>
|
||||
<van-button type="success" size="small" v-if="payWay.pay_type === 'wxpay'"> <i class="iconfont icon-wechat-pay"></i> 微信支付 </van-button>
|
||||
<van-button type="primary" size="small" v-if="payWay.pay_type === 'alipay'">
|
||||
<i class="iconfont icon-alipay"></i> 支付宝
|
||||
</van-button>
|
||||
<van-button type="success" size="small" v-if="payWay.pay_type === 'wxpay'">
|
||||
<i class="iconfont icon-wechat-pay"></i> 微信支付
|
||||
</van-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,7 +108,10 @@
|
||||
<van-cell-group inset>
|
||||
<van-field name="switch" label="暗黑主题">
|
||||
<template #input>
|
||||
<van-switch v-model="dark" @change="(val) => store.setTheme(val ? 'dark' : 'light')" />
|
||||
<van-switch
|
||||
v-model="dark"
|
||||
@change="(val) => store.setTheme(val ? 'dark' : 'light')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
@@ -130,163 +141,151 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { showFailToast, showLoadingToast, showNotify, showSuccessToast } from "vant";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { dateFormat, showLoginDialog } from "@/utils/libs";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { useRouter } from "vue-router";
|
||||
import { removeUserToken } from "@/store/session";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { removeUserToken } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { dateFormat, showLoginDialog } from '@/utils/libs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { showFailToast, showLoadingToast, showNotify, showSuccessToast } from 'vant'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const form = ref({
|
||||
username: "GeekMaster",
|
||||
nickname: "极客学长@001",
|
||||
mobile: "1300000000",
|
||||
avatar: "",
|
||||
username: 'GeekMaster',
|
||||
nickname: '极客学长@001',
|
||||
mobile: '1300000000',
|
||||
avatar: '',
|
||||
power: 0,
|
||||
});
|
||||
})
|
||||
const fileList = ref([
|
||||
{
|
||||
url: "/images/user-info.png",
|
||||
message: "上传中...",
|
||||
url: '/images/user-info.png',
|
||||
message: '上传中...',
|
||||
},
|
||||
]);
|
||||
])
|
||||
|
||||
const products = ref([]);
|
||||
const vipMonthPower = ref(0);
|
||||
const payWays = ref({});
|
||||
const router = useRouter();
|
||||
const userId = ref(0);
|
||||
const isLogin = ref(false);
|
||||
const showSettings = ref(false);
|
||||
const store = useSharedStore();
|
||||
const stream = ref(store.chatStream);
|
||||
const dark = ref(store.theme === "dark");
|
||||
const products = ref([])
|
||||
const vipMonthPower = ref(0)
|
||||
const payWays = ref({})
|
||||
const router = useRouter()
|
||||
const userId = ref(0)
|
||||
const isLogin = ref(false)
|
||||
const showSettings = ref(false)
|
||||
const store = useSharedStore()
|
||||
const stream = ref(store.chatStream)
|
||||
const dark = ref(store.theme === 'dark')
|
||||
const menuList = ref({})
|
||||
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
userId.value = user.id;
|
||||
isLogin.value = true;
|
||||
httpGet("/api/user/profile")
|
||||
userId.value = user.id
|
||||
isLogin.value = true
|
||||
httpGet('/api/user/profile')
|
||||
.then((res) => {
|
||||
form.value = res.data;
|
||||
fileList.value[0].url = form.value.avatar;
|
||||
form.value = res.data
|
||||
fileList.value[0].url = form.value.avatar
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e.message);
|
||||
showFailToast("获取用户信息失败");
|
||||
});
|
||||
console.log(e.message)
|
||||
showFailToast('获取用户信息失败')
|
||||
})
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {})
|
||||
|
||||
// 获取产品列表
|
||||
httpGet("/api/product/list")
|
||||
httpGet('/api/product/list')
|
||||
.then((res) => {
|
||||
products.value = res.data;
|
||||
products.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("获取产品套餐失败:" + e.message);
|
||||
});
|
||||
showFailToast('获取产品套餐失败:' + e.message)
|
||||
})
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
vipMonthPower.value = res.data["vip_month_power"];
|
||||
vipMonthPower.value = res.data['vip_month_power']
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("获取系统配置失败:" + e.message);
|
||||
});
|
||||
showFailToast('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/payment/payWays")
|
||||
httpGet('/api/payment/payWays')
|
||||
.then((res) => {
|
||||
payWays.value = res.data;
|
||||
payWays.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取支付方式失败:" + e.message);
|
||||
});
|
||||
});
|
||||
ElMessage.error('获取支付方式失败:' + e.message)
|
||||
})
|
||||
|
||||
// const afterRead = (file) => {
|
||||
// file.status = 'uploading';
|
||||
// file.message = '上传中...';
|
||||
// // 压缩图片并上传
|
||||
// new Compressor(file.file, {
|
||||
// quality: 0.6,
|
||||
// success(result) {
|
||||
// const formData = new FormData();
|
||||
// formData.append('file', result, result.name);
|
||||
// // 执行上传操作
|
||||
// httpPost('/api/upload', formData).then((res) => {
|
||||
// form.value.avatar = res.data.url
|
||||
// file.status = 'success'
|
||||
// httpPost('/api/user/profile/update', form.value).then(() => {
|
||||
// showSuccessToast('上传成功')
|
||||
// }).catch(() => {
|
||||
// showFailToast('上传失败')
|
||||
// })
|
||||
// }).catch((e) => {
|
||||
// showNotify({type: 'danger', message: '上传失败:' + e.message})
|
||||
// })
|
||||
// },
|
||||
// error(err) {
|
||||
// console.log(err.message);
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
getMenuList()
|
||||
})
|
||||
|
||||
const showPasswordDialog = ref(false);
|
||||
// 获取菜单列表
|
||||
const getMenuList = () => {
|
||||
httpGet('/api/menu/list')
|
||||
.then((res) => {
|
||||
res.data.forEach((item) => {
|
||||
menuList.value[item.url] = item
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('获取菜单列表失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showPasswordDialog = ref(false)
|
||||
const pass = ref({
|
||||
old: "",
|
||||
new: "",
|
||||
renew: "",
|
||||
});
|
||||
old: '',
|
||||
new: '',
|
||||
renew: '',
|
||||
})
|
||||
|
||||
const beforeClose = (action) => {
|
||||
new Promise((resolve) => {
|
||||
resolve(action === "confirm");
|
||||
});
|
||||
};
|
||||
resolve(action === 'confirm')
|
||||
})
|
||||
}
|
||||
|
||||
// 提交修改密码
|
||||
const updatePass = () => {
|
||||
if (pass.value.old === "") {
|
||||
return showNotify({ type: "danger", message: "请输入旧密码" });
|
||||
if (pass.value.old === '') {
|
||||
return showNotify({ type: 'danger', message: '请输入旧密码' })
|
||||
}
|
||||
if (!pass.value.new || pass.value.new.length < 8) {
|
||||
return showNotify({ type: "danger", message: "密码的长度为8-16个字符" });
|
||||
return showNotify({ type: 'danger', message: '密码的长度为8-16个字符' })
|
||||
}
|
||||
if (pass.value.renew !== pass.value.new) {
|
||||
return showNotify({ type: "danger", message: "两次输入密码不一致" });
|
||||
return showNotify({ type: 'danger', message: '两次输入密码不一致' })
|
||||
}
|
||||
httpPost("/api/user/password", {
|
||||
httpPost('/api/user/password', {
|
||||
old_pass: pass.value.old,
|
||||
password: pass.value.new,
|
||||
repass: pass.value.renew,
|
||||
})
|
||||
.then(() => {
|
||||
showSuccessToast("更新成功!");
|
||||
showPasswordDialog.value = false;
|
||||
showSuccessToast('更新成功!')
|
||||
showPasswordDialog.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("更新失败," + e.message);
|
||||
showPasswordDialog.value = false;
|
||||
});
|
||||
};
|
||||
showFailToast('更新失败,' + e.message)
|
||||
showPasswordDialog.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const pay = (product, payWay) => {
|
||||
if (!isLogin.value) {
|
||||
return showLoginDialog(router);
|
||||
return showLoginDialog(router)
|
||||
}
|
||||
|
||||
showLoadingToast({
|
||||
message: "正在创建订单",
|
||||
message: '正在创建订单',
|
||||
forbidClick: true,
|
||||
});
|
||||
let host = process.env.VUE_APP_API_HOST;
|
||||
if (host === "") {
|
||||
host = `${location.protocol}//${location.host}`;
|
||||
})
|
||||
let host = process.env.VUE_APP_API_HOST
|
||||
if (host === '') {
|
||||
host = `${location.protocol}//${location.host}`
|
||||
}
|
||||
httpPost(`${process.env.VUE_APP_API_HOST}/api/payment/doPay`, {
|
||||
product_id: product.id,
|
||||
@@ -294,27 +293,27 @@ const pay = (product, payWay) => {
|
||||
pay_type: payWay.pay_type,
|
||||
user_id: userId.value,
|
||||
host: host,
|
||||
device: "wechat",
|
||||
device: 'wechat',
|
||||
})
|
||||
.then((res) => {
|
||||
location.href = res.data;
|
||||
location.href = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("生成支付订单失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showFailToast('生成支付订单失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const logout = function () {
|
||||
httpGet("/api/user/logout")
|
||||
httpGet('/api/user/logout')
|
||||
.then(() => {
|
||||
removeUserToken();
|
||||
store.setIsLogin(false);
|
||||
router.push("/");
|
||||
removeUserToken()
|
||||
store.setIsLogin(false)
|
||||
router.push('/')
|
||||
})
|
||||
.catch(() => {
|
||||
showFailToast("注销失败!");
|
||||
});
|
||||
};
|
||||
showFailToast('注销失败!')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
@@ -3,29 +3,65 @@
|
||||
<van-form @submit="generate">
|
||||
<van-cell-group inset>
|
||||
<div>
|
||||
<van-field v-model="selectedModel" is-link label="生图模型" placeholder="选择生图模型" @click="showModelPicker = true" />
|
||||
<van-field
|
||||
v-model="selectedModel"
|
||||
is-link
|
||||
label="生图模型"
|
||||
placeholder="选择生图模型"
|
||||
@click="showModelPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showModelPicker" position="bottom" teleport="#app">
|
||||
<van-picker :columns="models" @cancel="showModelPicker = false" @confirm="modelConfirm" />
|
||||
<van-picker
|
||||
:columns="models"
|
||||
@cancel="showModelPicker = false"
|
||||
@confirm="modelConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
<div>
|
||||
<van-field v-model="quality" is-link label="图片质量" placeholder="选择图片质量" @click="showQualityPicker = true" />
|
||||
<van-field
|
||||
v-model="quality"
|
||||
is-link
|
||||
label="图片质量"
|
||||
placeholder="选择图片质量"
|
||||
@click="showQualityPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showQualityPicker" position="bottom" teleport="#app">
|
||||
<van-picker :columns="qualities" @cancel="showQualityPicker = false" @confirm="qualityConfirm" />
|
||||
<van-picker
|
||||
:columns="qualities"
|
||||
@cancel="showQualityPicker = false"
|
||||
@confirm="qualityConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<van-field v-model="size" is-link label="图片尺寸" placeholder="选择图片尺寸" @click="showSizePicker = true" />
|
||||
<van-field
|
||||
v-model="size"
|
||||
is-link
|
||||
label="图片尺寸"
|
||||
placeholder="选择图片尺寸"
|
||||
@click="showSizePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSizePicker" position="bottom" teleport="#app">
|
||||
<van-picker :columns="sizes" @cancel="showSizePicker = false" @confirm="sizeConfirm" />
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<van-field v-model="style" is-link label="图片样式" placeholder="选择图片样式" @click="showStylePicker = true" />
|
||||
<van-field
|
||||
v-model="style"
|
||||
is-link
|
||||
label="图片样式"
|
||||
placeholder="选择图片样式"
|
||||
@click="showStylePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showStylePicker" position="bottom" teleport="#app">
|
||||
<van-picker :columns="styles" @cancel="showStylePicker = false" @confirm="styleConfirm" />
|
||||
<van-picker
|
||||
:columns="styles"
|
||||
@cancel="showStylePicker = false"
|
||||
@confirm="styleConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +97,14 @@
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle v-model:current-rate="item.progress" :rate="item.progress" :speed="100" :text="item.progress + '%'" :stroke-width="60" size="90px" />
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,11 +140,19 @@
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small">删除</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image :src="item['img_url']" :class="item['can_opt'] ? '' : 'upscale'" lazy-load @click="imageView(item)" fit="cover">
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
@@ -109,7 +160,12 @@
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle />
|
||||
<el-button type="warning" v-if="item.publish" @click="publishImage($event, item, false)" circle>
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
@@ -124,324 +180,343 @@
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
<button style="display: none" class="copy-prompt-dall" :data-clipboard-text="prompt" id="copy-btn-dall">复制</button>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-dall"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-dall"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { Delete } from "@element-plus/icons-vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import Clipboard from "clipboard";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSessionId } from "@/store/session";
|
||||
import { showConfirmDialog, showDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast } from "vant";
|
||||
import { showLoginDialog } from "@/utils/libs";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { showLoginDialog } from '@/utils/libs'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import {
|
||||
showConfirmDialog,
|
||||
showDialog,
|
||||
showFailToast,
|
||||
showImagePreview,
|
||||
showNotify,
|
||||
showSuccessToast,
|
||||
showToast,
|
||||
} from 'vant'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40);
|
||||
const mjBoxHeight = ref(window.innerHeight - 150);
|
||||
const isLogin = ref(false);
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const isLogin = ref(false)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40;
|
||||
mjBoxHeight.value = window.innerHeight - 150;
|
||||
};
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
|
||||
const qualities = [
|
||||
{ text: "标准", value: "standard" },
|
||||
{ text: "高清", value: "hd" },
|
||||
];
|
||||
{ text: '标准', value: 'standard' },
|
||||
{ text: '高清', value: 'hd' },
|
||||
]
|
||||
const fluxSizes = [
|
||||
{ text: "1024x1024", value: "1024x1024" },
|
||||
{ text: "1024x768", value: "1024x768" },
|
||||
{ text: "768x1024", value: "768x1024" },
|
||||
{ text: "1280x960", value: "1280x960" },
|
||||
{ text: "960x1280", value: "960x1280" },
|
||||
{ text: "1366x768", value: "1366x768" },
|
||||
{ text: "768x1366", value: "768x1366" },
|
||||
];
|
||||
{ text: '1024x1024', value: '1024x1024' },
|
||||
{ text: '1024x768', value: '1024x768' },
|
||||
{ text: '768x1024', value: '768x1024' },
|
||||
{ text: '1280x960', value: '1280x960' },
|
||||
{ text: '960x1280', value: '960x1280' },
|
||||
{ text: '1366x768', value: '1366x768' },
|
||||
{ text: '768x1366', value: '768x1366' },
|
||||
]
|
||||
const dalleSizes = [
|
||||
{ text: "1024x1024", value: "1024x1024" },
|
||||
{ text: "1792x1024", value: "1792x1024" },
|
||||
{ text: "1024x1792", value: "1024x1792" },
|
||||
];
|
||||
{ text: '1024x1024', value: '1024x1024' },
|
||||
{ text: '1792x1024', value: '1792x1024' },
|
||||
{ text: '1024x1792', value: '1024x1792' },
|
||||
]
|
||||
|
||||
let sizes = dalleSizes;
|
||||
let sizes = dalleSizes
|
||||
const styles = [
|
||||
{ text: "生动", value: "vivid" },
|
||||
{ text: "自然", value: "natural" },
|
||||
];
|
||||
{ text: '生动', value: 'vivid' },
|
||||
{ text: '自然', value: 'natural' },
|
||||
]
|
||||
const params = ref({
|
||||
quality: qualities[0].value,
|
||||
size: sizes[0].value,
|
||||
style: styles[0].value,
|
||||
prompt: "",
|
||||
});
|
||||
const quality = ref(qualities[0].text);
|
||||
const size = ref(sizes[0].text);
|
||||
const style = ref(styles[0].text);
|
||||
prompt: '',
|
||||
})
|
||||
const quality = ref(qualities[0].text)
|
||||
const size = ref(sizes[0].text)
|
||||
const style = ref(styles[0].text)
|
||||
|
||||
const showQualityPicker = ref(false);
|
||||
const showStylePicker = ref(false);
|
||||
const showSizePicker = ref(false);
|
||||
const showModelPicker = ref(false);
|
||||
const showQualityPicker = ref(false)
|
||||
const showStylePicker = ref(false)
|
||||
const showSizePicker = ref(false)
|
||||
const showModelPicker = ref(false)
|
||||
|
||||
const runningJobs = ref([]);
|
||||
const finishedJobs = ref([]);
|
||||
const allowPulling = ref(true); // 是否允许轮询
|
||||
const tastPullHandler = ref(null);
|
||||
const router = useRouter();
|
||||
const power = ref(0);
|
||||
const dallPower = ref(0); // 画一张 DALL 图片消耗算力
|
||||
const runningJobs = ref([])
|
||||
const finishedJobs = ref([])
|
||||
const allowPulling = ref(true) // 是否允许轮询
|
||||
const tastPullHandler = ref(null)
|
||||
const router = useRouter()
|
||||
const power = ref(0)
|
||||
const dallPower = ref(0) // 画一张 DALL 图片消耗算力
|
||||
|
||||
const userId = ref(0);
|
||||
const store = useSharedStore();
|
||||
const clipboard = ref(null);
|
||||
const prompt = ref("");
|
||||
const models = ref([]);
|
||||
const selectedModel = ref(null);
|
||||
const userId = ref(0)
|
||||
const store = useSharedStore()
|
||||
const clipboard = ref(null)
|
||||
const prompt = ref('')
|
||||
const models = ref([])
|
||||
const selectedModel = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
initData();
|
||||
clipboard.value = new Clipboard(".copy-prompt-dall");
|
||||
clipboard.value.on("success", () => {
|
||||
showNotify({ type: "success", message: "复制成功", duration: 1000 });
|
||||
});
|
||||
clipboard.value.on("error", () => {
|
||||
showNotify({ type: "danger", message: "复制失败", duration: 2000 });
|
||||
});
|
||||
initData()
|
||||
clipboard.value = new Clipboard('.copy-prompt-dall')
|
||||
clipboard.value.on('success', () => {
|
||||
showNotify({ type: 'success', message: '复制成功', duration: 1000 })
|
||||
})
|
||||
clipboard.value.on('error', () => {
|
||||
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
|
||||
})
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
dallPower.value = res.data.dall_power;
|
||||
dallPower.value = res.data.dall_power
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: "danger", message: "获取系统配置失败:" + e.message });
|
||||
});
|
||||
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
|
||||
})
|
||||
|
||||
// 获取模型列表
|
||||
httpGet("/api/dall/models")
|
||||
httpGet('/api/dall/models')
|
||||
.then((res) => {
|
||||
for (let i = 0; i < res.data.length; i++) {
|
||||
models.value.push({ text: res.data[i].name, value: res.data[i].id, name: res.data[i].value });
|
||||
models.value.push({
|
||||
text: res.data[i].name,
|
||||
value: res.data[i].id,
|
||||
name: res.data[i].value,
|
||||
})
|
||||
}
|
||||
selectedModel.value = models.value[0]?.text;
|
||||
params.value.model_id = models.value[0]?.value;
|
||||
selectedModel.value = models.value[0]?.text
|
||||
params.value.model_id = models.value[0]?.value
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取模型列表失败:" + e.message);
|
||||
});
|
||||
});
|
||||
showMessageError('获取模型列表失败:' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
clipboard.value.destroy()
|
||||
if (tastPullHandler.value) {
|
||||
clearInterval(tastPullHandler.value);
|
||||
clearInterval(tastPullHandler.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
power.value = user["power"];
|
||||
isLogin.value = true;
|
||||
fetchRunningJobs();
|
||||
fetchFinishJobs(1);
|
||||
power.value = user['power']
|
||||
isLogin.value = true
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
|
||||
tastPullHandler.value = setInterval(() => {
|
||||
if (allowPulling.value) {
|
||||
fetchRunningJobs();
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/dall/jobs?finish=0`)
|
||||
.then((res) => {
|
||||
if (runningJobs.value.length !== res.data.items.length) {
|
||||
fetchFinishJobs(1);
|
||||
fetchFinishJobs(1)
|
||||
}
|
||||
if (res.data.items.length === 0) {
|
||||
allowPulling.value = false;
|
||||
allowPulling.value = false
|
||||
}
|
||||
runningJobs.value = res.data.items;
|
||||
runningJobs.value = res.data.items
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
|
||||
});
|
||||
};
|
||||
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
const error = ref(false);
|
||||
const page = ref(0);
|
||||
const pageSize = ref(10);
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref(false)
|
||||
const page = ref(0)
|
||||
const pageSize = ref(10)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = (page) => {
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
httpGet(`/api/dall/jobs?finish=1&page=${page}&page_size=${pageSize.value}`)
|
||||
.then((res) => {
|
||||
const jobs = res.data.items;
|
||||
const jobs = res.data.items
|
||||
if (jobs.length < pageSize.value) {
|
||||
finished.value = true;
|
||||
finished.value = true
|
||||
}
|
||||
const _jobs = [];
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
|
||||
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
|
||||
}
|
||||
_jobs.push(jobs[i]);
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
if (page === 1) {
|
||||
finishedJobs.value = _jobs;
|
||||
finishedJobs.value = _jobs
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(_jobs);
|
||||
finishedJobs.value = finishedJobs.value.concat(_jobs)
|
||||
}
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
page.value += 1;
|
||||
fetchFinishJobs(page.value);
|
||||
};
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
}
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null);
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (!isLogin.value) {
|
||||
return showLoginDialog(router);
|
||||
return showLoginDialog(router)
|
||||
}
|
||||
|
||||
if (params.value.prompt === "") {
|
||||
promptRef.value.focus();
|
||||
return showToast("请输入绘画提示词!");
|
||||
if (params.value.prompt === '') {
|
||||
promptRef.value.focus()
|
||||
return showToast('请输入绘画提示词!')
|
||||
}
|
||||
|
||||
if (!params.value.seed) {
|
||||
params.value.seed = -1;
|
||||
params.value.seed = -1
|
||||
}
|
||||
params.value.session_id = getSessionId();
|
||||
httpPost("/api/dall/image", params.value)
|
||||
params.value.session_id = getSessionId()
|
||||
httpPost('/api/dall/image', params.value)
|
||||
.then(() => {
|
||||
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...");
|
||||
power.value -= dallPower.value;
|
||||
allowPulling.value = true;
|
||||
showSuccessToast('绘画任务推送成功,请耐心等待任务执行...')
|
||||
power.value -= dallPower.value
|
||||
allowPulling.value = true
|
||||
runningJobs.value.push({
|
||||
progress: 0,
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("任务推送失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showFailToast('任务推送失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showPrompt = (item) => {
|
||||
prompt.value = item.prompt;
|
||||
prompt.value = item.prompt
|
||||
showConfirmDialog({
|
||||
title: "绘画提示词",
|
||||
title: '绘画提示词',
|
||||
message: item.prompt,
|
||||
confirmButtonText: "复制",
|
||||
cancelButtonText: "关闭",
|
||||
confirmButtonText: '复制',
|
||||
cancelButtonText: '关闭',
|
||||
})
|
||||
.then(() => {
|
||||
document.querySelector("#copy-btn-dall").click();
|
||||
document.querySelector('#copy-btn-dall').click()
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const showErrMsg = (item) => {
|
||||
showDialog({
|
||||
title: "错误详情",
|
||||
message: item["err_msg"],
|
||||
title: '错误详情',
|
||||
message: item['err_msg'],
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (event, item) => {
|
||||
event.stopPropagation();
|
||||
event.stopPropagation()
|
||||
showConfirmDialog({
|
||||
title: "标题",
|
||||
message: "此操作将会删除任务和图片,继续操作码?",
|
||||
title: '标题',
|
||||
message: '此操作将会删除任务和图片,继续操作码?',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/dall/remove", { id: item.id, user_id: item.user_id })
|
||||
httpGet('/api/dall/remove', { id: item.id, user_id: item.user_id })
|
||||
.then(() => {
|
||||
showSuccessToast("任务删除成功");
|
||||
fetchFinishJobs(1);
|
||||
showSuccessToast('任务删除成功')
|
||||
fetchFinishJobs(1)
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("任务删除失败:" + e.message);
|
||||
});
|
||||
showFailToast('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("您取消了操作");
|
||||
});
|
||||
};
|
||||
showToast('您取消了操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (event, item, action) => {
|
||||
event.stopPropagation();
|
||||
let text = "图片发布";
|
||||
event.stopPropagation()
|
||||
let text = '图片发布'
|
||||
if (action === false) {
|
||||
text = "取消发布";
|
||||
text = '取消发布'
|
||||
}
|
||||
httpGet("/api/dall/publish", { id: item.id, action: action, user_id: item.user_id })
|
||||
httpGet('/api/dall/publish', { id: item.id, action: action, user_id: item.user_id })
|
||||
.then(() => {
|
||||
showSuccessToast(text + "成功");
|
||||
item.publish = action;
|
||||
showSuccessToast(text + '成功')
|
||||
item.publish = action
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast(text + "失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showFailToast(text + '失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const imageView = (item) => {
|
||||
showImagePreview([item["img_url"]]);
|
||||
};
|
||||
showImagePreview([item['img_url']])
|
||||
}
|
||||
|
||||
const qualityConfirm = (item) => {
|
||||
params.value.quality = item.selectedOptions[0].value;
|
||||
quality.value = item.selectedOptions[0].text;
|
||||
showQualityPicker.value = false;
|
||||
};
|
||||
params.value.quality = item.selectedOptions[0].value
|
||||
quality.value = item.selectedOptions[0].text
|
||||
showQualityPicker.value = false
|
||||
}
|
||||
|
||||
const styleConfirm = (item) => {
|
||||
params.value.style = item.selectedOptions[0].value;
|
||||
style.value = item.selectedOptions[0].text;
|
||||
showStylePicker.value = false;
|
||||
};
|
||||
params.value.style = item.selectedOptions[0].value
|
||||
style.value = item.selectedOptions[0].text
|
||||
showStylePicker.value = false
|
||||
}
|
||||
|
||||
const sizeConfirm = (item) => {
|
||||
params.value.size = item.selectedOptions[0].value;
|
||||
size.value = item.selectedOptions[0].text;
|
||||
showSizePicker.value = false;
|
||||
};
|
||||
params.value.size = item.selectedOptions[0].value
|
||||
size.value = item.selectedOptions[0].text
|
||||
showSizePicker.value = false
|
||||
}
|
||||
|
||||
const modelConfirm = (item) => {
|
||||
params.value.model_id = item.selectedOptions[0].value;
|
||||
selectedModel.value = item.selectedOptions[0].text;
|
||||
showModelPicker.value = false;
|
||||
if (item.selectedOptions[0].name.startsWith("dall")) {
|
||||
sizes = dalleSizes;
|
||||
params.value.model_id = item.selectedOptions[0].value
|
||||
selectedModel.value = item.selectedOptions[0].text
|
||||
showModelPicker.value = false
|
||||
if (item.selectedOptions[0].name.startsWith('dall')) {
|
||||
sizes = dalleSizes
|
||||
} else {
|
||||
sizes = fluxSizes;
|
||||
sizes = fluxSizes
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mobile/image-sd.styl"
|
||||
@import '../../../assets/css/mobile/image-sd.styl'
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="4" v-for="item in rates" :key="item.value">
|
||||
<div :class="item.value === params.rate ? 'rate active' : 'rate'" @click="changeRate(item)">
|
||||
<div
|
||||
:class="item.value === params.rate ? 'rate active' : 'rate'"
|
||||
@click="changeRate(item)"
|
||||
>
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
@@ -18,7 +21,10 @@
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="8" v-for="item in models" :key="item.value">
|
||||
<div :class="item.value === params.model ? 'model active' : 'model'" @click="changeModel(item)">
|
||||
<div
|
||||
:class="item.value === params.model ? 'model active' : 'model'"
|
||||
@click="changeModel(item)"
|
||||
>
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
@@ -32,7 +38,12 @@
|
||||
<div class="text-line">
|
||||
<van-field label="创意度">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.chaos" :max="100" :step="1" @update:model-value="showToast('当前值:' + params.chaos)" />
|
||||
<van-slider
|
||||
v-model.number="params.chaos"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.chaos)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
@@ -40,7 +51,12 @@
|
||||
<div class="text-line">
|
||||
<van-field label="风格化">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.stylize" :max="1000" :step="1" @update:model-value="showToast('当前值:' + params.stylize)" />
|
||||
<van-slider
|
||||
v-model.number="params.stylize"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.stylize)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
@@ -85,14 +101,27 @@
|
||||
<div class="text-line">
|
||||
<van-field label="垫图权重">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.iw" :max="1" :step="0.01" @update:model-value="showToast('当前值:' + params.iw)" />
|
||||
<van-slider
|
||||
v-model.number="params.iw"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@update:model-value="showToast('当前值:' + params.iw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="tip-text">提示:只有于 niji6 和 v6 模型支持一致性功能,如果选择其他模型此功能将会生成失败。</div>
|
||||
<div class="tip-text">
|
||||
提示:只有于 niji6 和 v6 模型支持一致性功能,如果选择其他模型此功能将会生成失败。
|
||||
</div>
|
||||
<van-cell-group>
|
||||
<van-field v-model="params.cref" center clearable label="角色一致性" placeholder="请输入图片URL或者上传图片">
|
||||
<van-field
|
||||
v-model="params.cref"
|
||||
center
|
||||
clearable
|
||||
label="角色一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
@@ -102,7 +131,13 @@
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group>
|
||||
<van-field v-model="params.sref" center clearable label="风格一致性" placeholder="请输入图片URL或者上传图片">
|
||||
<van-field
|
||||
v-model="params.sref"
|
||||
center
|
||||
clearable
|
||||
label="风格一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
@@ -114,13 +149,20 @@
|
||||
<div class="text-line">
|
||||
<van-field label="一致性权重">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.cw" :max="100" :step="1" @update:model-value="showToast('当前值:' + params.cw)" />
|
||||
<van-slider
|
||||
v-model.number="params.cw"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.cw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="融图" name="blend">
|
||||
<div class="tip-text">请上传两张以上的图片,最多不超过五张,超过五张图片请使用图生图功能。</div>
|
||||
<div class="tip-text">
|
||||
请上传两张以上的图片,最多不超过五张,超过五张图片请使用图生图功能。
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
@@ -137,13 +179,24 @@
|
||||
<div class="text-line">
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field v-model="params.neg_prompt" rows="3" maxlength="2000" autosize type="textarea" placeholder="不想出现在图片上的元素(例如:树,建筑)" />
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
</div>
|
||||
|
||||
<div class="text-line pt-6">
|
||||
<el-tag>绘图消耗{{ mjPower }}算力,U/V 操作消耗{{ mjActionPower }}算力,当前算力:{{ power }}</el-tag>
|
||||
<el-tag
|
||||
>绘图消耗{{ mjPower }}算力,U/V 操作消耗{{ mjActionPower }}算力,当前算力:{{
|
||||
power
|
||||
}}</el-tag
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
@@ -164,7 +217,14 @@
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle v-model:current-rate="item.progress" :rate="item.progress" :speed="100" :text="item.progress + '%'" :stroke-width="60" size="90px" />
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -204,7 +264,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image :src="item['thumb_url']" :class="item['can_opt'] ? '' : 'upscale'" lazy-load @click="imageView(item)" fit="cover">
|
||||
<van-image
|
||||
:src="item['thumb_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
@@ -220,16 +286,29 @@
|
||||
<van-grid-item><a @click="upscale(2, item)" class="opt-btn">U2</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(3, item)" class="opt-btn">U3</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(4, item)" class="opt-btn">U4</a></van-grid-item>
|
||||
<van-grid-item><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item>
|
||||
<van-grid-item><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item>
|
||||
<van-grid-item><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item>
|
||||
<van-grid-item><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item>
|
||||
<van-grid-item
|
||||
><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item
|
||||
>
|
||||
</van-grid>
|
||||
</div>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(item)" circle />
|
||||
<el-button type="warning" v-if="item.publish" @click="publishImage(item, false)" circle>
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage(item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(item, true)" circle>
|
||||
@@ -245,44 +324,54 @@
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">复制</button>
|
||||
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { showConfirmDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast, showDialog } from "vant";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import Compressor from "compressorjs";
|
||||
import { getSessionId } from "@/store/session";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Delete } from "@element-plus/icons-vue";
|
||||
import { showLoginDialog } from "@/utils/libs";
|
||||
import Clipboard from "clipboard";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { showLoginDialog } from '@/utils/libs'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import Compressor from 'compressorjs'
|
||||
import {
|
||||
showConfirmDialog,
|
||||
showDialog,
|
||||
showFailToast,
|
||||
showImagePreview,
|
||||
showNotify,
|
||||
showSuccessToast,
|
||||
showToast,
|
||||
} from 'vant'
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const activeColspan = ref([""]);
|
||||
const activeColspan = ref([''])
|
||||
|
||||
const rates = [
|
||||
{ css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png" },
|
||||
{ css: "size2-3", value: "2:3", text: "2:3", img: "/images/mj/rate_3_4.png" },
|
||||
{ css: "size3-4", value: "3:4", text: "3:4", img: "/images/mj/rate_3_4.png" },
|
||||
{ css: "size4-3", value: "4:3", text: "4:3", img: "/images/mj/rate_4_3.png" },
|
||||
{ css: "size16-9", value: "16:9", text: "16:9", img: "/images/mj/rate_16_9.png" },
|
||||
{ css: "size9-16", value: "9:16", text: "9:16", img: "/images/mj/rate_9_16.png" },
|
||||
];
|
||||
{ css: 'square', value: '1:1', text: '1:1', img: '/images/mj/rate_1_1.png' },
|
||||
{ css: 'size2-3', value: '2:3', text: '2:3', img: '/images/mj/rate_3_4.png' },
|
||||
{ css: 'size3-4', value: '3:4', text: '3:4', img: '/images/mj/rate_3_4.png' },
|
||||
{ css: 'size4-3', value: '4:3', text: '4:3', img: '/images/mj/rate_4_3.png' },
|
||||
{ css: 'size16-9', value: '16:9', text: '16:9', img: '/images/mj/rate_16_9.png' },
|
||||
{ css: 'size9-16', value: '9:16', text: '9:16', img: '/images/mj/rate_9_16.png' },
|
||||
]
|
||||
const models = [
|
||||
{ text: "MJ-6.0", value: " --v 6", img: "/images/mj/mj-v6.png" },
|
||||
{ text: "MJ-5.2", value: " --v 5.2", img: "/images/mj/mj-v5.2.png" },
|
||||
{ text: "Niji5", value: " --niji 5", img: "/images/mj/mj-niji.png" },
|
||||
{ text: "Niji5 可爱", value: " --niji 5 --style cute", img: "/images/mj/nj1.jpg" },
|
||||
{ text: "Niji5 风景", value: " --niji 5 --style scenic", img: "/images/mj/nj2.jpg" },
|
||||
{ text: "Niji6", value: " --niji 6", img: "/images/mj/nj3.jpg" },
|
||||
];
|
||||
const imgList = ref([]);
|
||||
{ text: 'MJ-6.0', value: ' --v 6', img: '/images/mj/mj-v6.png' },
|
||||
{ text: 'MJ-5.2', value: ' --v 5.2', img: '/images/mj/mj-v5.2.png' },
|
||||
{ text: 'Niji5', value: ' --niji 5', img: '/images/mj/mj-niji.png' },
|
||||
{ text: 'Niji5 可爱', value: ' --niji 5 --style cute', img: '/images/mj/nj1.jpg' },
|
||||
{ text: 'Niji5 风景', value: ' --niji 5 --style scenic', img: '/images/mj/nj2.jpg' },
|
||||
{ text: 'Niji6', value: ' --niji 6', img: '/images/mj/nj3.jpg' },
|
||||
]
|
||||
const imgList = ref([])
|
||||
const params = ref({
|
||||
task_type: "image",
|
||||
task_type: 'image',
|
||||
rate: rates[0].value,
|
||||
model: models[0].value,
|
||||
chaos: 0,
|
||||
@@ -291,229 +380,229 @@ const params = ref({
|
||||
img_arr: [],
|
||||
raw: false,
|
||||
iw: 0,
|
||||
prompt: "",
|
||||
neg_prompt: "",
|
||||
prompt: '',
|
||||
neg_prompt: '',
|
||||
tile: false,
|
||||
quality: 0,
|
||||
cref: "",
|
||||
sref: "",
|
||||
cref: '',
|
||||
sref: '',
|
||||
cw: 0,
|
||||
});
|
||||
const userId = ref(0);
|
||||
const router = useRouter();
|
||||
const runningJobs = ref([]);
|
||||
const finishedJobs = ref([]);
|
||||
const power = ref(0);
|
||||
const activeName = ref("txt2img");
|
||||
const isLogin = ref(false);
|
||||
const prompt = ref("");
|
||||
const store = useSharedStore();
|
||||
const clipboard = ref(null);
|
||||
const taskPulling = ref(true);
|
||||
const tastPullHandler = ref(null);
|
||||
const downloadPulling = ref(false);
|
||||
const downloadPullHandler = ref(null);
|
||||
})
|
||||
const userId = ref(0)
|
||||
const router = useRouter()
|
||||
const runningJobs = ref([])
|
||||
const finishedJobs = ref([])
|
||||
const power = ref(0)
|
||||
const activeName = ref('txt2img')
|
||||
const isLogin = ref(false)
|
||||
const prompt = ref('')
|
||||
const store = useSharedStore()
|
||||
const clipboard = ref(null)
|
||||
const taskPulling = ref(true)
|
||||
const tastPullHandler = ref(null)
|
||||
const downloadPulling = ref(false)
|
||||
const downloadPullHandler = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
clipboard.value = new Clipboard(".copy-prompt");
|
||||
clipboard.value.on("success", () => {
|
||||
showNotify({ type: "success", message: "复制成功", duration: 1000 });
|
||||
});
|
||||
clipboard.value.on("error", () => {
|
||||
showNotify({ type: "danger", message: "复制失败", duration: 2000 });
|
||||
});
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
showNotify({ type: 'success', message: '复制成功', duration: 1000 })
|
||||
})
|
||||
clipboard.value.on('error', () => {
|
||||
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
|
||||
})
|
||||
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
power.value = user["power"];
|
||||
userId.value = user.id;
|
||||
isLogin.value = true;
|
||||
fetchRunningJobs();
|
||||
fetchFinishJobs(1);
|
||||
power.value = user['power']
|
||||
userId.value = user.id
|
||||
isLogin.value = true
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
|
||||
tastPullHandler.value = setInterval(() => {
|
||||
if (taskPulling.value) {
|
||||
fetchRunningJobs();
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
|
||||
downloadPullHandler.value = setInterval(() => {
|
||||
if (downloadPulling.value) {
|
||||
page.value = 1;
|
||||
fetchFinishJobs(1);
|
||||
page.value = 1
|
||||
fetchFinishJobs(1)
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
})
|
||||
.catch(() => {
|
||||
// router.push('/login')
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
clipboard.value.destroy()
|
||||
if (tastPullHandler.value) {
|
||||
clearInterval(tastPullHandler.value);
|
||||
clearInterval(tastPullHandler.value)
|
||||
}
|
||||
if (downloadPullHandler.value) {
|
||||
clearInterval(downloadPullHandler.value);
|
||||
clearInterval(downloadPullHandler.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const mjPower = ref(1);
|
||||
const mjActionPower = ref(1);
|
||||
const mjPower = ref(1)
|
||||
const mjActionPower = ref(1)
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
mjPower.value = res.data["mj_power"];
|
||||
mjActionPower.value = res.data["mj_action_power"];
|
||||
mjPower.value = res.data['mj_power']
|
||||
mjActionPower.value = res.data['mj_action_power']
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: "danger", message: "获取系统配置失败:" + e.message });
|
||||
});
|
||||
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
|
||||
})
|
||||
|
||||
// 获取运行中的任务
|
||||
const fetchRunningJobs = (userId) => {
|
||||
if (!isLogin.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
httpGet(`/api/mj/jobs?finish=0&user_id=${userId}`)
|
||||
.then((res) => {
|
||||
const jobs = res.data.items;
|
||||
const _jobs = [];
|
||||
const jobs = res.data.items
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
showNotify({
|
||||
message: `任务执行失败:${jobs[i]["err_msg"]}`,
|
||||
type: "danger",
|
||||
});
|
||||
if (jobs[i].type === "image") {
|
||||
power.value += mjPower.value;
|
||||
message: `任务执行失败:${jobs[i]['err_msg']}`,
|
||||
type: 'danger',
|
||||
})
|
||||
if (jobs[i].type === 'image') {
|
||||
power.value += mjPower.value
|
||||
} else {
|
||||
power.value += mjActionPower.value;
|
||||
power.value += mjActionPower.value
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i]);
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
if (runningJobs.value.length !== _jobs.length) {
|
||||
page.value = 1;
|
||||
downloadPulling.value = true;
|
||||
fetchFinishJobs(1);
|
||||
page.value = 1
|
||||
downloadPulling.value = true
|
||||
fetchFinishJobs(1)
|
||||
}
|
||||
if (_jobs.length === 0) {
|
||||
taskPulling.value = false;
|
||||
taskPulling.value = false
|
||||
}
|
||||
runningJobs.value = _jobs;
|
||||
runningJobs.value = _jobs
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
|
||||
});
|
||||
};
|
||||
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
const error = ref(false);
|
||||
const page = ref(0);
|
||||
const pageSize = ref(10);
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref(false)
|
||||
const page = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const fetchFinishJobs = (page) => {
|
||||
if (!isLogin.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
// 获取已完成的任务
|
||||
httpGet(`/api/mj/jobs?finish=1&page=${page}&page_size=${pageSize.value}`)
|
||||
.then((res) => {
|
||||
const jobs = res.data.items;
|
||||
let hasDownload = false;
|
||||
const jobs = res.data.items
|
||||
let hasDownload = false
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].type === "upscale" || jobs[i].type === "swapFace") {
|
||||
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
|
||||
if (jobs[i].type === 'upscale' || jobs[i].type === 'swapFace') {
|
||||
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
|
||||
} else {
|
||||
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/480/q/75";
|
||||
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/480/q/75'
|
||||
}
|
||||
|
||||
if (jobs[i]["img_url"] === "" && jobs[i].progress === 100) {
|
||||
hasDownload = true;
|
||||
if (jobs[i]['img_url'] === '' && jobs[i].progress === 100) {
|
||||
hasDownload = true
|
||||
}
|
||||
|
||||
if (jobs[i].type !== "upscale" && jobs[i].progress === 100) {
|
||||
jobs[i]["can_opt"] = true;
|
||||
if (jobs[i].type !== 'upscale' && jobs[i].progress === 100) {
|
||||
jobs[i]['can_opt'] = true
|
||||
}
|
||||
}
|
||||
|
||||
if (page === 1) {
|
||||
downloadPulling.value = hasDownload;
|
||||
downloadPulling.value = hasDownload
|
||||
}
|
||||
|
||||
if (jobs.length < pageSize.value) {
|
||||
finished.value = true;
|
||||
finished.value = true
|
||||
}
|
||||
|
||||
if (page === 1) {
|
||||
finishedJobs.value = jobs;
|
||||
finishedJobs.value = jobs
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(jobs);
|
||||
finishedJobs.value = finishedJobs.value.concat(jobs)
|
||||
}
|
||||
nextTick(() => (loading.value = false));
|
||||
nextTick(() => (loading.value = false))
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
error.value = true;
|
||||
showFailToast("获取任务失败:" + e.message);
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
error.value = true
|
||||
showFailToast('获取任务失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
page.value += 1;
|
||||
fetchFinishJobs(page.value);
|
||||
};
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
}
|
||||
|
||||
// 切换图片比例
|
||||
const changeRate = (item) => {
|
||||
params.value.rate = item.value;
|
||||
};
|
||||
params.value.rate = item.value
|
||||
}
|
||||
// 切换模型
|
||||
const changeModel = (item) => {
|
||||
params.value.model = item.value;
|
||||
};
|
||||
params.value.model = item.value
|
||||
}
|
||||
|
||||
const imgKey = ref("");
|
||||
const imgKey = ref('')
|
||||
const beforeUpload = (key) => {
|
||||
imgKey.value = key;
|
||||
};
|
||||
imgKey.value = key
|
||||
}
|
||||
|
||||
// 图片上传
|
||||
const uploadImg = (file) => {
|
||||
file.status = "uploading";
|
||||
file.status = 'uploading'
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", result, result.name);
|
||||
const formData = new FormData()
|
||||
formData.append('file', result, result.name)
|
||||
// 执行上传操作
|
||||
httpPost("/api/upload", formData)
|
||||
httpPost('/api/upload', formData)
|
||||
.then((res) => {
|
||||
file.url = res.data.url;
|
||||
if (imgKey.value !== "") {
|
||||
file.url = res.data.url
|
||||
if (imgKey.value !== '') {
|
||||
// 单张图片上传
|
||||
params.value[imgKey.value] = res.data.url;
|
||||
imgKey.value = "";
|
||||
params.value[imgKey.value] = res.data.url
|
||||
imgKey.value = ''
|
||||
}
|
||||
file.status = "done";
|
||||
file.status = 'done'
|
||||
})
|
||||
.catch((e) => {
|
||||
file.status = "failed";
|
||||
file.message = "上传失败";
|
||||
showFailToast("图片上传失败:" + e.message);
|
||||
});
|
||||
file.status = 'failed'
|
||||
file.message = '上传失败'
|
||||
showFailToast('图片上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(err) {
|
||||
console.log(err.message);
|
||||
console.log(err.message)
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const send = (url, index, item) => {
|
||||
httpPost(url, {
|
||||
@@ -525,126 +614,126 @@ const send = (url, index, item) => {
|
||||
prompt: item.prompt,
|
||||
})
|
||||
.then(() => {
|
||||
showSuccessToast("任务推送成功,请耐心等待任务执行...");
|
||||
power.value -= mjActionPower.value;
|
||||
showSuccessToast('任务推送成功,请耐心等待任务执行...')
|
||||
power.value -= mjActionPower.value
|
||||
runningJobs.value.push({
|
||||
progress: 0,
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("任务推送失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showFailToast('任务推送失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 图片放大任务
|
||||
const upscale = (index, item) => {
|
||||
send("/api/mj/upscale", index, item);
|
||||
};
|
||||
send('/api/mj/upscale', index, item)
|
||||
}
|
||||
|
||||
// 图片变换任务
|
||||
const variation = (index, item) => {
|
||||
send("/api/mj/variation", index, item);
|
||||
};
|
||||
send('/api/mj/variation', index, item)
|
||||
}
|
||||
|
||||
const generate = () => {
|
||||
if (!isLogin.value) {
|
||||
return showLoginDialog(router);
|
||||
return showLoginDialog(router)
|
||||
}
|
||||
|
||||
if (params.value.prompt === "" && params.value.task_type === "image") {
|
||||
return showFailToast("请输入绘画提示词!");
|
||||
if (params.value.prompt === '' && params.value.task_type === 'image') {
|
||||
return showFailToast('请输入绘画提示词!')
|
||||
}
|
||||
if (params.value.model.indexOf("niji") !== -1 && params.value.raw) {
|
||||
return showFailToast("动漫模型不允许启用原始模式");
|
||||
if (params.value.model.indexOf('niji') !== -1 && params.value.raw) {
|
||||
return showFailToast('动漫模型不允许启用原始模式')
|
||||
}
|
||||
params.value.session_id = getSessionId();
|
||||
params.value.img_arr = imgList.value.map((img) => img.url);
|
||||
httpPost("/api/mj/image", params.value)
|
||||
params.value.session_id = getSessionId()
|
||||
params.value.img_arr = imgList.value.map((img) => img.url)
|
||||
httpPost('/api/mj/image', params.value)
|
||||
.then(() => {
|
||||
showToast("绘画任务推送成功,请耐心等待任务执行");
|
||||
power.value -= mjPower.value;
|
||||
taskPulling.value = true;
|
||||
showToast('绘画任务推送成功,请耐心等待任务执行')
|
||||
power.value -= mjPower.value
|
||||
taskPulling.value = true
|
||||
runningJobs.value.push({
|
||||
progress: 0,
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("任务推送失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showFailToast('任务推送失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (item) => {
|
||||
showConfirmDialog({
|
||||
title: "删除提示",
|
||||
message: "此操作将会删除任务和图片,继续操作码?",
|
||||
title: '删除提示',
|
||||
message: '此操作将会删除任务和图片,继续操作码?',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/mj/remove", { id: item.id, user_id: item.user_id })
|
||||
httpGet('/api/mj/remove', { id: item.id, user_id: item.user_id })
|
||||
.then(() => {
|
||||
showSuccessToast("任务删除成功");
|
||||
fetchFinishJobs(1);
|
||||
showSuccessToast('任务删除成功')
|
||||
fetchFinishJobs(1)
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("任务删除失败:" + e.message);
|
||||
});
|
||||
showFailToast('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("您取消了操作");
|
||||
});
|
||||
};
|
||||
showToast('您取消了操作')
|
||||
})
|
||||
}
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (item, action) => {
|
||||
let text = "图片发布";
|
||||
let text = '图片发布'
|
||||
if (action === false) {
|
||||
text = "取消发布";
|
||||
text = '取消发布'
|
||||
}
|
||||
httpGet("/api/mj/publish", { id: item.id, action: action, user_id: item.user_id })
|
||||
httpGet('/api/mj/publish', { id: item.id, action: action, user_id: item.user_id })
|
||||
.then(() => {
|
||||
showSuccessToast(text + "成功");
|
||||
item.publish = action;
|
||||
showSuccessToast(text + '成功')
|
||||
item.publish = action
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast(text + "失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showFailToast(text + '失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showPrompt = (item) => {
|
||||
prompt.value = item.prompt;
|
||||
prompt.value = item.prompt
|
||||
showConfirmDialog({
|
||||
title: "绘画提示词",
|
||||
title: '绘画提示词',
|
||||
message: item.prompt,
|
||||
confirmButtonText: "复制",
|
||||
cancelButtonText: "关闭",
|
||||
confirmButtonText: '复制',
|
||||
cancelButtonText: '关闭',
|
||||
})
|
||||
.then(() => {
|
||||
document.querySelector("#copy-btn").click();
|
||||
document.querySelector('#copy-btn').click()
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const showErrMsg = (item) => {
|
||||
showDialog({
|
||||
title: "错误详情",
|
||||
message: item["err_msg"],
|
||||
title: '错误详情',
|
||||
message: item['err_msg'],
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const imageView = (item) => {
|
||||
showImagePreview([item["img_url"]]);
|
||||
};
|
||||
showImagePreview([item['img_url']])
|
||||
}
|
||||
|
||||
// 切换菜单
|
||||
const tabChange = (tab) => {
|
||||
if (tab === "txt2img" || tab === "img2img") {
|
||||
params.value.task_type = "image";
|
||||
if (tab === 'txt2img' || tab === 'img2img') {
|
||||
params.value.task_type = 'image'
|
||||
} else {
|
||||
params.value.task_type = tab;
|
||||
params.value.task_type = tab
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mobile/image-mj.styl"
|
||||
@import "../../../assets/css/mobile/image-mj.styl"
|
||||
</style>
|
||||
|
||||
@@ -3,9 +3,20 @@
|
||||
<van-form @submit="generate">
|
||||
<van-cell-group inset>
|
||||
<div>
|
||||
<van-field v-model="params.sampler" is-link readonly label="采样方法" placeholder="选择采样方法" @click="showSamplerPicker = true" />
|
||||
<van-field
|
||||
v-model="params.sampler"
|
||||
is-link
|
||||
readonly
|
||||
label="采样方法"
|
||||
placeholder="选择采样方法"
|
||||
@click="showSamplerPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
|
||||
<van-picker :columns="samplers" @cancel="showSamplerPicker = false" @confirm="samplerConfirm" />
|
||||
<van-picker
|
||||
:columns="samplers"
|
||||
@cancel="showSamplerPicker = false"
|
||||
@confirm="samplerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
@@ -24,17 +35,30 @@
|
||||
|
||||
<van-field v-model.number="params.steps" label="迭代步数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon name="info-o" @click="showInfo('值越大则代表细节越多,同时也意味着出图速度越慢,一般推荐20-30')" />
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="showInfo('值越大则代表细节越多,同时也意味着出图速度越慢,一般推荐20-30')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon name="info-o" @click="showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')" />
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.seed" label="随机因子" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon name="info-o" @click="showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')" />
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
@@ -46,9 +70,20 @@
|
||||
|
||||
<div v-if="params.hd_fix">
|
||||
<div>
|
||||
<van-field v-model="params.hd_scale_alg" is-link readonly label="放大算法" placeholder="选择放大算法" @click="showUpscalePicker = true" />
|
||||
<van-field
|
||||
v-model="params.hd_scale_alg"
|
||||
is-link
|
||||
readonly
|
||||
label="放大算法"
|
||||
placeholder="选择放大算法"
|
||||
@click="showUpscalePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
|
||||
<van-picker :columns="upscaleAlgArr" @cancel="showUpscalePicker = false" @confirm="upscaleConfirm" />
|
||||
<van-picker
|
||||
:columns="upscaleAlgArr"
|
||||
@cancel="showUpscalePicker = false"
|
||||
@confirm="upscaleConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
@@ -57,10 +92,18 @@
|
||||
|
||||
<van-field label="重绘幅度">
|
||||
<template #input>
|
||||
<van-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1" @update:model-value="showToast('当前值:' + params.hd_redraw_rate)" />
|
||||
<van-slider
|
||||
v-model.number="params.hd_redraw_rate"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
@update:model-value="showToast('当前值:' + params.hd_redraw_rate)"
|
||||
/>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<van-icon name="info-o" @click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')" />
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
@@ -76,7 +119,14 @@
|
||||
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field v-model="params.neg_prompt" rows="3" maxlength="2000" autosize type="textarea" placeholder="不想出现在图片上的元素(例如:树,建筑)" />
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
|
||||
@@ -103,7 +153,14 @@
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle v-model:current-rate="item.progress" :rate="item.progress" :speed="100" :text="item.progress + '%'" :stroke-width="60" size="90px" />
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -139,11 +196,19 @@
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small">删除</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image :src="item['img_url']" :class="item['can_opt'] ? '' : 'upscale'" lazy-load @click="imageView(item)" fit="cover">
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
@@ -151,7 +216,12 @@
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle />
|
||||
<el-button type="warning" v-if="item.publish" @click="publishImage($event, item, false)" circle>
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
@@ -166,49 +236,64 @@
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
<button style="display: none" class="copy-prompt-sd" :data-clipboard-text="prompt" id="copy-btn-sd">复制</button>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-sd"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-sd"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { Delete } from "@element-plus/icons-vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import Clipboard from "clipboard";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSessionId } from "@/store/session";
|
||||
import { showConfirmDialog, showDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast } from "vant";
|
||||
import { showLoginDialog } from "@/utils/libs";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { showLoginDialog } from '@/utils/libs'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import {
|
||||
showConfirmDialog,
|
||||
showDialog,
|
||||
showFailToast,
|
||||
showImagePreview,
|
||||
showNotify,
|
||||
showSuccessToast,
|
||||
showToast,
|
||||
} from 'vant'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40);
|
||||
const mjBoxHeight = ref(window.innerHeight - 150);
|
||||
const isLogin = ref(false);
|
||||
const activeColspan = ref([""]);
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const isLogin = ref(false)
|
||||
const activeColspan = ref([''])
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40;
|
||||
mjBoxHeight.value = window.innerHeight - 150;
|
||||
};
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const samplers = ref([
|
||||
{ text: "Euler a", value: "Euler a" },
|
||||
{ text: "DPM++ 2S a Karras", value: "DPM++ 2S a Karras" },
|
||||
{ text: "DPM++ 2M Karras", value: "DPM++ 2M Karras" },
|
||||
{ text: "DPM++ 2M SDE Karras", value: "DPM++ 2M SDE Karras" },
|
||||
{ text: "DPM++ 2M Karras", value: "DPM++ 2M Karras" },
|
||||
{ text: "DPM++ 3M SDE Karras", value: "DPM++ 3M SDE Karras" },
|
||||
]);
|
||||
const showSamplerPicker = ref(false);
|
||||
{ text: 'Euler a', value: 'Euler a' },
|
||||
{ text: 'DPM++ 2S a Karras', value: 'DPM++ 2S a Karras' },
|
||||
{ text: 'DPM++ 2M Karras', value: 'DPM++ 2M Karras' },
|
||||
{ text: 'DPM++ 2M SDE Karras', value: 'DPM++ 2M SDE Karras' },
|
||||
{ text: 'DPM++ 2M Karras', value: 'DPM++ 2M Karras' },
|
||||
{ text: 'DPM++ 3M SDE Karras', value: 'DPM++ 3M SDE Karras' },
|
||||
])
|
||||
const showSamplerPicker = ref(false)
|
||||
|
||||
const upscaleAlgArr = ref([
|
||||
{ text: "Latent", value: "Latent" },
|
||||
{ text: "ESRGAN_4x", value: "ESRGAN_4x" },
|
||||
{ text: "ESRGAN 4x+", value: "ESRGAN 4x+" },
|
||||
{ text: "SwinIR_4x", value: "SwinIR_4x" },
|
||||
{ text: "LDSR", value: "LDSR" },
|
||||
]);
|
||||
const showUpscalePicker = ref(false);
|
||||
{ text: 'Latent', value: 'Latent' },
|
||||
{ text: 'ESRGAN_4x', value: 'ESRGAN_4x' },
|
||||
{ text: 'ESRGAN 4x+', value: 'ESRGAN 4x+' },
|
||||
{ text: 'SwinIR_4x', value: 'SwinIR_4x' },
|
||||
{ text: 'LDSR', value: 'LDSR' },
|
||||
])
|
||||
const showUpscalePicker = ref(false)
|
||||
|
||||
const params = ref({
|
||||
width: 1024,
|
||||
@@ -222,260 +307,261 @@ const params = ref({
|
||||
hd_scale: 2,
|
||||
hd_scale_alg: upscaleAlgArr.value[0].value,
|
||||
hd_steps: 0,
|
||||
prompt: "",
|
||||
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
|
||||
});
|
||||
prompt: '',
|
||||
neg_prompt:
|
||||
'nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet',
|
||||
})
|
||||
|
||||
const runningJobs = ref([]);
|
||||
const finishedJobs = ref([]);
|
||||
const allowPulling = ref(true); // 是否允许轮询
|
||||
const tastPullHandler = ref(null);
|
||||
const router = useRouter();
|
||||
const runningJobs = ref([])
|
||||
const finishedJobs = ref([])
|
||||
const allowPulling = ref(true) // 是否允许轮询
|
||||
const tastPullHandler = ref(null)
|
||||
const router = useRouter()
|
||||
// 检查是否有画同款的参数
|
||||
const _params = router.currentRoute.value.params["copyParams"];
|
||||
const _params = router.currentRoute.value.params['copyParams']
|
||||
if (_params) {
|
||||
params.value = JSON.parse(_params);
|
||||
params.value = JSON.parse(_params)
|
||||
}
|
||||
const power = ref(0);
|
||||
const sdPower = ref(0); // 画一张 SD 图片消耗算力
|
||||
const power = ref(0)
|
||||
const sdPower = ref(0) // 画一张 SD 图片消耗算力
|
||||
|
||||
const userId = ref(0);
|
||||
const store = useSharedStore();
|
||||
const clipboard = ref(null);
|
||||
const prompt = ref("");
|
||||
const userId = ref(0)
|
||||
const store = useSharedStore()
|
||||
const clipboard = ref(null)
|
||||
const prompt = ref('')
|
||||
onMounted(() => {
|
||||
initData();
|
||||
clipboard.value = new Clipboard(".copy-prompt-sd");
|
||||
clipboard.value.on("success", () => {
|
||||
showNotify({ type: "success", message: "复制成功", duration: 1000 });
|
||||
});
|
||||
clipboard.value.on("error", () => {
|
||||
showNotify({ type: "danger", message: "复制失败", duration: 2000 });
|
||||
});
|
||||
initData()
|
||||
clipboard.value = new Clipboard('.copy-prompt-sd')
|
||||
clipboard.value.on('success', () => {
|
||||
showNotify({ type: 'success', message: '复制成功', duration: 1000 })
|
||||
})
|
||||
clipboard.value.on('error', () => {
|
||||
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
|
||||
})
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
sdPower.value = res.data.sd_power;
|
||||
params.value.neg_prompt = res.data.sd_neg_prompt;
|
||||
sdPower.value = res.data.sd_power
|
||||
params.value.neg_prompt = res.data.sd_neg_prompt
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: "danger", message: "获取系统配置失败:" + e.message });
|
||||
});
|
||||
});
|
||||
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
clipboard.value.destroy()
|
||||
if (tastPullHandler.value) {
|
||||
clearInterval(tastPullHandler.value);
|
||||
clearInterval(tastPullHandler.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
power.value = user["power"];
|
||||
userId.value = user.id;
|
||||
isLogin.value = true;
|
||||
fetchRunningJobs();
|
||||
fetchFinishJobs(1);
|
||||
power.value = user['power']
|
||||
userId.value = user.id
|
||||
isLogin.value = true
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
|
||||
tastPullHandler.value = setInterval(() => {
|
||||
if (allowPulling.value) {
|
||||
fetchRunningJobs();
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/sd/jobs?finish=0`)
|
||||
.then((res) => {
|
||||
const jobs = res.data.items;
|
||||
const _jobs = [];
|
||||
const jobs = res.data.items
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
showNotify({
|
||||
message: `任务ID:${jobs[i]["task_id"]} 原因:${jobs[i]["err_msg"]}`,
|
||||
type: "danger",
|
||||
});
|
||||
power.value += sdPower.value;
|
||||
continue;
|
||||
message: `任务ID:${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
|
||||
type: 'danger',
|
||||
})
|
||||
power.value += sdPower.value
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i]);
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
|
||||
if (runningJobs.value.length !== _jobs.length) {
|
||||
fetchFinishJobs(1);
|
||||
fetchFinishJobs(1)
|
||||
}
|
||||
|
||||
if (runningJobs.value.length === 0) {
|
||||
allowPulling.value = false;
|
||||
allowPulling.value = false
|
||||
}
|
||||
runningJobs.value = _jobs;
|
||||
runningJobs.value = _jobs
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
|
||||
});
|
||||
};
|
||||
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
const error = ref(false);
|
||||
const page = ref(0);
|
||||
const pageSize = ref(10);
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref(false)
|
||||
const page = ref(0)
|
||||
const pageSize = ref(10)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = (page) => {
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
httpGet(`/api/sd/jobs?finish=1&page=${page}&page_size=${pageSize.value}`)
|
||||
.then((res) => {
|
||||
const jobs = res.data.items;
|
||||
const jobs = res.data.items
|
||||
if (jobs.length < pageSize.value) {
|
||||
finished.value = true;
|
||||
finished.value = true
|
||||
}
|
||||
const _jobs = [];
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
jobs[i]["thumb_url"] = jobs[i]["img_url"] + "?imageView2/1/w/480/h/600/q/75";
|
||||
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
|
||||
}
|
||||
_jobs.push(jobs[i]);
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
if (page === 1) {
|
||||
finishedJobs.value = _jobs;
|
||||
finishedJobs.value = _jobs
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(_jobs);
|
||||
finishedJobs.value = finishedJobs.value.concat(_jobs)
|
||||
}
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
showNotify({ type: "danger", message: "获取任务失败:" + e.message });
|
||||
});
|
||||
};
|
||||
loading.value = false
|
||||
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
page.value += 1;
|
||||
fetchFinishJobs(page.value);
|
||||
};
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
}
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null);
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (!isLogin.value) {
|
||||
return showLoginDialog(router);
|
||||
return showLoginDialog(router)
|
||||
}
|
||||
|
||||
if (params.value.prompt === "") {
|
||||
promptRef.value.focus();
|
||||
return showToast("请输入绘画提示词!");
|
||||
if (params.value.prompt === '') {
|
||||
promptRef.value.focus()
|
||||
return showToast('请输入绘画提示词!')
|
||||
}
|
||||
|
||||
if (!params.value.seed) {
|
||||
params.value.seed = -1;
|
||||
params.value.seed = -1
|
||||
}
|
||||
params.value.session_id = getSessionId();
|
||||
httpPost("/api/sd/image", params.value)
|
||||
params.value.session_id = getSessionId()
|
||||
httpPost('/api/sd/image', params.value)
|
||||
.then(() => {
|
||||
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...");
|
||||
power.value -= sdPower.value;
|
||||
allowPulling.value = true;
|
||||
showSuccessToast('绘画任务推送成功,请耐心等待任务执行...')
|
||||
power.value -= sdPower.value
|
||||
allowPulling.value = true
|
||||
runningJobs.value.push({
|
||||
progress: 0,
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("任务推送失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showFailToast('任务推送失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showPrompt = (item) => {
|
||||
prompt.value = item.prompt;
|
||||
prompt.value = item.prompt
|
||||
showConfirmDialog({
|
||||
title: "绘画提示词",
|
||||
title: '绘画提示词',
|
||||
message: item.prompt,
|
||||
confirmButtonText: "复制",
|
||||
cancelButtonText: "关闭",
|
||||
confirmButtonText: '复制',
|
||||
cancelButtonText: '关闭',
|
||||
})
|
||||
.then(() => {
|
||||
document.querySelector("#copy-btn-sd").click();
|
||||
document.querySelector('#copy-btn-sd').click()
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const showErrMsg = (item) => {
|
||||
showDialog({
|
||||
title: "错误详情",
|
||||
message: item["err_msg"],
|
||||
title: '错误详情',
|
||||
message: item['err_msg'],
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (event, item) => {
|
||||
event.stopPropagation();
|
||||
event.stopPropagation()
|
||||
showConfirmDialog({
|
||||
title: "标题",
|
||||
message: "此操作将会删除任务和图片,继续操作码?",
|
||||
title: '标题',
|
||||
message: '此操作将会删除任务和图片,继续操作码?',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/sd/remove", { id: item.id, user_id: item.user })
|
||||
httpGet('/api/sd/remove', { id: item.id, user_id: item.user })
|
||||
.then(() => {
|
||||
showSuccessToast("任务删除成功");
|
||||
fetchFinishJobs(1);
|
||||
showSuccessToast('任务删除成功')
|
||||
fetchFinishJobs(1)
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast("任务删除失败:" + e.message);
|
||||
});
|
||||
showFailToast('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
showToast("您取消了操作");
|
||||
});
|
||||
};
|
||||
showToast('您取消了操作')
|
||||
})
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (event, item, action) => {
|
||||
event.stopPropagation();
|
||||
let text = "图片发布";
|
||||
event.stopPropagation()
|
||||
let text = '图片发布'
|
||||
if (action === false) {
|
||||
text = "取消发布";
|
||||
text = '取消发布'
|
||||
}
|
||||
httpGet("/api/sd/publish", { id: item.id, action: action, user_id: item.user })
|
||||
httpGet('/api/sd/publish', { id: item.id, action: action, user_id: item.user })
|
||||
.then(() => {
|
||||
showSuccessToast(text + "成功");
|
||||
item.publish = action;
|
||||
showSuccessToast(text + '成功')
|
||||
item.publish = action
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast(text + "失败:" + e.message);
|
||||
});
|
||||
};
|
||||
showFailToast(text + '失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const imageView = (item) => {
|
||||
showImagePreview([item["img_url"]]);
|
||||
};
|
||||
showImagePreview([item['img_url']])
|
||||
}
|
||||
|
||||
const samplerConfirm = (item) => {
|
||||
params.value.sampler = item.selectedOptions[0].text;
|
||||
showSamplerPicker.value = false;
|
||||
};
|
||||
params.value.sampler = item.selectedOptions[0].text
|
||||
showSamplerPicker.value = false
|
||||
}
|
||||
|
||||
const upscaleConfirm = (item) => {
|
||||
params.value.hd_scale_alg = item.selectedOptions[0].text;
|
||||
showUpscalePicker.value = false;
|
||||
};
|
||||
params.value.hd_scale_alg = item.selectedOptions[0].text
|
||||
showUpscalePicker.value = false
|
||||
}
|
||||
|
||||
const showInfo = (message) => {
|
||||
showDialog({
|
||||
title: "参数说明",
|
||||
title: '参数说明',
|
||||
message: message,
|
||||
}).then(() => {
|
||||
// on close
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mobile/image-sd.styl"
|
||||
<style lang="stylus" scoped>
|
||||
@import "../../../assets/css/mobile/image-sd.styl"
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
}
|
||||
|
||||
57
web/vite.config.js
Normal file
57
web/vite.config.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 加载环境变量
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const apiHost = env.VITE_API_HOST || 'http://localhost:5678'
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
{
|
||||
'@vueuse/core': ['useMouse', 'useFetch'],
|
||||
},
|
||||
],
|
||||
dts: true, // 生成 TypeScript 声明文件
|
||||
}),
|
||||
],
|
||||
base: env.VITE_BASE_URL,
|
||||
build: {
|
||||
outDir: 'dist', // 构建输出目录
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'~@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
|
||||
server: {
|
||||
port: 8888, // 设置你想要的端口号
|
||||
open: false, // 可选:启动服务器时自动打开浏览器
|
||||
...(process.env.NODE_ENV === 'development'
|
||||
? {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: apiHost,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
'/static/upload/': {
|
||||
target: apiHost,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
const { defineConfig } = require("@vue/cli-service");
|
||||
const path = require("path");
|
||||
let webpack = require("webpack");
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
lintOnSave: false, //关闭eslint校验
|
||||
productionSourceMap: false, //在生产模式中禁用 Source Map,既可以减少包大小,也可以加密源码
|
||||
|
||||
configureWebpack: {
|
||||
// disable performance hints
|
||||
performance: {
|
||||
hints: false,
|
||||
},
|
||||
plugins: [new webpack.optimize.MinChunkSizePlugin({ minChunkSize: 10000 })],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
publicPath: "/",
|
||||
|
||||
outputDir: "dist",
|
||||
crossorigin: "anonymous",
|
||||
devServer: {
|
||||
client: {
|
||||
overlay: false // 关闭错误覆盖层
|
||||
},
|
||||
allowedHosts: "all",
|
||||
port: 8888,
|
||||
proxy: {
|
||||
"/static/upload/": {
|
||||
target: process.env.VUE_APP_API_HOST,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user