diff --git a/api/go/core/app_server.go b/api/go/core/app_server.go index aae63391..5a993643 100644 --- a/api/go/core/app_server.go +++ b/api/go/core/app_server.go @@ -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) diff --git a/api/go/core/config.go b/api/go/core/config.go index f32f76b0..6c43003d 100644 --- a/api/go/core/config.go +++ b/api/go/core/config.go @@ -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), diff --git a/api/go/core/types/config.go b/api/go/core/types/config.go index ede1f7df..9289f78f 100644 --- a/api/go/core/types/config.go +++ b/api/go/core/types/config.go @@ -12,6 +12,7 @@ type AppConfig struct { MysqlDns string // mysql 连接地址 Manager Manager // 后台管理员账户信息 StaticDir string // 静态资源目录 + StaticUrl string // 静态资源 URL } // Manager 管理员 diff --git a/api/go/handler/chat_handler.go b/api/go/handler/chat_handler.go index 9130a007..177e5296 100644 --- a/api/go/handler/chat_handler.go +++ b/api/go/handler/chat_handler.go @@ -182,7 +182,10 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession } } } - logger.Info("聊天上下文:", chatCtx) + + if h.App.Debug { // 调试打印聊天上下文 + logger.Info("聊天上下文:", chatCtx) + } } req.Messages = append(chatCtx, types.Message{ Role: "user", diff --git a/api/go/handler/upload_handler.go b/api/go/handler/upload_handler.go new file mode 100644 index 00000000..ca3248af --- /dev/null +++ b/api/go/handler/upload_handler.go @@ -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) +} diff --git a/api/go/handler/user_handler.go b/api/go/handler/user_handler.go index ea6b61a4..7cbe66d3 100644 --- a/api/go/handler/user_handler.go +++ b/api/go/handler/user_handler.go @@ -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 { diff --git a/api/go/main.go b/api/go/main.go index ac09bdba..d6d0e886 100644 --- a/api/go/main.go +++ b/api/go/main.go @@ -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) diff --git a/api/go/store/vo/user.go b/api/go/store/vo/user.go index 987b750e..95b6a5ba 100644 --- a/api/go/store/vo/user.go +++ b/api/go/store/vo/user.go @@ -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 } diff --git a/web/package-lock.json b/web/package-lock.json index c45695cb..7ddbdee1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index da771ccb..898238ed 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/components/ConfigDialog.vue b/web/src/components/ConfigDialog.vue index bac79509..319da219 100644 --- a/web/src/components/ConfigDialog.vue +++ b/web/src/components/ConfigDialog.vue @@ -9,23 +9,33 @@
- + - + + + + + + - + - + - + - + - + - + {{ form['calls'] }} @@ -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(() => { diff --git a/web/src/components/mobile/ChatPrompt.vue b/web/src/components/mobile/ChatPrompt.vue index 01bda6e9..d009576c 100644 --- a/web/src/components/mobile/ChatPrompt.vue +++ b/web/src/components/mobile/ChatPrompt.vue @@ -73,6 +73,7 @@ onMounted(() => { .content { word-break break-word; + text-align left padding: 5px 10px; background-color: #98E165; color #444444 diff --git a/web/src/main.js b/web/src/main.js index bb1e6fb7..d386f3ad 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -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') diff --git a/web/src/views/ChatPlus.vue b/web/src/views/ChatPlus.vue index a3a5015b..719b2868 100644 --- a/web/src/views/ChatPlus.vue +++ b/web/src/views/ChatPlus.vue @@ -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) => { diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index b33a608d..64484cbb 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -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") }) diff --git a/web/src/views/admin/UserList.vue b/web/src/views/admin/UserList.vue index ec7ee5b1..930f5d07 100644 --- a/web/src/views/admin/UserList.vue +++ b/web/src/views/admin/UserList.vue @@ -38,8 +38,8 @@ @@ -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; diff --git a/web/src/views/mobile/ChatSession.vue b/web/src/views/mobile/ChatSession.vue index 9e071524..47e8b92b 100644 --- a/web/src/views/mobile/ChatSession.vue +++ b/web/src/views/mobile/ChatSession.vue @@ -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; diff --git a/web/src/views/mobile/Home.vue b/web/src/views/mobile/Home.vue index 94b24d4a..2ff621fd 100644 --- a/web/src/views/mobile/Home.vue +++ b/web/src/views/mobile/Home.vue @@ -17,9 +17,20 @@