mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	feat: vue-mobile => 完成用户信息修改功能,前后端都添加文件上传功能。
This commit is contained in:
		@@ -17,6 +17,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AppServer struct {
 | 
			
		||||
	Debug        bool
 | 
			
		||||
	AppConfig    *types.AppConfig
 | 
			
		||||
	Engine       *gin.Engine
 | 
			
		||||
	ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
 | 
			
		||||
@@ -33,6 +34,7 @@ func NewServer(appConfig *types.AppConfig) *AppServer {
 | 
			
		||||
	gin.SetMode(gin.ReleaseMode)
 | 
			
		||||
	gin.DefaultWriter = io.Discard
 | 
			
		||||
	return &AppServer{
 | 
			
		||||
		Debug:         false,
 | 
			
		||||
		AppConfig:     appConfig,
 | 
			
		||||
		Engine:        gin.Default(),
 | 
			
		||||
		ChatContexts:  types.NewLMap[string, []types.Message](),
 | 
			
		||||
@@ -44,9 +46,11 @@ func NewServer(appConfig *types.AppConfig) *AppServer {
 | 
			
		||||
 | 
			
		||||
func (s *AppServer) Init(debug bool) {
 | 
			
		||||
	if debug { // 调试模式允许跨域请求 API
 | 
			
		||||
		s.Debug = debug
 | 
			
		||||
		logger.Info("Enabled debug mode")
 | 
			
		||||
		s.Engine.Use(corsMiddleware())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.Engine.Use(sessionMiddleware(s.AppConfig))
 | 
			
		||||
	s.Engine.Use(authorizeMiddleware(s))
 | 
			
		||||
	s.Engine.Use(errorHandler)
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ func NewDefaultConfig() *types.AppConfig {
 | 
			
		||||
		ProxyURL:  "",
 | 
			
		||||
		Manager:   types.Manager{Username: "admin", Password: "admin123"},
 | 
			
		||||
		StaticDir: "./static",
 | 
			
		||||
		StaticUrl: "http://localhost/5678/static",
 | 
			
		||||
 | 
			
		||||
		Session: types.Session{
 | 
			
		||||
			SecretKey: utils.RandString(64),
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ type AppConfig struct {
 | 
			
		||||
	MysqlDns  string  // mysql 连接地址
 | 
			
		||||
	Manager   Manager // 后台管理员账户信息
 | 
			
		||||
	StaticDir string  // 静态资源目录
 | 
			
		||||
	StaticUrl string  // 静态资源 URL
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Manager 管理员
 | 
			
		||||
 
 | 
			
		||||
@@ -182,8 +182,11 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if h.App.Debug { // 调试打印聊天上下文
 | 
			
		||||
			logger.Info("聊天上下文:", chatCtx)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	req.Messages = append(chatCtx, types.Message{
 | 
			
		||||
		Role:    "user",
 | 
			
		||||
		Content: prompt,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										67
									
								
								api/go/handler/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								api/go/handler/upload_handler.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/utils/resp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UploadHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB) *UploadHandler {
 | 
			
		||||
	handler := &UploadHandler{db: db}
 | 
			
		||||
	handler.App = app
 | 
			
		||||
	return handler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) Upload(c *gin.Context) {
 | 
			
		||||
	file, err := c.FormFile("file")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("文件上传失败: %s", err.Error()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filePath, err := h.genFilePath(file.Filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("文件上传失败: %s", err.Error()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 将文件保存到指定路径
 | 
			
		||||
	err = c.SaveUploadedFile(file, filePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, fmt.Sprintf("文件保存失败: %s", err.Error()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, h.genFileUrl(filePath))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生成上传文件路径
 | 
			
		||||
func (h *UploadHandler) genFilePath(filename string) (string, error) {
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	dir := fmt.Sprintf("%s/upload/%d/%d", h.App.AppConfig.StaticDir, now.Year(), now.Month())
 | 
			
		||||
	_, err := os.Stat(dir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		err = os.MkdirAll(dir, 0755)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("创建上传目录失败:%s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	fileExt := filepath.Ext(filename)
 | 
			
		||||
	return fmt.Sprintf("%s/%d%s", dir, now.UnixMilli(), fileExt), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生成上传文件 URL
 | 
			
		||||
func (h *UploadHandler) genFileUrl(filePath string) string {
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	filename := filepath.Base(filePath)
 | 
			
		||||
	return fmt.Sprintf("%s/upload/%d/%d/%s", h.App.AppConfig.StaticUrl, now.Year(), now.Month(), filename)
 | 
			
		||||
}
 | 
			
		||||
@@ -233,8 +233,38 @@ func (h *UserHandler) Session(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type userProfile struct {
 | 
			
		||||
	Id         uint             `json:"id"`
 | 
			
		||||
	Username   string           `json:"username"`
 | 
			
		||||
	Nickname   string           `json:"nickname"`
 | 
			
		||||
	Avatar     string           `json:"avatar"`
 | 
			
		||||
	ChatConfig types.ChatConfig `json:"chat_config"`
 | 
			
		||||
	Calls      int              `json:"calls"`
 | 
			
		||||
	Tokens     int              `json:"tokens"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) Profile(c *gin.Context) {
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.db.First(&user, user.Id)
 | 
			
		||||
	var profile userProfile
 | 
			
		||||
	err = utils.CopyObject(user, &profile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("对象拷贝失败:", err.Error())
 | 
			
		||||
		resp.ERROR(c, "获取用户信息失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	profile.Id = user.Id
 | 
			
		||||
	resp.SUCCESS(c, profile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) ProfileUpdate(c *gin.Context) {
 | 
			
		||||
	var data vo.User
 | 
			
		||||
	var data userProfile
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
@@ -272,26 +302,6 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UserHandler) Profile(c *gin.Context) {
 | 
			
		||||
	user, err := utils.GetLoginUser(c, h.db)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.db.First(&user, user.Id)
 | 
			
		||||
	var userVo vo.User
 | 
			
		||||
	err = utils.CopyObject(user, &userVo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error("对象拷贝失败:", err.Error())
 | 
			
		||||
		resp.ERROR(c, "获取用户信息失败")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userVo.Id = user.Id
 | 
			
		||||
	resp.SUCCESS(c, userVo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Password 更新密码
 | 
			
		||||
func (h *UserHandler) Password(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -93,8 +93,9 @@ func main() {
 | 
			
		||||
		fx.Provide(handler.NewChatRoleHandler),
 | 
			
		||||
		fx.Provide(handler.NewUserHandler),
 | 
			
		||||
		fx.Provide(handler.NewChatHandler),
 | 
			
		||||
		fx.Provide(admin.NewConfigHandler),
 | 
			
		||||
		fx.Provide(handler.NewUploadHandler),
 | 
			
		||||
 | 
			
		||||
		fx.Provide(admin.NewConfigHandler),
 | 
			
		||||
		fx.Provide(admin.NewAdminHandler),
 | 
			
		||||
		fx.Provide(admin.NewApiKeyHandler),
 | 
			
		||||
		fx.Provide(admin.NewUserHandler),
 | 
			
		||||
@@ -126,8 +127,11 @@ func main() {
 | 
			
		||||
			group.GET("tokens", h.Tokens)
 | 
			
		||||
			group.GET("stop", h.StopGenerate)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
 | 
			
		||||
			s.Engine.POST("/api/upload", h.Upload)
 | 
			
		||||
		}),
 | 
			
		||||
 | 
			
		||||
		//
 | 
			
		||||
		// 管理后台控制器
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/config/")
 | 
			
		||||
			group.POST("update", h.Update)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,16 +4,16 @@ import "chatplus/core/types"
 | 
			
		||||
 | 
			
		||||
type User struct {
 | 
			
		||||
	BaseVo
 | 
			
		||||
	Username    string           `json:"username,omitempty"`
 | 
			
		||||
	Nickname    string           `json:"nickname,omitempty"`
 | 
			
		||||
	Avatar      string           `json:"avatar,omitempty"`
 | 
			
		||||
	Salt        string           `json:"salt,omitempty"`          // 密码盐
 | 
			
		||||
	Tokens      int64            `json:"tokens,omitempty"`        // 剩余tokens
 | 
			
		||||
	Calls       int              `json:"calls,omitempty"`         // 剩余对话次数
 | 
			
		||||
	ChatConfig  types.ChatConfig `json:"chat_config,omitempty"`   // 聊天配置
 | 
			
		||||
	ChatRoles   []string         `json:"chat_roles,omitempty"`    // 聊天角色集合
 | 
			
		||||
	ExpiredTime int64            `json:"expired_time,omitempty"`  // 账户到期时间
 | 
			
		||||
	Status      bool             `json:"status,omitempty"`        // 当前状态
 | 
			
		||||
	LastLoginAt int64            `json:"last_login_at,omitempty"` // 最后登录时间
 | 
			
		||||
	LastLoginIp string           `json:"last_login_ip,omitempty"` // 最后登录 IP
 | 
			
		||||
	Username    string           `json:"username"`
 | 
			
		||||
	Nickname    string           `json:"nickname"`
 | 
			
		||||
	Avatar      string           `json:"avatar"`
 | 
			
		||||
	Salt        string           `json:"salt"`          // 密码盐
 | 
			
		||||
	Tokens      int64            `json:"tokens"`        // 剩余tokens
 | 
			
		||||
	Calls       int              `json:"calls"`         // 剩余对话次数
 | 
			
		||||
	ChatConfig  types.ChatConfig `json:"chat_config"`   // 聊天配置
 | 
			
		||||
	ChatRoles   []string         `json:"chat_roles"`    // 聊天角色集合
 | 
			
		||||
	ExpiredTime int64            `json:"expired_time"`  // 账户到期时间
 | 
			
		||||
	Status      bool             `json:"status"`        // 当前状态
 | 
			
		||||
	LastLoginAt int64            `json:"last_login_at"` // 最后登录时间
 | 
			
		||||
	LastLoginIp string           `json:"last_login_ip"` // 最后登录 IP
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										45
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -11,6 +11,7 @@
 | 
			
		||||
        "@element-plus/icons-vue": "^2.1.0",
 | 
			
		||||
        "axios": "^0.27.2",
 | 
			
		||||
        "clipboard": "^2.0.11",
 | 
			
		||||
        "compressorjs": "^1.2.1",
 | 
			
		||||
        "core-js": "^3.8.3",
 | 
			
		||||
        "element-plus": "^2.1.11",
 | 
			
		||||
        "good-storage": "^1.1.1",
 | 
			
		||||
@@ -3634,6 +3635,11 @@
 | 
			
		||||
      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/blueimp-canvas-to-blob": {
 | 
			
		||||
      "version": "3.29.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
 | 
			
		||||
      "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/body-parser": {
 | 
			
		||||
      "version": "1.19.2",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.19.2.tgz",
 | 
			
		||||
@@ -4225,6 +4231,15 @@
 | 
			
		||||
      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/compressorjs": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "blueimp-canvas-to-blob": "^3.29.0",
 | 
			
		||||
        "is-blob": "^2.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/concat-map": {
 | 
			
		||||
      "version": "0.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
 | 
			
		||||
@@ -6632,6 +6647,17 @@
 | 
			
		||||
        "node": ">=8"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/is-blob": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/is-ci": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz",
 | 
			
		||||
@@ -14142,6 +14168,11 @@
 | 
			
		||||
      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "blueimp-canvas-to-blob": {
 | 
			
		||||
      "version": "3.29.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
 | 
			
		||||
      "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="
 | 
			
		||||
    },
 | 
			
		||||
    "body-parser": {
 | 
			
		||||
      "version": "1.19.2",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.19.2.tgz",
 | 
			
		||||
@@ -14611,6 +14642,15 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "compressorjs": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "blueimp-canvas-to-blob": "^3.29.0",
 | 
			
		||||
        "is-blob": "^2.1.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "concat-map": {
 | 
			
		||||
      "version": "0.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
 | 
			
		||||
@@ -16523,6 +16563,11 @@
 | 
			
		||||
        "binary-extensions": "^2.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "is-blob": {
 | 
			
		||||
      "version": "2.1.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw=="
 | 
			
		||||
    },
 | 
			
		||||
    "is-ci": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
    "@element-plus/icons-vue": "^2.1.0",
 | 
			
		||||
    "axios": "^0.27.2",
 | 
			
		||||
    "clipboard": "^2.0.11",
 | 
			
		||||
    "compressorjs": "^1.2.1",
 | 
			
		||||
    "core-js": "^3.8.3",
 | 
			
		||||
    "element-plus": "^2.1.11",
 | 
			
		||||
    "good-storage": "^1.1.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -9,23 +9,33 @@
 | 
			
		||||
    <div class="user-info" id="user-info">
 | 
			
		||||
      <el-form v-if="form.id" :model="form" label-width="120px">
 | 
			
		||||
        <el-form-item label="昵称">
 | 
			
		||||
          <el-input v-model="form['nickname']"/>
 | 
			
		||||
          <el-input v-model="form.nickname"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="头像">
 | 
			
		||||
          <el-input v-model="form['avatar']"/>
 | 
			
		||||
          <el-upload
 | 
			
		||||
              class="avatar-uploader"
 | 
			
		||||
              :auto-upload="true"
 | 
			
		||||
              :show-file-list="false"
 | 
			
		||||
              :http-request="afterRead"
 | 
			
		||||
          >
 | 
			
		||||
            <el-avatar v-if="form.avatar" :src="form.avatar" shape="square" :size="100"/>
 | 
			
		||||
            <el-icon v-else class="avatar-uploader-icon">
 | 
			
		||||
              <Plus/>
 | 
			
		||||
            </el-icon>
 | 
			
		||||
          </el-upload>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="用户名">
 | 
			
		||||
          <el-input v-model="form['username']" disabled/>
 | 
			
		||||
          <el-input v-model="form.username" readonly disabled/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
        <el-form-item label="聊天上下文">
 | 
			
		||||
          <el-switch v-model="form['chat_config']['enable_context']"/>
 | 
			
		||||
          <el-switch v-model="form.chat_config.enable_context"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="聊天记录">
 | 
			
		||||
          <el-switch v-model="form['chat_config']['enable_history']"/>
 | 
			
		||||
          <el-switch v-model="form.chat_config.enable_history"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="Model">
 | 
			
		||||
          <el-select v-model="form['chat_config']['model']" placeholder="默认会话模型">
 | 
			
		||||
          <el-select v-model="form.chat_config.model" placeholder="默认会话模型">
 | 
			
		||||
            <el-option
 | 
			
		||||
                v-for="item in models"
 | 
			
		||||
                :key="item"
 | 
			
		||||
@@ -35,10 +45,10 @@
 | 
			
		||||
          </el-select>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="MaxTokens">
 | 
			
		||||
          <el-input v-model.number="form['chat_config']['max_tokens']"/>
 | 
			
		||||
          <el-input v-model.number="form.chat_config.max_tokens"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="Temperature">
 | 
			
		||||
          <el-input v-model.number="form['chat_config']['temperature']"/>
 | 
			
		||||
          <el-input v-model.number="form.chat_config.temperature"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="剩余调用次数">
 | 
			
		||||
          <el-tag>{{ form['calls'] }}</el-tag>
 | 
			
		||||
@@ -69,6 +79,9 @@
 | 
			
		||||
import {computed, onMounted, ref} from "vue"
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {Plus} from "@element-plus/icons-vue";
 | 
			
		||||
import Compressor from "compressorjs";
 | 
			
		||||
import {showNotify} from "vant";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  show: Boolean,
 | 
			
		||||
@@ -79,7 +92,14 @@ const props = defineProps({
 | 
			
		||||
const showDialog = computed(() => {
 | 
			
		||||
  return props.show
 | 
			
		||||
})
 | 
			
		||||
const form = ref({})
 | 
			
		||||
const form = ref({
 | 
			
		||||
  username: '',
 | 
			
		||||
  nickname: '',
 | 
			
		||||
  avatar: '',
 | 
			
		||||
  calls: 0,
 | 
			
		||||
  tokens: 0,
 | 
			
		||||
  chat_configs: {}
 | 
			
		||||
})
 | 
			
		||||
const top = computed(() => {
 | 
			
		||||
  if (window.innerHeight < 1024) {
 | 
			
		||||
    return '5vh';
 | 
			
		||||
@@ -97,6 +117,29 @@ onMounted(() => {
 | 
			
		||||
  });
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const afterRead = (file) => {
 | 
			
		||||
  console.log(file)
 | 
			
		||||
  // 压缩图片并上传
 | 
			
		||||
  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
 | 
			
		||||
        ElMessage.success({message: '上传成功', appendTo: '#user-info', duration: 1000})
 | 
			
		||||
      }).catch((e) => {
 | 
			
		||||
        console.log(e.message)
 | 
			
		||||
        ElMessage.error({message: '上传失败', appendTo: '#user-info'})
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    error(err) {
 | 
			
		||||
      console.log(err.message);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits(['hide', 'update-user']);
 | 
			
		||||
const save = function () {
 | 
			
		||||
  httpPost('/api/user/profile/update', form.value).then(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -73,6 +73,7 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
    .content {
 | 
			
		||||
      word-break break-word;
 | 
			
		||||
      text-align left
 | 
			
		||||
      padding: 5px 10px;
 | 
			
		||||
      background-color: #98E165;
 | 
			
		||||
      color #444444
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,9 @@ import {
 | 
			
		||||
    Switch,
 | 
			
		||||
    Tabbar,
 | 
			
		||||
    TabbarItem,
 | 
			
		||||
    TextEllipsis
 | 
			
		||||
    Tag,
 | 
			
		||||
    TextEllipsis,
 | 
			
		||||
    Uploader
 | 
			
		||||
} from "vant";
 | 
			
		||||
import router from "@/router";
 | 
			
		||||
 | 
			
		||||
@@ -58,6 +60,8 @@ app.use(SwipeCell);
 | 
			
		||||
app.use(Dialog);
 | 
			
		||||
app.use(ShareSheet);
 | 
			
		||||
app.use(Switch);
 | 
			
		||||
app.use(Uploader);
 | 
			
		||||
app.use(Tag);
 | 
			
		||||
app.use(router).use(ElementPlus).mount('#app')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -209,7 +209,7 @@ import {
 | 
			
		||||
  VideoPause
 | 
			
		||||
} from '@element-plus/icons-vue'
 | 
			
		||||
import 'highlight.js/styles/a11y-dark.css'
 | 
			
		||||
import {dateFormat, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs";
 | 
			
		||||
import {dateFormat, isMobile, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs";
 | 
			
		||||
import {ElMessage, ElMessageBox} from "element-plus";
 | 
			
		||||
import hl from "highlight.js";
 | 
			
		||||
import {getSessionId, removeLoginUser} from "@/store/session";
 | 
			
		||||
@@ -241,6 +241,10 @@ const showConfigDialog = ref(false);
 | 
			
		||||
const showPasswordDialog = ref(false);
 | 
			
		||||
const isLogin = ref(false)
 | 
			
		||||
 | 
			
		||||
if (isMobile()) {
 | 
			
		||||
  router.replace("/mobile")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  resizeElement();
 | 
			
		||||
  checkSession().then((user) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,11 +6,16 @@
 | 
			
		||||
import {ref} from "vue"
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import {checkSession} from "@/action/session";
 | 
			
		||||
import {isMobile} from "@/utils/libs";
 | 
			
		||||
 | 
			
		||||
const title = ref("HI, ChatGPT PLUS!");
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
checkSession().then(() => {
 | 
			
		||||
  router.push("chat")
 | 
			
		||||
  if (isMobile()) {
 | 
			
		||||
    router.push("/mobile")
 | 
			
		||||
  } else {
 | 
			
		||||
    router.push("/chat")
 | 
			
		||||
  }
 | 
			
		||||
}).catch(() => {
 | 
			
		||||
  router.push("login")
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -38,8 +38,8 @@
 | 
			
		||||
        <el-pagination v-if="users.total > 0"
 | 
			
		||||
                       background
 | 
			
		||||
                       layout="total, prev, pager, next"
 | 
			
		||||
                       :current-page="users.page"
 | 
			
		||||
                       :page-size="users.page_size"
 | 
			
		||||
                       v-model:current-page="users.page"
 | 
			
		||||
                       v-model:page-size="users.page_size"
 | 
			
		||||
                       :total="users.total"
 | 
			
		||||
                       @current-change="fetchUserList(users.page, users.page_size)"
 | 
			
		||||
        />
 | 
			
		||||
@@ -110,7 +110,7 @@ import {ElMessage, ElMessageBox} from "element-plus";
 | 
			
		||||
import {dateFormat, disabledDate, removeArrayItem} from "@/utils/libs";
 | 
			
		||||
 | 
			
		||||
// 变量定义
 | 
			
		||||
const users = ref({})
 | 
			
		||||
const users = ref({page: 1, page_size: 15})
 | 
			
		||||
 | 
			
		||||
const user = ref({chat_roles: []})
 | 
			
		||||
const roles = ref([])
 | 
			
		||||
@@ -128,7 +128,7 @@ const loading = ref(true)
 | 
			
		||||
const userEditFormRef = ref(null)
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  fetchUserList(1, 10)
 | 
			
		||||
  fetchUserList(users.value.page, users.value.page_size)
 | 
			
		||||
  // 获取角色列表
 | 
			
		||||
  httpGet('/api/admin/role/list').then((res) => {
 | 
			
		||||
    roles.value = res.data;
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,7 @@ const onLoad = () => {
 | 
			
		||||
        blocks.forEach((block) => {
 | 
			
		||||
          hl.highlightElement(block)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        scrollListBox()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
@@ -239,12 +240,10 @@ const connect = function (chat_id, role_id) {
 | 
			
		||||
            blocks.forEach((block) => {
 | 
			
		||||
              hl.highlightElement(block)
 | 
			
		||||
            })
 | 
			
		||||
            scrollListBox()
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        nextTick(() => {
 | 
			
		||||
          scrollListBox()
 | 
			
		||||
        })
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -275,7 +274,7 @@ const connect = function (chat_id, role_id) {
 | 
			
		||||
 | 
			
		||||
// 将聊天框的滚动条滑动到最底部
 | 
			
		||||
const scrollListBox = () => {
 | 
			
		||||
  document.getElementById('message-list-box').scrollTo(0, document.getElementById('message-list-box').scrollHeight)
 | 
			
		||||
  document.getElementById('message-list-box').scrollTo(0, document.getElementById('message-list-box').scrollHeight + 46)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const sendMessage = () => {
 | 
			
		||||
@@ -352,6 +351,7 @@ const shareChat = () => {
 | 
			
		||||
.mobile-chat {
 | 
			
		||||
  .message-list-box {
 | 
			
		||||
    padding-top 50px
 | 
			
		||||
    padding-bottom 10px
 | 
			
		||||
    overflow-x auto
 | 
			
		||||
    background #F5F5F5;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,20 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {getMobileTheme} from "@/store/system";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import {isMobile} from "@/utils/libs";
 | 
			
		||||
import {checkSession} from "@/action/session";
 | 
			
		||||
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
checkSession().then(() => {
 | 
			
		||||
  if (!isMobile()) {
 | 
			
		||||
    router.replace('/chat')
 | 
			
		||||
  }
 | 
			
		||||
}).catch(() => {
 | 
			
		||||
  router.push('/login')
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const active = ref('home')
 | 
			
		||||
 | 
			
		||||
const onChange = (index) => {
 | 
			
		||||
  console.log(index)
 | 
			
		||||
  // showToast(`标签 ${index}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,23 +3,43 @@
 | 
			
		||||
    <van-nav-bar :title="title"/>
 | 
			
		||||
 | 
			
		||||
    <div class="content">
 | 
			
		||||
      <van-form @submit="onSubmit">
 | 
			
		||||
        <van-cell-group inset>
 | 
			
		||||
      <van-form @submit="save">
 | 
			
		||||
        <van-cell-group inset v-model="form">
 | 
			
		||||
          <van-field
 | 
			
		||||
              v-model="username"
 | 
			
		||||
              v-model="form.username"
 | 
			
		||||
              name="用户名"
 | 
			
		||||
              label="用户名"
 | 
			
		||||
              readonly
 | 
			
		||||
              disabled
 | 
			
		||||
              placeholder="用户名"
 | 
			
		||||
              :rules="[{ required: true, message: '请填写用户名' }]"
 | 
			
		||||
          />
 | 
			
		||||
          <van-field
 | 
			
		||||
              v-model="password"
 | 
			
		||||
              type="password"
 | 
			
		||||
              name="密码"
 | 
			
		||||
              label="密码"
 | 
			
		||||
              placeholder="密码"
 | 
			
		||||
              :rules="[{ required: true, message: '请填写密码' }]"
 | 
			
		||||
              v-model="form.nickname"
 | 
			
		||||
              name="昵称"
 | 
			
		||||
              label="昵称"
 | 
			
		||||
              placeholder="昵称"
 | 
			
		||||
              :rules="[{ required: true, message: '请填写用户昵称' }]"
 | 
			
		||||
          />
 | 
			
		||||
          <van-field label="头像">
 | 
			
		||||
            <template #input>
 | 
			
		||||
              <van-uploader v-model="fileList"
 | 
			
		||||
                            reupload max-count="1"
 | 
			
		||||
                            :deletable="false"
 | 
			
		||||
                            :after-read="afterRead"/>
 | 
			
		||||
            </template>
 | 
			
		||||
          </van-field>
 | 
			
		||||
 | 
			
		||||
          <van-field label="剩余次数">
 | 
			
		||||
            <template #input>
 | 
			
		||||
              <van-tag type="success">{{ form.calls }}</van-tag>
 | 
			
		||||
            </template>
 | 
			
		||||
          </van-field>
 | 
			
		||||
 | 
			
		||||
          <van-field label="剩余 Tokens">
 | 
			
		||||
            <template #input>
 | 
			
		||||
              <van-tag type="primary">{{ form.tokens }}</van-tag>
 | 
			
		||||
            </template>
 | 
			
		||||
          </van-field>
 | 
			
		||||
        </van-cell-group>
 | 
			
		||||
        <div style="margin: 16px;">
 | 
			
		||||
          <van-button round block type="primary" native-type="submit">
 | 
			
		||||
@@ -32,9 +52,68 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {onMounted, ref} from "vue";
 | 
			
		||||
import {showFailToast, showNotify, showSuccessToast} from "vant";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import Compressor from 'compressorjs';
 | 
			
		||||
 | 
			
		||||
const title = ref('用户设置')
 | 
			
		||||
const form = ref({
 | 
			
		||||
  username: '',
 | 
			
		||||
  nickname: '',
 | 
			
		||||
  avatar: '',
 | 
			
		||||
  calls: 0,
 | 
			
		||||
  tokens: 0
 | 
			
		||||
})
 | 
			
		||||
const fileList = ref([
 | 
			
		||||
  {
 | 
			
		||||
    url: 'https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg',
 | 
			
		||||
    message: '上传中...',
 | 
			
		||||
  }
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  httpGet('/api/user/profile').then(res => {
 | 
			
		||||
    form.value = res.data
 | 
			
		||||
    fileList.value[0].url = form.value.avatar
 | 
			
		||||
  }).catch((e) => {
 | 
			
		||||
    console.log(e.message)
 | 
			
		||||
    showFailToast('获取用户信息失败')
 | 
			
		||||
  });
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
        file.status = 'success'
 | 
			
		||||
        showNotify({type: 'success', message: '上传成功'})
 | 
			
		||||
      }).catch((e) => {
 | 
			
		||||
        console.log(e.message)
 | 
			
		||||
        showNotify({type: 'danger', message: '上传失败'})
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    error(err) {
 | 
			
		||||
      console.log(err.message);
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const save = () => {
 | 
			
		||||
  httpPost('/api/user/profile/update', form.value).then(() => {
 | 
			
		||||
    showSuccessToast('保存成功')
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    showFailToast('保存失败')
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
        <van-cell-group inset>
 | 
			
		||||
          <van-field
 | 
			
		||||
              v-model="form.chat_config.model"
 | 
			
		||||
              is-link
 | 
			
		||||
              readonly
 | 
			
		||||
              label="默认模型"
 | 
			
		||||
              placeholder=""
 | 
			
		||||
@@ -15,8 +14,8 @@
 | 
			
		||||
          />
 | 
			
		||||
          <van-field
 | 
			
		||||
              v-model.number="form.chat_config.max_tokens"
 | 
			
		||||
              name="MaxTokens"
 | 
			
		||||
              type="number"
 | 
			
		||||
              name="MaxTokens"
 | 
			
		||||
              label="MaxTokens"
 | 
			
		||||
              placeholder="每次请求最大 token 数量"
 | 
			
		||||
              :rules="[{ required: true, message: '请填写 MaxTokens' }]"
 | 
			
		||||
@@ -51,7 +50,7 @@
 | 
			
		||||
        </van-cell-group>
 | 
			
		||||
        <div style="margin: 16px;">
 | 
			
		||||
          <van-button round block type="primary" native-type="submit">
 | 
			
		||||
            保存
 | 
			
		||||
            提交
 | 
			
		||||
          </van-button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </van-form>
 | 
			
		||||
@@ -90,7 +89,6 @@ const models = ref([])
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 获取最新用户信息
 | 
			
		||||
  httpGet('/api/user/profile').then(res => {
 | 
			
		||||
    console.log(res.data)
 | 
			
		||||
    form.value = res.data
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    showFailToast('获取用户信息失败')
 | 
			
		||||
@@ -99,7 +97,6 @@ onMounted(() => {
 | 
			
		||||
  // 加载系统配置
 | 
			
		||||
  httpGet('/api/admin/config/get?key=system').then(res => {
 | 
			
		||||
    const mds = res.data.models;
 | 
			
		||||
    console.log(mds)
 | 
			
		||||
    mds.forEach(item => {
 | 
			
		||||
      models.value.push({text: item, value: item})
 | 
			
		||||
    })
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user