├── README.md ├── src ├── enum │ └── index.ts ├── plugins │ ├── modules │ │ ├── dict.ts │ │ ├── api.ts │ │ ├── components.ts │ │ └── utils.ts │ └── index.ts ├── settings │ ├── dict │ │ └── index.ts │ └── config │ │ └── http.ts ├── mock │ ├── types │ │ ├── chat │ │ │ ├── chat.dto.ts │ │ │ ├── chat.io.ts │ │ │ └── chat.vo.ts │ │ ├── message │ │ │ ├── message.dto.ts │ │ │ ├── message.io.ts │ │ │ └── message.vo.ts │ │ └── contact │ │ │ └── contact.vo.ts │ ├── index.ts │ └── modules │ │ ├── note.ts │ │ ├── login.ts │ │ ├── contact.ts │ │ ├── chat.ts │ │ └── message.ts ├── css │ ├── dark │ │ └── css-vars.css │ ├── main.css │ └── app.css ├── assets │ ├── logo.png │ ├── male.png │ ├── female.png │ ├── default_avatar.png │ └── svg │ │ ├── more.svg │ │ ├── refresh.svg │ │ ├── notice.svg │ │ ├── right.svg │ │ ├── edit.svg │ │ ├── weixin.svg │ │ ├── github.svg │ │ └── publish.svg ├── view │ ├── album │ │ └── index.vue │ ├── group │ │ └── index.vue │ ├── errors │ │ └── 404 │ │ │ └── index.vue │ ├── note │ │ └── index.vue │ ├── settings │ │ └── index.vue │ ├── friend │ │ └── index.vue │ ├── chat │ │ ├── index.vue │ │ └── components │ │ │ ├── chat-item.vue │ │ │ └── chat-history.vue │ ├── home │ │ └── index.vue │ ├── contacts │ │ ├── index.vue │ │ └── components │ │ │ ├── set-contect-info-dialog.vue │ │ │ ├── contact-detail.vue │ │ │ ├── contact-list.vue │ │ │ └── add-contact-dialog.vue │ ├── login │ │ └── index.vue │ └── register │ │ └── index.vue ├── router │ ├── routes │ │ ├── index.ts │ │ ├── permission │ │ │ ├── index.ts │ │ │ └── modules │ │ │ │ ├── chat.ts │ │ │ │ ├── friends.ts │ │ │ │ ├── note.ts │ │ │ │ ├── album.ts │ │ │ │ ├── settings.ts │ │ │ │ └── contacts.ts │ │ └── basic.ts │ ├── types │ │ └── index.ts │ └── index.ts ├── api │ ├── modules │ │ ├── user.ts │ │ ├── message.ts │ │ ├── login.ts │ │ ├── chat.ts │ │ └── contact.ts │ └── index.ts ├── vite-env.d.ts ├── App.vue ├── hooks │ ├── index.ts │ └── modules │ │ ├── useAsyncComponent.ts │ │ ├── useSocket.ts │ │ ├── useTheme.ts │ │ ├── useCurrentInstance.ts │ │ ├── useLoadMore.ts │ │ └── usePageList.ts ├── layout │ ├── index.vue │ └── themes │ │ └── default │ │ ├── index.vue │ │ └── components │ │ ├── content │ │ └── index.vue │ │ ├── user │ │ └── index.vue │ │ ├── logo │ │ └── index.vue │ │ ├── aside │ │ └── index.vue │ │ └── menu │ │ └── index.vue ├── utils │ ├── helper │ │ ├── common.ts │ │ ├── data.ts │ │ └── is.ts │ └── http │ │ └── index.ts ├── store │ ├── index.ts │ └── modules │ │ ├── chat.ts │ │ └── user.ts ├── main.ts └── components │ ├── Column │ └── index.vue │ ├── SvgRender │ └── index.vue │ └── List │ └── index.vue ├── server ├── .gitignore ├── nodemon.json ├── upload │ └── avatar │ │ ├── v2-19ef12572eab9ccb8effd2bed190c627_1440w.jpg │ │ ├── v2-318306e6f4e7634bab9b028f4b6ad8a7_1440w.jpg │ │ ├── v2-34db10bc84d68357d1b4ebd96a9c3486_1440w.jpg │ │ ├── v2-756c726fc86dcb956d5c3df3e37a94a5_1440w.jpg │ │ ├── v2-863219151c7fd476b36bf3bd331b4c7b_1440w.jpg │ │ ├── v2-b74015573f15ba0e19737d971fe93a6f_1440w.jpg │ │ ├── v2-ba5b73f021a0f245a8b875d77de78b14_1440w.jpg │ │ ├── v2-bf65778ab4e5c18a9f5d11c7e97162f9_1440w.jpg │ │ ├── v2-e5254513e490224150b9b2d69ba1bf51_1440w.jpg │ │ ├── v2-f162a3f5c583e288773041f58028df0d_1440w.jpg │ │ └── v2-f1a6a154adf94f2b3aadc0c9ccf13f5b_1440w.jpg ├── config │ ├── app.js │ ├── db.js │ ├── index.js │ └── user.js ├── .env ├── .env.test ├── .env.uat ├── .env.development ├── .env.production ├── main.js ├── Dockerfile ├── router │ ├── index.js │ └── modules │ │ ├── chat.js │ │ ├── message.js │ │ ├── contact.js │ │ └── user.js ├── README.md ├── db │ └── index.js ├── docker-compose.yml ├── model │ ├── contact.js │ ├── chat.js │ ├── message_read.js │ ├── message_delete.js │ ├── message.js │ └── user.js ├── middleware │ ├── errorHandler.js │ └── tokenAuth.js ├── validate │ ├── chat.js │ ├── contact.js │ ├── message.js │ └── user.js ├── app │ ├── index.js │ └── socket.js ├── package.json ├── utils │ └── common.js ├── controller │ ├── chat.js │ ├── message.js │ ├── contact.js │ └── user.js ├── service │ ├── message.js │ ├── contact.js │ └── user.js └── init.sql ├── .eslintignore ├── .prettierignore ├── .husky └── _ │ ├── commit-msg │ ├── post-commit │ ├── post-merge │ ├── pre-auto-gc │ ├── pre-commit │ ├── pre-push │ ├── pre-rebase │ ├── applypatch-msg │ ├── post-applypatch │ ├── post-checkout │ ├── post-rewrite │ ├── pre-applypatch │ ├── pre-merge-commit │ ├── prepare-commit-msg │ ├── husky.sh │ └── h ├── public ├── img.png └── logo.svg ├── vercel.json ├── tsconfig.json ├── .commitlintrc.cjs ├── .prettierrc ├── index.html ├── .gitignore ├── tsconfig.node.json ├── .github └── workflows │ └── deploy.yml ├── tsconfig.app.json ├── .release-it.json ├── .eslintrc.cjs ├── package.json ├── vite.config.ts └── CHANGELOG.md /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/enum/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/plugins/modules/dict.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/settings/dict/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/mock/types/chat/chat.dto.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/dark/css-vars.css: -------------------------------------------------------------------------------- 1 | html.dark {} -------------------------------------------------------------------------------- /src/mock/types/message/message.dto.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mock/types/message/message.io.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # 忽略格式化文件 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.husky/_/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/post-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/post-merge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/pre-auto-gc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/pre-rebase: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/applypatch-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/post-applypatch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/post-checkout: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/post-rewrite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/pre-applypatch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/pre-merge-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /.husky/_/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/h" -------------------------------------------------------------------------------- /public/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/public/img.png -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["/**/*.js"], 3 | "ignore": ["node_modules"] 4 | } -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/src/assets/male.png -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @custom-variant dark (&:where(.dark, .dark *)); -------------------------------------------------------------------------------- /src/assets/female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/src/assets/female.png -------------------------------------------------------------------------------- /src/assets/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/src/assets/default_avatar.png -------------------------------------------------------------------------------- /src/view/album/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 相册 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/view/group/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 群聊 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/mock/types/chat/chat.io.ts: -------------------------------------------------------------------------------- 1 | export interface IChatListIo { 2 | page: number 3 | pageSize: number 4 | keywords: string 5 | } 6 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import basicRoutes from './basic' 2 | 3 | const routes = [...basicRoutes] 4 | 5 | export default routes 6 | -------------------------------------------------------------------------------- /src/view/errors/404/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/view/note/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 笔记 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/api/(.*)", 5 | "destination": "http://49.232.248.93:3030/" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/view/settings/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 设置 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /server/upload/avatar/v2-19ef12572eab9ccb8effd2bed190c627_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-19ef12572eab9ccb8effd2bed190c627_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-318306e6f4e7634bab9b028f4b6ad8a7_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-318306e6f4e7634bab9b028f4b6ad8a7_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-34db10bc84d68357d1b4ebd96a9c3486_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-34db10bc84d68357d1b4ebd96a9c3486_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-756c726fc86dcb956d5c3df3e37a94a5_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-756c726fc86dcb956d5c3df3e37a94a5_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-863219151c7fd476b36bf3bd331b4c7b_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-863219151c7fd476b36bf3bd331b4c7b_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-b74015573f15ba0e19737d971fe93a6f_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-b74015573f15ba0e19737d971fe93a6f_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-ba5b73f021a0f245a8b875d77de78b14_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-ba5b73f021a0f245a8b875d77de78b14_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-bf65778ab4e5c18a9f5d11c7e97162f9_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-bf65778ab4e5c18a9f5d11c7e97162f9_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-e5254513e490224150b9b2d69ba1bf51_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-e5254513e490224150b9b2d69ba1bf51_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-f162a3f5c583e288773041f58028df0d_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-f162a3f5c583e288773041f58028df0d_1440w.jpg -------------------------------------------------------------------------------- /server/upload/avatar/v2-f1a6a154adf94f2b3aadc0c9ccf13f5b_1440w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZRMYDYCG/go-chat/HEAD/server/upload/avatar/v2-f1a6a154adf94f2b3aadc0c9ccf13f5b_1440w.jpg -------------------------------------------------------------------------------- /src/api/modules/user.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | // 搜索用户列表 4 | export const searchUserList = (params) => { 5 | return http.get('/api/v1/user/searchUser', params) 6 | } 7 | -------------------------------------------------------------------------------- /src/api/modules/message.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | // 获取聊天消息列表 4 | export const getChatMessageList = (params) => { 5 | return http.get('/api/v1/message/list', params) 6 | } 7 | -------------------------------------------------------------------------------- /.husky/_/husky.sh: -------------------------------------------------------------------------------- 1 | echo "husky - DEPRECATED 2 | 3 | Please remove the following two lines from $0: 4 | 5 | #!/usr/bin/env sh 6 | . \"\$(dirname -- \"\$0\")/_/husky.sh\" 7 | 8 | They WILL FAIL in v10.0.0 9 | " -------------------------------------------------------------------------------- /server/config/app.js: -------------------------------------------------------------------------------- 1 | const { 2 | APP_PORT, 3 | DB_HOST 4 | } = process.env 5 | 6 | const app = { 7 | APP_PORT, 8 | DB_HOST, 9 | JWT_SECRET: 'GC_Course$10' 10 | } 11 | 12 | module.exports = app -------------------------------------------------------------------------------- /src/view/friend/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 朋友圈 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | ### 2 | # @Description: 共用配置 3 | ### 4 | 5 | # 项目HTTP服务监听端口 6 | APP_PORT = 3030 7 | 8 | # 数据库参数 9 | DB_HOST = 127.0.0.1 10 | DB_NAME = go_chat 11 | DB_PORT = 3306 12 | DB_USER = root 13 | DB_PASSWORD = 123456 -------------------------------------------------------------------------------- /server/.env.test: -------------------------------------------------------------------------------- 1 | ### 2 | # @Description: 生产环境下配置 3 | ### 4 | 5 | # 项目HTTP服务监听端口 6 | APP_PORT = 3000 7 | 8 | # 数据库参数 9 | DB_HOST = 127.0.0.1 10 | DB_NAME = go_chat 11 | DB_PORT = 3306 12 | DB_USER = root 13 | DB_PASSWORD = 12345678 -------------------------------------------------------------------------------- /server/.env.uat: -------------------------------------------------------------------------------- 1 | ### 2 | # @Description: 生产环境下配置 3 | ### 4 | 5 | # 项目HTTP服务监听端口 6 | APP_PORT = 3000 7 | 8 | # 数据库参数 9 | DB_HOST = 127.0.0.1 10 | DB_NAME = go_chat 11 | DB_PORT = 3306 12 | DB_USER = root 13 | DB_PASSWORD = 12345678 -------------------------------------------------------------------------------- /server/.env.development: -------------------------------------------------------------------------------- 1 | ### 2 | # @Description: 开发环境下配置 3 | ### 4 | 5 | # 项目HTTP服务监听端口 6 | APP_PORT = 3030 7 | 8 | # 数据库参数 9 | DB_HOST = 127.0.0.1 10 | DB_NAME = go_chat 11 | DB_PORT = 3306 12 | DB_USER = root 13 | DB_PASSWORD = 123456 -------------------------------------------------------------------------------- /src/mock/types/chat/chat.vo.ts: -------------------------------------------------------------------------------- 1 | export interface IChatListVo { 2 | user_id: number 3 | nickname: string 4 | slogan: string 5 | avatar: string 6 | last_message: string 7 | send_time: string 8 | create_time: string 9 | } 10 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /server/.env.production: -------------------------------------------------------------------------------- 1 | ### 2 | # @Description: 生产环境下配置 3 | ### 4 | 5 | # 项目HTTP服务监听端口 6 | APP_PORT = 3000 7 | 8 | # 数据库参数 9 | DB_HOST = llm-mini-chat-mysql 10 | DB_NAME = go_chat 11 | DB_PORT = 3306 12 | DB_USER = root 13 | DB_PASSWORD = 12345678 -------------------------------------------------------------------------------- /src/mock/types/contact/contact.vo.ts: -------------------------------------------------------------------------------- 1 | export interface IContactVo { 2 | user_id: number 3 | account: string 4 | nickname: string 5 | remark: string 6 | desc: string 7 | sex: string 8 | avatar: string 9 | create_time: string 10 | } 11 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | const app = require('./app') 2 | const appConfig = require('./config/app') 3 | 4 | const { 5 | APP_PORT, 6 | } = appConfig 7 | 8 | app.listen(APP_PORT, () => { 9 | console.log(`server is running on http://localhost:${APP_PORT}`) 10 | }) 11 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/config/db.js: -------------------------------------------------------------------------------- 1 | const { 2 | DB_HOST, 3 | DB_NAME, 4 | DB_PORT, 5 | DB_USER, 6 | DB_PASSWORD, 7 | } = process.env 8 | 9 | const db = { 10 | DB_HOST, 11 | DB_NAME, 12 | DB_PORT, 13 | DB_USER, 14 | DB_PASSWORD, 15 | } 16 | 17 | module.exports = db 18 | -------------------------------------------------------------------------------- /src/mock/types/message/message.vo.ts: -------------------------------------------------------------------------------- 1 | export interface IMessageListVo { 2 | id: number 3 | user_id: number 4 | content: string 5 | receiver_id: number 6 | avatar: string 7 | is_me: boolean 8 | status: number 9 | send_time: string 10 | create_time: string 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modules/useAsyncComponent.ts' 2 | export * from './modules/useCurrentInstance.ts' 3 | export * from './modules/useLoadMore.ts' 4 | export * from './modules/usePageList.ts' 5 | export * from './modules/useSocket.ts' 6 | export * from './modules/useTheme.ts' 7 | -------------------------------------------------------------------------------- /.commitlintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | ['build', 'ci', 'chore', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'addLog'], 8 | ], 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | const api = {} 2 | const apiFiles = import.meta.glob('./modules/*.ts', { eager: true }) 3 | 4 | Object.entries(apiFiles).forEach(([path, module]) => { 5 | const fileName = (path.match(/\/(\w+)\./) as RegExpMatchArray)[1] 6 | api[fileName] = module 7 | }) 8 | 9 | export default api 10 | -------------------------------------------------------------------------------- /src/api/modules/login.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | // 账号注册 4 | export const accountRegister = (data) => { 5 | return http.post('/api/v1/user/register', data) 6 | } 7 | 8 | // 账号密码登录 9 | export const accountLogin = (data) => { 10 | return http.post('/api/v1/user/login', data) 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "plugins": [ 10 | "prettier-plugin-organize-imports", 11 | "prettier-plugin-tailwindcss" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | # 指定基础镜像 2 | FROM node:alpine 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 将 package.json 复制到容器中 8 | COPY package.json ./ 9 | 10 | # 使用 npm 安装依赖 11 | RUN npm install 12 | 13 | # 将项目的所有文件复制到容器中 14 | COPY . . 15 | 16 | # 暴露应用的端口 17 | EXPOSE 3030 18 | 19 | # 启动应用 20 | CMD ["node", "main.js"] 21 | -------------------------------------------------------------------------------- /src/router/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IRouteMeta { 2 | icon: string 3 | activeIcon: string 4 | title: string 5 | } 6 | 7 | export interface IRoute { 8 | name?: string 9 | path: string 10 | component?: () => Promise 11 | meta?: IRouteMeta 12 | redirect?: string 13 | children?: IRoute[] 14 | } 15 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /server/router/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const Router = require('koa-router') 3 | 4 | const router = new Router({ 5 | prefix: '/api/v1' 6 | }) 7 | 8 | fs.readdirSync(`${__dirname}/modules`).forEach((file) => { 9 | const module = require(`./modules/${file}`) 10 | router.use(module.routes()) 11 | }) 12 | 13 | module.exports = router -------------------------------------------------------------------------------- /src/view/chat/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const dotenv = require('dotenv') 3 | const minimist = require('minimist') 4 | const argv = minimist(process.argv.slice(2)) 5 | 6 | dotenv.config({ 7 | path: path.resolve(__dirname, '../.env'), 8 | }) 9 | 10 | dotenv.config({ 11 | path: path.resolve(__dirname, `../.env.${argv.mode}`), 12 | }) 13 | -------------------------------------------------------------------------------- /src/utils/helper/common.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_HOST } from '@/settings/config/http' 2 | 3 | /** 4 | * 非处理数据的功能性工具汇总 5 | * * * formatServerFilePath 补全完善后的服务器文件访问路径 6 | * */ 7 | export const formatServerFilePath = (src) => { 8 | if (!src) { 9 | return '' 10 | } 11 | return /^https*:\/\//.test(src) ? src : `${HTTP_HOST}upload/avatar/${src}` 12 | } 13 | -------------------------------------------------------------------------------- /src/router/routes/permission/index.ts: -------------------------------------------------------------------------------- 1 | import type { IRoute } from '../../types/index.ts' 2 | 3 | const routes: IRoute[] = [] 4 | 5 | const routesFiles = import.meta.glob('./modules/*.ts', { eager: true }) as Record 6 | 7 | Object.entries(routesFiles).forEach(([, module]) => { 8 | routes.push(module.default) 9 | }) 10 | 11 | export default routes 12 | -------------------------------------------------------------------------------- /src/settings/config/http.ts: -------------------------------------------------------------------------------- 1 | // 请求地址 2 | export const HTTP_HOST = 'http://49.232.248.93:3030/' 3 | 4 | // 请求超时时长 5 | export const HTTP_TIMEOUT = 5000 6 | 7 | // 业务处理成功状态码 8 | export const HTTP_SUCCESS_CODE = 200 9 | 10 | // 登录状态失效状态码 11 | export const HTTP_TOKEN_ERROR_CODE = 400 12 | 13 | export const HTTP_CODE = { 14 | HTTP_SUCCESS_CODE, 15 | HTTP_TOKEN_ERROR_CODE, 16 | } 17 | -------------------------------------------------------------------------------- /src/layout/themes/default/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 插件注册 3 | * @param {App} app - Vue 应用实例 4 | */ 5 | 6 | import type { App, Plugin } from 'vue' 7 | 8 | const modules: Record = import.meta.glob('./modules/*.ts', { 9 | eager: true, 10 | }) 11 | 12 | export const registerPlugins = (app: App) => { 13 | Object.values(modules).forEach((module) => { 14 | app.use(module.default) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/layout/themes/default/components/content/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | llm-mini-chat 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/utils/helper/data.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | /** 4 | * 数据处理工具汇总 5 | * * * formatDate 日期格式化 6 | * */ 7 | 8 | /** 9 | * 日期格式化字符串形式 10 | * @param timestamp 日期时间戳 11 | * @param format 模式 12 | * @returns 格式化时间String 13 | */ 14 | export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => { 15 | if (!date) { 16 | return '' 17 | } 18 | 19 | return moment(date).format(format) 20 | } 21 | -------------------------------------------------------------------------------- /src/router/routes/permission/modules/chat.ts: -------------------------------------------------------------------------------- 1 | const main = { 2 | name: 'ChatMain', 3 | path: '/chat/main', 4 | component: () => import('@/view/chat/index.vue'), 5 | meta: { 6 | icon: 'ri-message-2-line', 7 | activeIcon: 'ri-message-2-fill', 8 | title: '聊天', 9 | }, 10 | } 11 | 12 | const chat = { 13 | path: '/chat', 14 | redirect: '/chat/main', 15 | children: [main], 16 | } 17 | 18 | export default chat 19 | -------------------------------------------------------------------------------- /src/api/modules/chat.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | // 创建聊天记录 4 | export const createChat = (params) => { 5 | return http.post('/api/v1/chat/create', params) 6 | } 7 | 8 | // 获取聊天记录列表 9 | export const getChatHistoryList = (params) => { 10 | return http.get('/api/v1/chat/list', params) 11 | } 12 | 13 | // 删除聊天记录 14 | export const deleteChat = (params) => { 15 | return http.delete('/api/v1/chat/delete', params) 16 | } 17 | -------------------------------------------------------------------------------- /src/router/routes/permission/modules/friends.ts: -------------------------------------------------------------------------------- 1 | const main = { 2 | name: 'FriendMain', 3 | path: '/friend/main', 4 | component: () => import('@/view/friend/index.vue'), 5 | meta: { 6 | icon: 'ri-settings-line', 7 | activeIcon: 'ri-settings-fill', 8 | title: '朋友圈', 9 | }, 10 | } 11 | 12 | const settings = { 13 | path: '/friend', 14 | redirect: '/friend/main', 15 | children: [main], 16 | } 17 | 18 | export default settings 19 | -------------------------------------------------------------------------------- /src/router/routes/permission/modules/note.ts: -------------------------------------------------------------------------------- 1 | const main = { 2 | name: 'NoteMain', 3 | path: '/note/main', 4 | component: () => import('@/view/note/index.vue'), 5 | meta: { 6 | icon: 'ri-contacts-book-3-line', 7 | activeIcon: 'ri-contacts-book-3-fill', 8 | title: '笔记', 9 | }, 10 | } 11 | 12 | const contacts = { 13 | path: '/file', 14 | redirect: '/note/main', 15 | children: [main], 16 | } 17 | 18 | export default contacts 19 | -------------------------------------------------------------------------------- /src/router/routes/permission/modules/album.ts: -------------------------------------------------------------------------------- 1 | const main = { 2 | name: 'AlbumMain', 3 | path: '/album/main', 4 | component: () => import('@/view/album/index.vue'), 5 | meta: { 6 | icon: 'ri-contacts-book-3-line', 7 | activeIcon: 'ri-contacts-book-3-fill', 8 | title: '相册', 9 | }, 10 | } 11 | 12 | const contacts = { 13 | path: '/album', 14 | redirect: '/album/main', 15 | children: [main], 16 | } 17 | 18 | export default contacts 19 | -------------------------------------------------------------------------------- /src/router/routes/permission/modules/settings.ts: -------------------------------------------------------------------------------- 1 | const main = { 2 | name: 'SettingsMain', 3 | path: '/settings/main', 4 | component: () => import('@/view/settings/index.vue'), 5 | meta: { 6 | icon: 'ri-settings-line', 7 | activeIcon: 'ri-settings-fill', 8 | title: '设置', 9 | }, 10 | } 11 | 12 | const settings = { 13 | path: '/settings', 14 | redirect: '/settings/main', 15 | children: [main], 16 | } 17 | 18 | export default settings 19 | -------------------------------------------------------------------------------- /src/router/routes/permission/modules/contacts.ts: -------------------------------------------------------------------------------- 1 | const main = { 2 | name: 'ContactsMain', 3 | path: '/contacts/main', 4 | component: () => import('@/view/contacts/index.vue'), 5 | meta: { 6 | icon: 'ri-contacts-book-3-line', 7 | activeIcon: 'ri-contacts-book-3-fill', 8 | title: '通讯录', 9 | }, 10 | } 11 | 12 | const contacts = { 13 | path: '/contacts', 14 | redirect: '/contacts/main', 15 | children: [main], 16 | } 17 | 18 | export default contacts 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # 自动注册ElementPlus组件插件生成的文件 27 | auto-imports.d.ts 28 | components.d.ts 29 | 30 | # 构建的产物 31 | stats.html -------------------------------------------------------------------------------- /src/view/home/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | lightChat 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/mock/index.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs' 2 | 3 | const startMock = () => { 4 | Mock.setup({ 5 | timeout: '300-500', 6 | }) 7 | const modules = import.meta.glob('./modules/*.ts', { eager: true }) 8 | Object.entries(modules).forEach(([, module]) => { 9 | ;(module as { default: () => void }).default() 10 | }) 11 | } 12 | 13 | const initMock = (development: boolean): void => { 14 | if (development === false) { 15 | return 16 | } 17 | 18 | startMock() 19 | } 20 | 21 | export default initMock 22 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { useUserStore } from '@/store/modules/user.ts' 2 | import { createPinia } from 'pinia' 3 | 4 | const pinia = createPinia() 5 | 6 | export const registerStore = (app) => { 7 | app.use(pinia) 8 | 9 | const userStore = useUserStore() 10 | const user = localStorage.getItem('user') 11 | 12 | if (user) { 13 | userStore.updateUserInfo(JSON.parse(user)) 14 | } 15 | 16 | const token = localStorage.getItem('token') 17 | if (token) { 18 | userStore.updateToken(token) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/modules/api.ts: -------------------------------------------------------------------------------- 1 | import api from '@/api' 2 | import * as HTTP from '@/settings/config/http' 3 | import type { App } from 'vue' 4 | 5 | const apiPlugin = { 6 | install(app: App): void { 7 | app.config.globalProperties.$api = api 8 | app.provide('$api', api) 9 | 10 | app.config.globalProperties.$HTTP = HTTP 11 | app.provide('$HTTP', HTTP) 12 | 13 | app.config.globalProperties.$HTTP_CODE = HTTP.HTTP_CODE 14 | app.provide('$HTTP_CODE', HTTP.HTTP_CODE) 15 | }, 16 | } 17 | export default apiPlugin 18 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | llm-mini-caht 4 | 5 | 6 | 🙎 一勺 | 💻 开发爱好者 | 🛸 Jiang Su , China 7 | 8 | -------------------------------------------------------------------------------- /server/router/modules/chat.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | const tokenAuth = require('../../middleware/tokenAuth') 3 | const chatController = require('../../controller/chat') 4 | 5 | const chatRouter = new Router({ 6 | prefix: '/chat' 7 | }) 8 | 9 | // 需要做tokenAuth等中间件验证的路由放在此处下面定义,否则在上面定义 10 | chatRouter.use(tokenAuth) 11 | 12 | // 获取聊天列表 13 | chatRouter.get('/list', chatController.list) 14 | 15 | // 添加聊天 16 | chatRouter.post('/create', chatController.create) 17 | 18 | // 操作-删除聊天 19 | chatRouter.delete('/delete', chatController.delete) 20 | 21 | module.exports = chatRouter 22 | -------------------------------------------------------------------------------- /src/api/modules/contact.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | // 添加联系人(好友) 4 | export const createContact = (params) => { 5 | return http.post('/api/v1/contact/create', params) 6 | } 7 | 8 | // 获取联系人列表 9 | export const getContactList = (params) => { 10 | return http.get('/api/v1/contact/list', params) 11 | } 12 | 13 | // 设置联系人备注、描述 14 | export const setContactInfo = (params) => { 15 | return http.patch('/api/v1/contact/setInfo', params) 16 | } 17 | 18 | // 删除联系人(好友) 19 | export const deleteContact = (params) => { 20 | return http.delete('/api/v1/contact/delete', params) 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/modules/components.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | 3 | interface VueComponent { 4 | name?: string 5 | } 6 | 7 | interface Module { 8 | default: VueComponent 9 | } 10 | 11 | const componentPlugin = { 12 | install(app: App) { 13 | const modules = import.meta.glob('@/components/**/*.vue', { 14 | eager: true, 15 | }) as Record 16 | 17 | Object.entries(modules).forEach(([, module]) => { 18 | if (module.default.name) { 19 | app.component(module.default.name, module.default) 20 | } 21 | }) 22 | }, 23 | } 24 | 25 | export default componentPlugin 26 | -------------------------------------------------------------------------------- /src/plugins/modules/utils.ts: -------------------------------------------------------------------------------- 1 | import * as common from '@/utils/helper/common' 2 | import * as data from '@/utils/helper/data' 3 | import * as is from '@/utils/helper/is' 4 | import type { App } from 'vue' 5 | 6 | const utilsPlugin = { 7 | install(app: App) { 8 | // common 9 | app.config.globalProperties.$common = common 10 | app.provide('$common', common) 11 | 12 | // is 13 | app.config.globalProperties.$is = is 14 | app.provide('$is', is) 15 | 16 | // data 17 | app.config.globalProperties.$dataHelpers = data 18 | app.provide('$dataHelpers', data) 19 | }, 20 | } 21 | 22 | export default utilsPlugin 23 | -------------------------------------------------------------------------------- /server/router/modules/message.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | const tokenAuth = require('../../middleware/tokenAuth') 3 | const messageController = require('../../controller/message') 4 | 5 | const messageRouter = new Router({ 6 | prefix: '/message' 7 | }) 8 | 9 | // 需要做tokenAuth等中间件验证的路由放在此处下面定义,否则在上面定义 10 | messageRouter.use(tokenAuth) 11 | 12 | // 获取聊天消息列表 13 | messageRouter.get('/list', messageController.list) 14 | 15 | // 操作-删除聊天消息 16 | // messageRouter.delete('/delete', messageController.delete) 17 | 18 | // 设置为已读 19 | // messageRouter.patch('/read', messageController.read) 20 | 21 | 22 | module.exports = messageRouter 23 | -------------------------------------------------------------------------------- /.husky/_/h: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | [ "$HUSKY" = "2" ] && set -x 3 | n=$(basename "$0") 4 | s=$(dirname "$(dirname "$0")")/$n 5 | 6 | [ ! -f "$s" ] && exit 0 7 | 8 | if [ -f "$HOME/.huskyrc" ]; then 9 | echo "husky - '~/.huskyrc' is DEPRECATED, please move your code to ~/.config/husky/init.sh" 10 | fi 11 | i="${XDG_CONFIG_HOME:-$HOME/.config}/husky/init.sh" 12 | [ -f "$i" ] && . "$i" 13 | 14 | [ "${HUSKY-}" = "0" ] && exit 0 15 | 16 | export PATH="node_modules/.bin:$PATH" 17 | sh -e "$s" "$@" 18 | c=$? 19 | 20 | [ $c != 0 ] && echo "husky - $n script failed (code $c)" 21 | [ $c = 127 ] && echo "husky - command not found in PATH=$PATH" 22 | exit $c 23 | -------------------------------------------------------------------------------- /server/config/user.js: -------------------------------------------------------------------------------- 1 | // 用户注册默认头像 2 | const DEFAULT_AVATERS = [ 3 | 'v2-19ef12572eab9ccb8effd2bed190c627_1440w.jpg', 4 | 'v2-34db10bc84d68357d1b4ebd96a9c3486_1440w.jpg', 5 | 'v2-756c726fc86dcb956d5c3df3e37a94a5_1440w.jpg', 6 | 'v2-318306e6f4e7634bab9b028f4b6ad8a7_1440w.jpg', 7 | 'v2-863219151c7fd476b36bf3bd331b4c7b_1440w.jpg', 8 | 'v2-b74015573f15ba0e19737d971fe93a6f_1440w.jpg', 9 | 'v2-ba5b73f021a0f245a8b875d77de78b14_1440w.jpg', 10 | 'v2-bf65778ab4e5c18a9f5d11c7e97162f9_1440w.jpg', 11 | 'v2-f1a6a154adf94f2b3aadc0c9ccf13f5b_1440w.jpg', 12 | 'v2-f162a3f5c583e288773041f58028df0d_1440w.jpg' 13 | ] 14 | 15 | module.exports = { 16 | DEFAULT_AVATERS 17 | } 18 | -------------------------------------------------------------------------------- /server/db/index.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | const { 3 | DB_HOST, 4 | DB_NAME, 5 | DB_USER, 6 | DB_PASSWORD, 7 | } = require('../config/db') 8 | 9 | const sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASSWORD, { 10 | host: DB_HOST, 11 | dialect: 'mysql', 12 | timezone: '+08:00', 13 | define: { 14 | schema: 'gc', 15 | schemaDelimiter: '_' 16 | }, 17 | retry: { 18 | max: 10, // 最大重试次数 19 | timeout: 5000, // 每次重试的间隔时间(毫秒) 20 | }, 21 | }) 22 | 23 | sequelize 24 | .authenticate() 25 | .then(() => { 26 | console.log('数据库连接成功') 27 | }) 28 | .catch(err => { 29 | console.log('数据库连接失败', err) 30 | }) 31 | 32 | 33 | module.exports = sequelize 34 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /server/router/modules/contact.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | const tokenAuth = require('../../middleware/tokenAuth') 3 | const contactController = require('../../controller/contact') 4 | 5 | const contactRouter = new Router({ 6 | prefix: '/contact' 7 | }) 8 | 9 | // 需要做tokenAuth等中间件验证的路由放在此处下面定义,否则在上面定义 10 | contactRouter.use(tokenAuth) 11 | 12 | // 获取联系人列表 13 | contactRouter.get('/list', contactController.list) 14 | 15 | // 添加联系人(好友) 16 | contactRouter.post('/create', contactController.create) 17 | 18 | // 联系人资料设置 19 | contactRouter.patch('/setInfo', contactController.setInfo) 20 | 21 | // 操作-删除联系人 22 | contactRouter.delete('/delete', contactController.delete) 23 | 24 | 25 | module.exports = contactRouter 26 | -------------------------------------------------------------------------------- /server/router/modules/user.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router') 2 | const tokenAuth = require('../../middleware/tokenAuth') 3 | const userController = require('../../controller/user') 4 | 5 | const userRouter = new Router({ 6 | prefix: '/user' 7 | }) 8 | 9 | // 注册用户 10 | userRouter.post('/register', userController.register) 11 | 12 | // 用户登录 13 | userRouter.post('/login', userController.login) 14 | 15 | // 需要做tokenAuth等中间件验证的路由放在此处下面定义,否则在上面定义 16 | userRouter.use(tokenAuth) 17 | 18 | // 上传用户图像 19 | userRouter.patch('/uploadAvater', userController.uploadAvater) 20 | 21 | // 更新用户信息 22 | userRouter.patch('/updateInfo', userController.updateInfo) 23 | 24 | // 搜索用户列表 25 | userRouter.get('/searchUser', userController.searchUser) 26 | 27 | module.exports = userRouter -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | /* 根元素相关样式表 */ 2 | html body::-webkit-scrollbar, 3 | #app::-webkit-scrollbar { 4 | width: 8px; 5 | height: 8px; 6 | } 7 | 8 | html body::-webkit-scrollbar-thumb, 9 | #app::-webkit-scrollbar-thumb { 10 | background-color: #eaeaeb; 11 | border: 3px solid transparent; 12 | border-radius: 7px; 13 | } 14 | 15 | html body::-webkit-scrollbar-thumb:hover, 16 | #app::-webkit-scrollbar-thumb:hover { 17 | background-color: #d4d5d6; 18 | } 19 | 20 | body { 21 | height: 100vh; 22 | } 23 | 24 | #app { 25 | width: 100vw; 26 | height: 100vh; 27 | } 28 | 29 | /* 包裹容器样式表 */ 30 | .page-wrapper { 31 | padding: 0 15px; 32 | } 33 | 34 | /* ElementPlus样式覆盖表 */ 35 | .el-scrollbar .el-scrollbar__bar.is-vertical { 36 | width: 8px; 37 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Remote Server 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Deploy to Remote Server 14 | uses: appleboy/ssh-action@master 15 | with: 16 | host: ${{ secrets.SERVER_IP }} 17 | username: ${{ secrets.SSH_USER }} 18 | key: ${{ secrets.SSH_PRIVATE_KEY }} 19 | script: | 20 | set -e # 如果任何命令失败,立即退出 21 | cd ~/lightChat/web # 进入项目目录 22 | git clone git@github.com:ZRMYDYCG/llm-go-chat-client.git # 克隆项目到服务器 23 | git pull # 拉取最新代码 24 | sudo docker compose down --rmi all # 停止并删除所有容器 25 | sudo docker compose up -d # 构建并启动容器 26 | -------------------------------------------------------------------------------- /src/utils/http/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_TIMEOUT } from '@/settings/config/http' 2 | import { useUserStore } from '@/store/modules/user' 3 | import axios from 'axios' 4 | 5 | // 创建http axios实例 6 | const http = axios.create({ 7 | // baseURL: HTTP_HOST, 8 | timeout: HTTP_TIMEOUT, 9 | }) 10 | 11 | // 请求拦截 12 | http.interceptors.request.use( 13 | function (config) { 14 | const userStore = useUserStore() 15 | config.headers.Authorization = userStore.token 16 | return config 17 | }, 18 | function (error) { 19 | return Promise.reject(error) 20 | }, 21 | ) 22 | 23 | // 响应拦截 24 | http.interceptors.response.use( 25 | function (response) { 26 | return response 27 | }, 28 | function (error) { 29 | return Promise.reject(error) 30 | }, 31 | ) 32 | 33 | export default http 34 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import App from '@/App.vue' 2 | import { registerPlugins } from '@/plugins' 3 | import { registerRouter } from '@/router' 4 | import { registerStore } from '@/store' 5 | // import 'element-plus/theme-chalk/dark/css-vars.css' 6 | import { createApp } from 'vue' 7 | import './css/app.css' 8 | // import './css/dark/css-vars.css' 9 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 10 | import './css/main.css' 11 | 12 | import mock from './mock' 13 | 14 | mock(false) 15 | 16 | function bootstrap() { 17 | const app = createApp(App) 18 | registerStore(app) 19 | registerRouter(app) 20 | registerPlugins(app) 21 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 22 | app.component(key, component) 23 | } 24 | 25 | app.mount('#app') 26 | } 27 | 28 | bootstrap() 29 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", // 设置解析非相对模块名称的基本目录,默认为当前目录 4 | "paths": { 5 | "@/*": ["src/*"] // 定义模块名称到基于 baseUrl 的路径映射,这里将 @ 符号映射到 src 目录 6 | }, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", // 指定 TypeScript 构建信息文件的路径 8 | /* Linting */ 9 | "strict": true, // 启用所有严格类型检查选项 10 | "noUnusedLocals": false, // 禁止未使用的局部变量(这里改为 false) 11 | "noUnusedParameters": false, // 禁止未使用的参数(这里改为 false) 12 | "noFallthroughCasesInSwitch": true, // 禁止 switch 语句中的穿透情况(即 case 语句没有 break) 13 | "noUncheckedSideEffectImports": true, // 禁止导入具有副作用的模块而不进行类型检查 14 | "noImplicitAny": false // 允许隐式的 any 类型 15 | }, 16 | 17 | 18 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] // 指定包含在编译过程中的文件,这里包括 src 目录下所有 .ts, .tsx 和 .vue 文件 19 | } 20 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | # Koa 应用服务 5 | llm-mini-chat-server: 6 | build: . 7 | container_name: llm-mini-chat-server 8 | ports: 9 | - "3030:3030" 10 | - "3001:3001" 11 | depends_on: 12 | - mysql 13 | networks: 14 | - llm-mini-chat-network 15 | 16 | # MySQL 数据库服务 17 | mysql: 18 | image: mysql 19 | container_name: llm-mini-chat-mysql 20 | environment: 21 | - MYSQL_ROOT_PASSWORD=123456 22 | - MYSQL_DATABASE=go_chat 23 | ports: 24 | - "3307:3306" 25 | volumes: 26 | - llm-mysql-data:/var/lib/mysql # 持久化 MySQL 数据 27 | networks: 28 | - llm-mini-chat-network 29 | 30 | # 定义卷 31 | volumes: 32 | llm-mysql-data: 33 | 34 | # 定义网络 35 | networks: 36 | llm-mini-chat-network: 37 | driver: bridge # 使用桥接驱动 -------------------------------------------------------------------------------- /server/model/contact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 联系人表 3 | */ 4 | const { DataTypes } = require('sequelize') 5 | const db = require('../db') 6 | 7 | const Contact = db.define('contact', { 8 | user_id: { 9 | type: DataTypes.INTEGER(11), 10 | allowNull: false, 11 | comment: '用户id', 12 | }, 13 | reciver_id: { 14 | type: DataTypes.INTEGER(11), 15 | allowNull: false, 16 | comment: '联系人或群id', 17 | }, 18 | remark: { 19 | type: DataTypes.CHAR(50), 20 | allowNull: true, 21 | comment: '联系人备注名', 22 | }, 23 | desc: { 24 | type: DataTypes.CHAR(150), 25 | allowNull: true, 26 | comment: '联系人描述信息', 27 | } 28 | }, { 29 | // 显式指定表名为`contact` 30 | tableName: 'contact', 31 | createdAt: 'created_time', 32 | updatedAt: 'update_time', 33 | }) 34 | 35 | // Contact.sync({ force: true }) 36 | module.exports = Contact -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { useUserStore } from '@/store/modules/user' 2 | import { createRouter, createWebHistory } from 'vue-router' 3 | import routes from './routes' 4 | 5 | /** 6 | * 创建路由器 7 | * */ 8 | const router = createRouter({ 9 | history: createWebHistory(), 10 | routes, 11 | scrollBehavior() { 12 | return { 13 | top: 0, 14 | } 15 | }, 16 | }) 17 | 18 | /** 19 | * 全局前置守卫 20 | * * * whiteRoutes 白名单路由 21 | * */ 22 | const whiteRoutes = ['/register', '/login', '/404'] 23 | 24 | router.beforeEach((to, from, next) => { 25 | const userStore = useUserStore() 26 | const { token } = userStore 27 | if (!whiteRoutes.includes(to.path) && token === '') { 28 | next('/login') 29 | } else { 30 | next() 31 | } 32 | }) 33 | 34 | export const registerRouter = (app) => { 35 | app.use(router) 36 | } 37 | 38 | export default router 39 | -------------------------------------------------------------------------------- /src/assets/svg/more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 错误处理中间件 3 | */ 4 | class CustomerError extends Error { 5 | payload = { 6 | code: 0, 7 | message: 'error', 8 | } 9 | } 10 | class ValidateError extends CustomerError {} 11 | class DataError extends CustomerError {} 12 | 13 | const formValidateErrorHandler = async (context, next) => { 14 | await next().catch((error) => { 15 | if(error instanceof CustomerError) { 16 | const { payload: { text: message, code } } = error 17 | // 表单校验类型错误处理 18 | context.body = { 19 | code, 20 | message, 21 | } 22 | } else { 23 | // 其他未处理类型错误处理 24 | const { message } = error 25 | context.body = { 26 | code: 0, 27 | message, 28 | } 29 | } 30 | }) 31 | } 32 | 33 | const errorHandler = { 34 | ValidateError, 35 | DataError, 36 | formValidateErrorHandler 37 | } 38 | 39 | module.exports = errorHandler 40 | -------------------------------------------------------------------------------- /server/model/chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 聊天表 3 | */ 4 | const { Model, DataTypes } = require("sequelize"); 5 | const db = require("../db"); 6 | 7 | class Chat extends Model {} 8 | 9 | Chat.init( 10 | { 11 | user_id: { 12 | type: DataTypes.INTEGER(11), 13 | allowNull: false, 14 | comment: "用户id", 15 | }, 16 | reciver_id: { 17 | type: DataTypes.INTEGER(11), 18 | allowNull: false, 19 | comment: "接收用户或群id", 20 | }, 21 | type: { 22 | type: DataTypes.ENUM, 23 | values: ["0", "1"], 24 | allowNull: false, 25 | defaultValue: "0", 26 | comment: "聊天类型(0: 1v1、1群聊)", 27 | }, 28 | }, 29 | { 30 | sequelize: db, 31 | // 模型默认名 32 | modelName: "chat", 33 | // 显式指定表名为`chat` 34 | tableName: "chat", 35 | createdAt: "created_time", 36 | updatedAt: "update_time", 37 | } 38 | ); 39 | 40 | // Chat.sync({ force: true }) 41 | module.exports = Chat; 42 | -------------------------------------------------------------------------------- /server/validate/chat.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * 聊天相关表单数据校验规则rules 3 | */ 4 | 5 | // 添加聊天校验规则 6 | const createChatRules = { 7 | reciver_id: [{ 8 | type: 'string', 9 | required: true, 10 | message: { 11 | text: '好友id必须', 12 | code: 400001001 13 | }, 14 | }], 15 | } 16 | 17 | // 删除聊天校验规则 18 | const deleteChatRules = { 19 | chat_id: [{ 20 | type: 'string', 21 | required: true, 22 | message: { 23 | text: '聊天id必须', 24 | code: 400002001 25 | }, 26 | }], 27 | reciver_id: [{ 28 | type: 'string', 29 | required: true, 30 | message: { 31 | text: '接收人id必须', 32 | code: 400002002 33 | }, 34 | }], 35 | user_id: [{ 36 | type: 'number', 37 | required: true, 38 | message: { 39 | text: '用户id必须', 40 | code: 400002003 41 | }, 42 | }], 43 | } 44 | 45 | const chatRules = { 46 | createChatRules, 47 | deleteChatRules, 48 | } 49 | 50 | module.exports = chatRules 51 | -------------------------------------------------------------------------------- /src/store/modules/chat.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { computed, ref } from 'vue' 3 | 4 | const useChatStore = defineStore('chatStore', () => { 5 | // 聊天记录列表 6 | const chatList = ref([]) 7 | const updateChatList = (list: any[]) => { 8 | chatList.value = list 9 | } 10 | 11 | // 正在进行中的聊天id 12 | const activeChatId = ref() 13 | const updateActiveChatId = (id: number) => { 14 | activeChatId.value = id 15 | } 16 | 17 | // 正在进行中的聊天 18 | const activeChat = computed(() => { 19 | const index = chatList.value.findIndex((chat) => { 20 | return chat.reciver_id === activeChatId.value 21 | }) 22 | 23 | if (index > -1) { 24 | return chatList.value[index] 25 | } else { 26 | return null 27 | } 28 | }) 29 | 30 | return { 31 | chatList, 32 | updateChatList, 33 | activeChatId, 34 | updateActiveChatId, 35 | activeChat, 36 | } 37 | }) 38 | 39 | export default useChatStore 40 | -------------------------------------------------------------------------------- /server/app/index.js: -------------------------------------------------------------------------------- 1 | require('../config') 2 | const Koa = require('koa') 3 | require('./socket') 4 | const path = require('path') 5 | const KoaBody = require('koa-body') 6 | const router = require('../router') 7 | const { formValidateErrorHandler: formValidateErrorHandlerMiddleware } = require('../middleware/errorHandler') 8 | 9 | const serveStatic = require('koa-static') 10 | const mount = require('koa-mount') 11 | 12 | const app = new Koa() 13 | 14 | app.use( 15 | KoaBody.default({ 16 | multipart: true, 17 | formidable: { 18 | uploadDir: path.join(__dirname, '../upload'), 19 | keepExtensions: true, 20 | }, 21 | parsedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'], 22 | returnRawBody: true, 23 | }), 24 | ) 25 | 26 | // 统一错误处理 27 | app.use(formValidateErrorHandlerMiddleware) 28 | 29 | // 注册路由 30 | app.use(router.routes()) 31 | 32 | // 静态资源访问 33 | app.use(mount('/upload', serveStatic(path.join(__dirname, '../upload')))) 34 | 35 | module.exports = app 36 | -------------------------------------------------------------------------------- /src/layout/themes/default/components/user/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | {{ nickname ? nickname : '未登录' }} 16 | 17 | 18 | 19 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/hooks/modules/useAsyncComponent.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@/utils/helper/is' 2 | import type { DefineComponent } from 'vue' 3 | import { defineAsyncComponent } from 'vue' 4 | 5 | interface AsyncComponentOptions { 6 | component: () => Promise 7 | wait?: () => Promise 8 | timeout?: number 9 | } 10 | 11 | export const useAsyncComponent = (options: AsyncComponentOptions): { AsyncComponent: DefineComponent } => { 12 | const { component, wait, timeout } = options 13 | 14 | const loadDelay = async (wait: () => Promise) => { 15 | if (!isFunction(wait)) { 16 | throw new Error(`wait: ${wait}需要为函数`) 17 | } 18 | await wait() 19 | } 20 | 21 | const AsyncComponent = defineAsyncComponent({ 22 | // 加载函数 23 | loader: async () => { 24 | if (wait) { 25 | await loadDelay(wait) 26 | } 27 | return component() 28 | }, 29 | timeout, 30 | }) 31 | 32 | return { 33 | AsyncComponent, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/svg/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/validate/contact.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * 联系人相关表单数据校验规则rules 3 | */ 4 | 5 | // 添加好友校验规则 6 | const createContactRules = { 7 | reciver_id: [{ 8 | type: 'string', 9 | required: true, 10 | message: { 11 | text: '好友id必须', 12 | code: 300001001 13 | }, 14 | }], 15 | } 16 | 17 | // 删除联系人校验规则 18 | const deleteContactRules = { 19 | id: [{ 20 | type: 'string', 21 | required: true, 22 | message: { 23 | text: 'id必须', 24 | code: 300001004 25 | }, 26 | }], 27 | } 28 | 29 | // 联系人资料设置校验规则 30 | const setInfoRules = { 31 | id: [{ 32 | type: 'string', 33 | required: true, 34 | message: { 35 | text: 'id必须', 36 | code: 300001005 37 | }, 38 | }], 39 | user_id: [{ 40 | type: 'string', 41 | required: true, 42 | message: { 43 | text: '用户id必须', 44 | code: 300001006 45 | }, 46 | }], 47 | } 48 | 49 | 50 | const contactRules = { 51 | createContactRules, 52 | deleteContactRules, 53 | setInfoRules, 54 | } 55 | 56 | module.exports = contactRules 57 | -------------------------------------------------------------------------------- /src/hooks/modules/useSocket.ts: -------------------------------------------------------------------------------- 1 | import { useUserStore } from '@/store/modules/user.ts' 2 | import { ElMessage } from 'element-plus' 3 | import { io, Socket } from 'socket.io-client' 4 | import { watch } from 'vue' 5 | 6 | let socket: Socket | null = null 7 | 8 | export const useSocket = () => { 9 | const userStore = useUserStore() 10 | 11 | const connectSocket = () => { 12 | watch( 13 | () => userStore.token, 14 | (token) => { 15 | if (token) { 16 | socket = io('http://localhost:3001', { 17 | auth: { 18 | token, 19 | }, 20 | }) 21 | 22 | // 全局消息监听 23 | socket.on('server:global-error-message', ({ message }: { message: string }) => { 24 | ElMessage.error({ 25 | message, 26 | duration: 3000, 27 | }) 28 | }) 29 | } 30 | }, 31 | { 32 | immediate: true, 33 | }, 34 | ) 35 | } 36 | 37 | return { 38 | socket, 39 | connectSocket, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/validate/message.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * 聊天消息相关表单数据校验规则rules 3 | */ 4 | //获取聊天消息列表校验规则 5 | const getMessageListRules = { 6 | reciver_id: [{ 7 | type: 'string', 8 | required: true, 9 | message: { 10 | text: '接收人id必须', 11 | code: 200001001 12 | }, 13 | }], 14 | } 15 | 16 | // 删除聊天消息校验规则 17 | const deleteMessageRules = { 18 | id: [{ 19 | type: 'string', 20 | required: true, 21 | message: { 22 | text: '聊天消息id必须', 23 | code: 200002001 24 | }, 25 | }], 26 | } 27 | 28 | // 操作聊天消息状态校验规则 29 | const updateMessageReadRules = { 30 | id: [{ 31 | type: 'string', 32 | required: true, 33 | message: { 34 | text: '聊天消息id必须', 35 | code: 200003001 36 | }, 37 | }], 38 | status: [{ 39 | type: 'string', 40 | required: true, 41 | message: { 42 | text: '聊天消息状态必须', 43 | code: 200003002 44 | }, 45 | }], 46 | } 47 | 48 | 49 | const messageRules = { 50 | getMessageListRules, 51 | deleteMessageRules, 52 | updateMessageReadRules 53 | } 54 | 55 | module.exports = messageRules 56 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "serve:dev": "node main.js --mode development", 8 | "serve:test": "node main.js --mode test", 9 | "serve:uat": "node main.js --mode uat", 10 | "serve:prod": "node main.js --mode production", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "amqplib": "^0.10.4", 17 | "async-validator": "^4.2.5", 18 | "bcrypt": "^5.1.1", 19 | "dotenv": "^16.4.5", 20 | "jsonwebtoken": "^9.0.2", 21 | "koa": "^2.15.3", 22 | "koa-body": "^6.0.1", 23 | "koa-mount": "^4.0.0", 24 | "koa-qs": "^3.0.0", 25 | "koa-router": "^8.0.8", 26 | "koa-static": "^5.0.0", 27 | "lodash": "^4.17.21", 28 | "minimist": "^1.2.8", 29 | "moment": "^2.30.1", 30 | "mysql2": "^3.11.0", 31 | "sequelize": "^6.37.3", 32 | "socket.io": "^4.7.5" 33 | }, 34 | "devDependencies": { 35 | "nodemon": "^3.1.9" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/svg/notice.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/model/message_read.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 聊天消息阅读状态表 3 | */ 4 | const { Model, DataTypes } = require("sequelize"); 5 | const db = require("../db"); 6 | const moment = require("moment"); 7 | 8 | class MessageRead extends Model {} 9 | 10 | MessageRead.init( 11 | { 12 | message_id: { 13 | type: DataTypes.INTEGER(11), 14 | allowNull: false, 15 | comment: "聊天消息id", 16 | }, 17 | user_id: { 18 | type: DataTypes.INTEGER(11), 19 | allowNull: false, 20 | comment: "用户id", 21 | }, 22 | read_time: { 23 | type: DataTypes.DATE, 24 | allowNull: true, 25 | comment: "阅读时间", 26 | get() { 27 | const rawValue = this.getDataValue("read_time"); 28 | return rawValue ? moment(rawValue).format("YYYY-MM-DD HH:mm") : null; 29 | }, 30 | }, 31 | }, 32 | { 33 | sequelize: db, 34 | // 模型默认名 35 | modelName: "message_read", 36 | // 显式指定表名为`message_read` 37 | tableName: "message_read", 38 | createdAt: "created_time", 39 | updatedAt: "update_time", 40 | } 41 | ); 42 | 43 | // MessageRead.sync({ force: true }) 44 | module.exports = MessageRead; 45 | -------------------------------------------------------------------------------- /src/hooks/modules/useTheme.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 白天黑夜模式切换 3 | * */ 4 | import { ref, watchEffect } from 'vue' 5 | 6 | type Theme = 'light' | 'dark' 7 | 8 | export function useTheme() { 9 | const theme = ref(getInitialTheme()) 10 | 11 | function getInitialTheme(): Theme { 12 | // 优先使用 localStorage 保存的主题 13 | const savedTheme = localStorage.getItem('theme') as Theme | null 14 | if (savedTheme) return savedTheme 15 | 16 | // 其次使用系统偏好 17 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 18 | } 19 | 20 | function applyTheme(newTheme: Theme) { 21 | theme.value = newTheme 22 | localStorage.setItem('theme', newTheme) 23 | 24 | if (newTheme === 'dark') { 25 | document.documentElement.classList.add('dark') 26 | } else { 27 | document.documentElement.classList.remove('dark') 28 | } 29 | } 30 | 31 | function toggleTheme() { 32 | applyTheme(theme.value === 'dark' ? 'light' : 'dark') 33 | } 34 | 35 | // 初始化时应用主题 36 | watchEffect(() => applyTheme(theme.value)) 37 | 38 | return { 39 | theme, 40 | toggleTheme, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/layout/themes/default/components/logo/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 14 | lightChat 15 | 16 | 17 | 18 | 19 | 43 | -------------------------------------------------------------------------------- /src/hooks/modules/useCurrentInstance.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @description: 当前组件实例hooks,用以提供常用工具等 3 | */ 4 | 5 | import type { ComponentInternalInstance, ComponentPublicInstance } from 'vue' 6 | import { getCurrentInstance } from 'vue' 7 | import { useRoute, useRouter } from 'vue-router' 8 | 9 | // 定义 proxy 上的自定义属性类型 10 | interface CustomProperties { 11 | $api: any 12 | $HTTP: any 13 | $HTTP_CODE: any 14 | $dict: any 15 | $is: any 16 | $dataHelpers: any 17 | $common: any 18 | } 19 | 20 | export const useCurrentInstance = () => { 21 | const router = useRouter() 22 | const route = useRoute() 23 | 24 | const currentInstance = getCurrentInstance() 25 | 26 | if (!currentInstance) { 27 | throw new Error('useCurrentInstance must be called within a setup function') 28 | } 29 | 30 | const { proxy } = currentInstance as ComponentInternalInstance & { proxy: ComponentPublicInstance & CustomProperties } 31 | 32 | const { $api, $HTTP, $HTTP_CODE, $dict, $is, $dataHelpers, $common } = proxy 33 | 34 | return { 35 | router, 36 | route, 37 | currentInstance, 38 | proxy, 39 | $api, 40 | $HTTP, 41 | $HTTP_CODE, 42 | $dict, 43 | $is, 44 | $dataHelpers, 45 | $common, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/mock/modules/note.ts: -------------------------------------------------------------------------------- 1 | import { mock, Random } from 'mockjs' 2 | import querystring from 'querystring' 3 | 4 | const noteMock = () => { 5 | // 列表模拟接口 6 | const contactList: any[] = [] 7 | const count = 100 8 | for (let i = 0; i < count; i += 1) { 9 | const note = { 10 | cover: `https://picsum.photos/300?random=${i}`, 11 | desc: Random.sentence(3, 15), 12 | title: Random.sentence(3, 8), 13 | } 14 | 15 | contactList.push( 16 | mock({ 17 | id: '@increment', 18 | ...note, 19 | }), 20 | ) 21 | } 22 | mock(/\/api\/v1\/note\/list/, 'get', (request) => { 23 | const { keywords = '', page = 1, pageSize = 10 } = querystring.parse(request.url) as any 24 | 25 | const mockList = contactList.filter((item) => { 26 | // 关键词搜索 27 | return !(keywords && item.desc.indexOf(keywords) === -1) 28 | }) 29 | 30 | const pageList = mockList.filter((item, index) => { 31 | return index < page * pageSize && index >= (page - 1) * pageSize 32 | }) 33 | 34 | const count = mockList.length 35 | return { 36 | code: 200, 37 | message: '请求成功', 38 | data: { 39 | rows: pageList, 40 | count, 41 | }, 42 | } 43 | }) 44 | } 45 | 46 | export default noteMock 47 | -------------------------------------------------------------------------------- /src/router/routes/basic.ts: -------------------------------------------------------------------------------- 1 | import permissionRoutes from './permission' 2 | 3 | // 根路由 4 | export const rootRoute = { 5 | path: '/', 6 | name: 'Layout', 7 | component: () => import('@/layout/index.vue'), 8 | children: [ 9 | { 10 | name: 'Home', 11 | path: '/home', 12 | component: () => import('@/view/home/index.vue'), 13 | meta: { 14 | icon: 'ri-home-smile-2-line', 15 | activeIcon: 'ri-home-smile-2-fill', 16 | title: '首页', 17 | }, 18 | }, 19 | ...permissionRoutes, 20 | ], 21 | } 22 | 23 | // 登录页路由 24 | export const loginRoutes = { 25 | path: '/login', 26 | name: 'Login', 27 | component: () => import('@/view/login/index.vue'), 28 | } 29 | 30 | // 注册页路由 31 | export const registerRoutes = { 32 | path: '/register', 33 | name: 'Register', 34 | component: () => import('@/view/register/index.vue'), 35 | } 36 | 37 | // 404页路由 38 | export const notFoundRoutes = [ 39 | { 40 | path: '/:path(.*)*', 41 | name: 'NotFound', 42 | redirect: '/404', 43 | }, 44 | { 45 | path: '/404', 46 | name: '404', 47 | component: () => import('@/view/errors/404/index.vue'), 48 | }, 49 | ] 50 | 51 | const basicRoutes = [rootRoute, loginRoutes, registerRoutes, ...notFoundRoutes] 52 | 53 | export default basicRoutes 54 | -------------------------------------------------------------------------------- /server/middleware/tokenAuth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 用户登录与否token验证中间件 3 | */ 4 | const { verify } = require('jsonwebtoken') 5 | const { JWT_SECRET } = require('../config/app') 6 | const { ValidateError, DataError } = require('../middleware/errorHandler') 7 | 8 | const tokenAuth = async (context, next) => { 9 | const { authorization: token } = context.request.header 10 | 11 | if (!token) { 12 | let message = 'token值缺失' 13 | let error = new ValidateError(message) 14 | error.payload = { 15 | code: 100901001, 16 | text: message 17 | } 18 | throw error 19 | } 20 | 21 | try { 22 | const user = verify(token, JWT_SECRET) 23 | context.state.user = user 24 | } catch ({ name }) { 25 | if (name === 'TokenExpiredError') { 26 | let message = 'token已过期' 27 | let error = new DataError(message) 28 | error.payload = { 29 | code: 100901002, 30 | text: message 31 | } 32 | throw error 33 | } else if (name === 'JsonWebTokenError') { 34 | let message = '无效的token' 35 | let error = new DataError(message) 36 | error.payload = { 37 | code: 100901003, 38 | text: message 39 | } 40 | throw error 41 | } 42 | } 43 | await next() 44 | } 45 | 46 | module.exports = tokenAuth -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@release-it/conventional-changelog": { 4 | "preset": { 5 | "name": "conventionalcommits", 6 | "types": [ 7 | { "type": "feat", "section": "✨ Features | 新功能" }, 8 | { "type": "fix", "section": "🐛 Bug Fixes | Bug 修复" }, 9 | { "type": "chore", "section": "🎫 Chores | 其他更新" }, 10 | { "type": "docs", "section": "📝 Documentation | 文档" }, 11 | { "type": "style", "section": "💄 Styles | 风格" }, 12 | { "type": "refactor", "section": "♻ Code Refactoring | 代码重构" }, 13 | { "type": "perf", "section": "⚡ Performance Improvements | 性能优化" }, 14 | { "type": "test", "section": "✅ Tests | 测试" }, 15 | { "type": "revert", "section": "⏪ Reverts | 回退" }, 16 | { "type": "build", "section": "👷 Build System | 构建" }, 17 | { "type": "ci", "section": "🔧 Continuous Integration | CI 配置" }, 18 | { "type": "config", "section": "🔨 CONFIG | 配置" } 19 | ] 20 | }, 21 | "infile": "CHANGELOG.md", 22 | "ignoreRecommendedBump": true, 23 | "strictSemVer": true 24 | } 25 | }, 26 | "git": { 27 | "commitMessage": "chore: Release v${version}" 28 | }, 29 | "github": { 30 | "release": true, 31 | "draft": false 32 | } 33 | } -------------------------------------------------------------------------------- /src/layout/themes/default/components/aside/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 45 | -------------------------------------------------------------------------------- /server/model/message_delete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 聊天消息删除情况表 3 | */ 4 | const { Model, DataTypes } = require("sequelize"); 5 | const db = require("../db"); 6 | const moment = require("moment"); 7 | 8 | class MessageDelete extends Model {} 9 | 10 | MessageDelete.init( 11 | { 12 | message_id: { 13 | type: DataTypes.INTEGER(11), 14 | allowNull: false, 15 | comment: "聊天消息id", 16 | }, 17 | user_id: { 18 | type: DataTypes.INTEGER(11), 19 | allowNull: false, 20 | comment: "用户id", 21 | }, 22 | is_delete: { 23 | type: DataTypes.ENUM, 24 | values: ["0", "1"], 25 | allowNull: false, 26 | defaultValue: "0", 27 | comment: "是否删除(0正常 1软删除)", 28 | }, 29 | delete_time: { 30 | type: DataTypes.DATE, 31 | allowNull: false, 32 | comment: "删除时间", 33 | get() { 34 | const rawValue = this.getDataValue("delete_time"); 35 | return rawValue ? moment(rawValue).format("YYYY-MM-DD HH:mm") : null; 36 | }, 37 | }, 38 | }, 39 | { 40 | sequelize: db, 41 | // 模型默认名 42 | modelName: "message_delete", 43 | // 显式指定表名为`message_delete` 44 | tableName: "message_delete", 45 | createdAt: "created_time", 46 | updatedAt: "update_time", 47 | } 48 | ); 49 | 50 | // MessageDelete.sync({ force: true }); 51 | module.exports = MessageDelete; 52 | -------------------------------------------------------------------------------- /src/mock/modules/login.ts: -------------------------------------------------------------------------------- 1 | import { mock } from 'mockjs' 2 | 3 | const loginMock = () => { 4 | mock(/\/api\/v1\/user\/regist/, 'post', (options) => { 5 | const params = JSON.parse(options.body) 6 | // console.log('/api/v1/register接口请求参数', params) 7 | 8 | if (!params) return 9 | 10 | return { 11 | code: 200, 12 | data: null, 13 | message: '注册成功', 14 | } 15 | }) 16 | 17 | mock(/\/api\/v1\/user\/login/, 'post', (options) => { 18 | const params = JSON.parse(options.body) 19 | // console.log('/api/v1/login接口请求参数', params) 20 | 21 | if (!params) return 22 | 23 | // 32位随机token生成 24 | function returnToken() { 25 | const abc = 'abcdefghijklmnopqrstuvwxyz1234567890'.split('') 26 | let token = '' 27 | for (let i = 0; i < 32; i += 1) { 28 | token += abc[Math.floor(Math.random() * abc.length)] 29 | } 30 | return token 31 | } 32 | 33 | // 用户信息 34 | const userInfo = { 35 | info: { 36 | account: 'todo_6666', 37 | avatar: 'http://localhost:3000/upload/avater/1c7d29ca73ae4d6cfc4fd5c14.jpg', 38 | birthday: '2005-01-01', 39 | id: 1, 40 | sex: '1', 41 | }, 42 | token: returnToken(), 43 | } 44 | 45 | return { 46 | code: 200, 47 | data: userInfo, 48 | message: '登录成功', 49 | } 50 | }) 51 | } 52 | 53 | export default loginMock 54 | -------------------------------------------------------------------------------- /server/model/message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 聊天消息表 3 | */ 4 | const { Model, DataTypes } = require("sequelize"); 5 | const db = require("../db"); 6 | const moment = require("moment"); 7 | class Message extends Model {} 8 | 9 | Message.init( 10 | { 11 | user_id: { 12 | type: DataTypes.INTEGER(11), 13 | allowNull: false, 14 | comment: "用户id", 15 | }, 16 | reciver_id: { 17 | type: DataTypes.INTEGER(11), 18 | allowNull: false, 19 | comment: "接收用户或群id", 20 | }, 21 | content: { 22 | type: DataTypes.TEXT, 23 | allowNull: false, 24 | comment: "消息内容", 25 | }, 26 | type: { 27 | type: DataTypes.ENUM, 28 | values: ["0", "1", "2", "3"], 29 | allowNull: false, 30 | defaultValue: "0", 31 | comment: "消息类型(0文本 1图片 2语音 3视频)", 32 | }, 33 | send_time: { 34 | type: DataTypes.DATE, 35 | allowNull: true, 36 | comment: "发送时间", 37 | get() { 38 | const rawValue = this.getDataValue("send_time"); 39 | return rawValue ? moment(rawValue).format("YYYY-MM-DD HH:mm") : null; 40 | }, 41 | }, 42 | }, 43 | { 44 | sequelize: db, 45 | // 模型默认名 46 | modelName: "message", 47 | // 显式指定表名为`message` 48 | tableName: "message", 49 | createdAt: "created_time", 50 | updatedAt: "update_time", 51 | } 52 | ); 53 | 54 | // Message.sync({ force: true }) 55 | module.exports = Message; 56 | -------------------------------------------------------------------------------- /src/components/Column/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 49 | 50 | 56 | -------------------------------------------------------------------------------- /src/components/SvgRender/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 59 | 60 | -------------------------------------------------------------------------------- /src/layout/themes/default/components/menu/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | {{ menu.meta.title }} 14 | 15 | 16 | 17 | 18 | 19 | 37 | 38 | 44 | -------------------------------------------------------------------------------- /server/model/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 用户表 3 | */ 4 | const { DataTypes } = require('sequelize') 5 | const db = require('../db') 6 | 7 | const User = db.define('user', { 8 | account: { 9 | type: DataTypes.CHAR(50), 10 | unique: true, 11 | allowNull: false, 12 | comment: '账号', 13 | }, 14 | password: { 15 | type: DataTypes.CHAR(64), 16 | allowNull: false, 17 | comment: '密码', 18 | }, 19 | salt: { 20 | type: DataTypes.CHAR(100), 21 | allowNull: false, 22 | comment: '密码盐', 23 | }, 24 | avatar: { 25 | type: DataTypes.CHAR(255), 26 | allowNull: true, 27 | comment: '用户头像', 28 | }, 29 | nickname: { 30 | type: DataTypes.CHAR(30), 31 | allowNull: true, 32 | comment: '名称(昵称)', 33 | }, 34 | sex: { 35 | type: DataTypes.ENUM, 36 | values: ['0', '1', '2'], 37 | allowNull: false, 38 | defaultValue: '0', 39 | comment: '待办完成状态(0女 1男 2未知)', 40 | }, 41 | birthday: { 42 | type: DataTypes.DATEONLY, 43 | allowNull: false, 44 | comment: '生日', 45 | }, 46 | status: { 47 | type: DataTypes.ENUM, 48 | values: ['0', '1'], 49 | allowNull: false, 50 | defaultValue: '0', 51 | comment: '账号状态(0正常 1已冻结)', 52 | }, 53 | is_delete: { 54 | type: DataTypes.ENUM, 55 | values: ['0', '1'], 56 | allowNull: false, 57 | defaultValue: '0', 58 | comment: '是否删除(0正常 1软删除)', 59 | }, 60 | }, { 61 | // 显式指定表名为`user` 62 | tableName: 'user', 63 | createdAt: 'created_time', 64 | updatedAt: 'update_time', 65 | }) 66 | 67 | // User.sync({ force: true }) 68 | module.exports = User -------------------------------------------------------------------------------- /src/assets/svg/right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/mock/modules/contact.ts: -------------------------------------------------------------------------------- 1 | import { mock, Random } from 'mockjs' 2 | import querystring from 'querystring' 3 | import type { IContactVo } from '../types/contact/contact.vo.ts' 4 | 5 | const contactMock = () => { 6 | // 列表模拟接口 7 | const contactList: IContactVo[] = [] 8 | const count = 100 9 | for (let i = 0; i < count; i += 1) { 10 | const contact = { 11 | user_id: Math.floor(Math.random() * 1000), 12 | account: Random.string(6, 10), 13 | nickname: mock('@name'), 14 | remark: mock('@name'), 15 | desc: Random.sentence(3, 15), 16 | sex: Math.random() > 0.5 ? '0' : '1', 17 | } 18 | const extraInfo = { 19 | create_time: mock('@datetime'), 20 | } 21 | 22 | contactList.push( 23 | mock({ 24 | ...contact, 25 | avatar: `https://picsum.photos/300?random=${i}`, 26 | ...extraInfo, 27 | }), 28 | ) 29 | } 30 | mock(/\/api\/v1\/contact\/list/, 'get', (request) => { 31 | const { keywords = '', page = 1, pageSize = 10 } = querystring.parse(request.url) as any 32 | 33 | const mockList = contactList.filter((item) => { 34 | // 关键词搜索 35 | return !(keywords && item.nickname.indexOf(keywords) === -1) 36 | }) 37 | 38 | const pageList = mockList.filter((item, index) => { 39 | return index < page * pageSize && index >= (page - 1) * pageSize 40 | }) 41 | 42 | const count = mockList.length 43 | return { 44 | code: 200, 45 | message: '请求成功', 46 | data: { 47 | rows: pageList, 48 | count, 49 | }, 50 | } 51 | }) 52 | } 53 | 54 | export default contactMock 55 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | es2021: true, 6 | }, 7 | parser: 'vue-eslint-parser', 8 | extends: [ 9 | 'eslint:recommended', //继承 ESLint 内置的推荐规则 10 | 'plugin:vue/vue3-recommended', // 继承 Vue.js 3 的推荐规则 11 | 'plugin:@typescript-eslint/recommended', //继承 TypeScript ESLint 插件的推荐规则 12 | 'plugin:prettier/recommended', //继承 Prettier 的推荐规则 13 | 'eslint-config-prettier', //关闭 ESLint 中与 Prettier 冲突的规则 14 | ], 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | parser: '@typescript-eslint/parser', 18 | sourceType: 'module', 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | }, 23 | ignorePatterns: ['dist', 'node_modules', '.eslintrc.cjs', 'commitlint.config.cjs'], 24 | plugins: ['vue', '@typescript-eslint', 'prettier'], 25 | rules: { 26 | 'vue/multi-word-component-names': 'off', // 禁用vue文件强制多个单词命名 27 | '@typescript-eslint/no-explicit-any': 'off', //允许使用any 28 | 'no-unused-vars': 'off', // 禁用未使用变量的检查 29 | '@typescript-eslint/no-unused-vars': 'off', // 禁用 TypeScript 中未使用变量的检查 30 | '@typescript-eslint/no-this-alias': [ 31 | 'error', 32 | { 33 | allowedNames: ['that'], // this可用的局部变量名称 34 | }, 35 | ], 36 | '@typescript-eslint/ban-ts-comment': 'off', //允许使用@ts-ignore 37 | '@typescript-eslint/no-non-null-assertion': 'off', //允许使用非空断言 38 | 'no-console': [ 39 | //提交时不允许有console.log 40 | 'warn', 41 | { 42 | allow: ['warn', 'error'], 43 | }, 44 | ], 45 | 'no-debugger': 'warn', //提交时不允许有debugger 46 | semi: 'off', // 允许语句后面没有分号 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/helper/is.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * 数据类型检测工具汇总 3 | * * * @function isArray: 检测给定的值是否为数组 4 | * * * @function isObject: 检测给定的值是否为对象(注意:在JavaScript中,数组和null也会被认为是对象) 5 | * * * @function isString: 检测给定的值是否为字符串 6 | * * * @function isNumber: 检测给定的值是否为数字 7 | * * * @function isFunction: 检测给定的值是否为函数(包括异步函数) 8 | * * * @function isAsyncFunction: 检测给定的值是否为异步函数 9 | * * * @function isRegExp: 检测给定的值是否为正则表达式对象 10 | * * * @function isDef: 检测给定的值是否已定义(即不是undefined) 11 | * * * @function isUnDef: 检测给定的值是否未定义(即为undefined) 12 | * * * @function isNull: 检测给定的值是否为null 13 | */ 14 | 15 | export function dataTypeCheck(value, type) { 16 | return Object.prototype.toString.call(value) === `[object ${type}]` 17 | } 18 | 19 | export function isArray(value) { 20 | return dataTypeCheck(value, 'Array') 21 | } 22 | 23 | export function isObject(value) { 24 | return dataTypeCheck(value, 'Object') 25 | } 26 | 27 | export function isString(value) { 28 | return dataTypeCheck(value, 'String') 29 | } 30 | 31 | export function isNumber(value) { 32 | return dataTypeCheck(value, 'Number') 33 | } 34 | 35 | export function isFunction(value) { 36 | return dataTypeCheck(value, 'Function') || isAsyncFunction(value) 37 | } 38 | 39 | export function isAsyncFunction(value) { 40 | return dataTypeCheck(value, 'AsyncFunction') 41 | } 42 | 43 | export function isRegExp(value) { 44 | return dataTypeCheck(value, 'RegExp') 45 | } 46 | 47 | export function isDef(value) { 48 | return !dataTypeCheck(value, 'Undefined') 49 | } 50 | 51 | export function isUnDef(value) { 52 | return !isDef(value) 53 | } 54 | 55 | export function isNull(value) { 56 | return dataTypeCheck(value, 'Null') 57 | } 58 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import $api from '@/api' 2 | import { HTTP_CODE, HTTP_HOST } from '@/settings/config/http' 3 | import { defineStore } from 'pinia' 4 | import { ref } from 'vue' 5 | 6 | interface UserInfo { 7 | avatar: string 8 | [key: string]: any 9 | } 10 | 11 | export const useUserStore = defineStore('userStore', () => { 12 | const userInfo = ref(null) 13 | const updateUserInfo = (user: UserInfo) => { 14 | userInfo.value = user 15 | 16 | // 数据持久化 17 | localStorage.setItem('user', JSON.stringify(user)) 18 | } 19 | 20 | const token = ref('') 21 | const updateToken = (value: string) => { 22 | token.value = value 23 | 24 | // 数据持久化 25 | localStorage.setItem('token', value) 26 | } 27 | 28 | // 登录action 29 | const accountLogin = (data: { account: string; password: string }) => { 30 | return new Promise((resolve, reject) => { 31 | ;($api as any).login 32 | .accountLogin(data) 33 | .then((res) => { 34 | const { code, data, message } = res.data 35 | if (code === HTTP_CODE.HTTP_SUCCESS_CODE) { 36 | const { avatar, ...extra } = data.info 37 | updateUserInfo({ 38 | avatar: `${HTTP_HOST}upload/avatar/${avatar}`, 39 | ...extra, 40 | }) 41 | updateToken(data.token) 42 | resolve() 43 | } else { 44 | reject(message) 45 | } 46 | }) 47 | .catch(() => { 48 | reject('登录失败') 49 | }) 50 | }) 51 | } 52 | 53 | return { 54 | userInfo, 55 | updateUserInfo, 56 | token, 57 | updateToken, 58 | accountLogin, 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /src/mock/modules/chat.ts: -------------------------------------------------------------------------------- 1 | import { mock, Random } from 'mockjs' 2 | import querystring from 'querystring' 3 | import type { IChatListVo } from '../types/chat/chat.vo.ts' 4 | 5 | const chatMock = () => { 6 | /** 7 | * 聊天列表模拟接口 8 | * */ 9 | const chatList: IChatListVo[] = [] 10 | const count = 100 11 | for (let i = 0; i < count; i += 1) { 12 | const chat = { 13 | user_id: Math.floor(Math.random() * 1000), 14 | nickname: mock('@name'), 15 | slogan: Random.sentence(3, 5), 16 | last_message: Random.sentence(3, 5), 17 | } 18 | const extraInfo = { 19 | send_time: mock('@datetime'), 20 | create_time: mock('@datetime'), 21 | } 22 | 23 | chatList.push( 24 | mock({ 25 | ...chat, 26 | avatar: `https://picsum.photos/300?random=${i}`, 27 | ...extraInfo, 28 | }), 29 | ) 30 | } 31 | mock(/\/api\/v1\/chat\/list/, 'get', (request) => { 32 | const queryParams = querystring.parse(request.url) as Record 33 | 34 | const keywords = queryParams.keywords || '' 35 | const page = parseInt(queryParams.page || '1', 10) 36 | const pageSize = parseInt(queryParams.pageSize || '10', 10) 37 | 38 | const mockList = chatList.filter((item) => { 39 | // 关键词搜索 40 | return !(keywords && item.nickname.indexOf(keywords) === -1) 41 | }) 42 | 43 | const pageList = mockList.filter((item, index) => { 44 | return index < page * pageSize && index >= (page - 1) * pageSize 45 | }) 46 | 47 | const count = mockList.length 48 | return { 49 | code: 200, 50 | message: '请求成功', 51 | data: { 52 | rows: pageList, 53 | count, 54 | }, 55 | } 56 | }) 57 | } 58 | 59 | export default chatMock 60 | -------------------------------------------------------------------------------- /src/assets/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/utils/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 通用方法 3 | */ 4 | const Schema = require("async-validator"); 5 | 6 | // 表单数据校验 7 | async function validateFormData(data, rules) { 8 | const validator = new Schema.default(rules); 9 | 10 | return await validator 11 | .validate(data) 12 | .then(() => { 13 | return { 14 | data, 15 | error: null, 16 | message: null, 17 | }; 18 | }) 19 | .catch(({ errors }) => { 20 | return { 21 | data: null, 22 | errors, 23 | message: errors[0].message, 24 | }; 25 | }); 26 | } 27 | 28 | // 成功返回 29 | function successResponse(message = "success", data = null, code = 200) { 30 | return { 31 | code, 32 | data, 33 | message, 34 | }; 35 | } 36 | 37 | // 失败返回 38 | function failResponse(message = "fail", data = null, code = 0) { 39 | return { 40 | code, 41 | data, 42 | message, 43 | }; 44 | } 45 | 46 | // 对象扁平化 47 | function flatObject(obj, autoPrefix = true, prefix = "", noPrefixAttrs = []) { 48 | let flatObj = {}; 49 | Object.entries(obj).forEach(([key, val]) => { 50 | if (Object.prototype.toString.call(val) === "[object Object]") { 51 | flatObj = { 52 | ...flatObj, 53 | ...flatObject(val, autoPrefix, autoPrefix ? key : '', noPrefixAttrs), 54 | }; 55 | } else { 56 | if (autoPrefix && !noPrefixAttrs.includes(key)) { 57 | prefix = prefix ? prefix + "_" : ""; 58 | flatObj[`${prefix}${key}`] = val; 59 | } else { 60 | flatObj[key] = val; 61 | } 62 | } 63 | }); 64 | 65 | return flatObj; 66 | } 67 | 68 | const commonUtils = { 69 | validateFormData, 70 | successResponse, 71 | failResponse, 72 | flatObject, 73 | }; 74 | 75 | module.exports = commonUtils; 76 | -------------------------------------------------------------------------------- /server/controller/chat.js: -------------------------------------------------------------------------------- 1 | const chatService = require("../service/chat"); 2 | const { successResponse, failResponse } = require("../utils/common"); 3 | 4 | class ChatController { 5 | /** 6 | * 添加聊天 7 | * @param {*} context 8 | */ 9 | create = async (context) => { 10 | const { reciver_id, type = 0 } = context.request.body; 11 | 12 | const { id: user_id } = context.state.user; 13 | 14 | const data = { 15 | user_id, 16 | reciver_id: reciver_id ? String(reciver_id) : reciver_id, 17 | type: String(type), 18 | }; 19 | 20 | const res = await chatService.createChat(data); 21 | 22 | context.body = res 23 | ? successResponse("添加聊天成功!") 24 | : failResponse("添加聊天失败!"); 25 | }; 26 | 27 | /** 28 | * 获取聊天列表 29 | * @param {*} context 30 | */ 31 | list = async (context) => { 32 | const { keywords = "", type = null, page = 1, limit = 10 } = context.query; 33 | const { id: user_id } = context.state.user; 34 | const data = { 35 | user_id, 36 | keywords, 37 | type, 38 | page: Number(page), 39 | limit: Number(limit), 40 | }; 41 | const res = await chatService.getChatList(data); 42 | 43 | context.body = res 44 | ? successResponse("获取聊天列表成功!", res) 45 | : failResponse("获取聊天列表失败!"); 46 | }; 47 | 48 | /** 49 | * 操作-删除聊天 50 | * @param {*} context 51 | */ 52 | delete = async (context) => { 53 | const { chat_id, reciver_id } = context.query; 54 | const { id: user_id } = context.state.user; 55 | const data = { 56 | chat_id, 57 | reciver_id, 58 | user_id, 59 | }; 60 | const res = await chatService.deleteChat(data); 61 | 62 | context.body = res 63 | ? successResponse("删除聊天成功!") 64 | : failResponse("删除聊天失败!"); 65 | }; 66 | } 67 | 68 | module.exports = new ChatController(); 69 | -------------------------------------------------------------------------------- /server/controller/message.js: -------------------------------------------------------------------------------- 1 | const messageService = require("../service/message"); 2 | const { successResponse, failResponse } = require("../utils/common"); 3 | 4 | class MessageController { 5 | /** 6 | * 获取聊天消息列表 7 | * @param {*} context 8 | */ 9 | list = async (context) => { 10 | const { reciver_id, keywords = '', type = null, status = null, page = 1, limit = 10 } = context.query; 11 | const { id: user_id } = context.state.user; 12 | const data = { 13 | reciver_id, 14 | user_id, 15 | keywords, 16 | type, 17 | status, 18 | page: Number(page), 19 | limit: Number(limit), 20 | }; 21 | const res = await messageService.getMessageList(data); 22 | 23 | context.body = res 24 | ? successResponse("获取聊天消息列表成功!", res) 25 | : failResponse("获取聊天消息列表失败!"); 26 | }; 27 | 28 | /** 29 | * 操作-删除聊天消息 30 | * @param {*} context 31 | */ 32 | delete = async (context) => { 33 | const { id } = context.query; 34 | const { id: user_id } = context.state.user; 35 | const data = { 36 | id, 37 | user_id, 38 | }; 39 | const res = await messageService.deleteMessage(data); 40 | 41 | context.body = res 42 | ? successResponse("删除聊天消息成功!") 43 | : failResponse("删除聊天消息失败!"); 44 | }; 45 | 46 | /** 47 | * 设置为已读 48 | * @param {*} context 49 | */ 50 | // read = async (context) => { 51 | // const { id } = context.request.body; 52 | // const { id: user_id } = context.state.user; 53 | // const data = { 54 | // id: String(id), 55 | // user_id, 56 | // status: "1", 57 | // read_time: new Date(), 58 | // }; 59 | // const res = await messageService.updateMessageRead(data); 60 | 61 | // context.body = res 62 | // ? successResponse("设置为已读成功!") 63 | // : failResponse("设置为已读失败!"); 64 | // }; 65 | } 66 | 67 | module.exports = new MessageController(); 68 | -------------------------------------------------------------------------------- /src/hooks/modules/useLoadMore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 加载更多 3 | */ 4 | import { debounce } from 'lodash-es' 5 | import { onActivated, onBeforeUnmount, onDeactivated } from 'vue' 6 | 7 | interface UseLoadMoreOptions { 8 | type?: 'top' | 'bottom' | 'both' 9 | scrollTopCallback?: () => void 10 | scrollBottomCallback?: () => void 11 | container?: HTMLElement 12 | distance?: number 13 | } 14 | 15 | export const useLoadMore = (options: UseLoadMoreOptions = {}) => { 16 | const { 17 | type = 'both', 18 | scrollTopCallback, 19 | scrollBottomCallback, 20 | container = document.documentElement, 21 | distance = 0, 22 | } = options 23 | 24 | // 判断滚动条是否滚动到顶部 25 | const isScrollTop = (distance: number = 0) => { 26 | const scrollTop = container.scrollTop 27 | return scrollTop - distance <= 0 28 | } 29 | 30 | // 判断滚动条是否滚动到底部 31 | const isScrollBottom = (distance: number = 0) => { 32 | const scrollHeight = container.scrollHeight 33 | const scrollTop = container.scrollTop 34 | const clientHeight = container.clientHeight 35 | return scrollHeight - distance <= scrollTop + clientHeight 36 | } 37 | 38 | // 滚动监听回调 39 | const handleScroll = debounce(() => { 40 | if (['top', 'both'].includes(type) && isScrollTop(distance)) { 41 | scrollTopCallback?.() 42 | } 43 | 44 | if (['bottom', 'both'].includes(type) && isScrollBottom(distance)) { 45 | scrollBottomCallback?.() 46 | } 47 | }, 300) 48 | 49 | container.addEventListener('scroll', handleScroll) 50 | 51 | onBeforeUnmount(() => { 52 | container.removeEventListener('scroll', handleScroll) 53 | }) 54 | 55 | onActivated(() => { 56 | container.addEventListener('scroll', handleScroll) 57 | }) 58 | 59 | onDeactivated(() => { 60 | container.removeEventListener('scroll', handleScroll) 61 | }) 62 | 63 | return { 64 | isScrollTop, 65 | isScrollBottom, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/mock/modules/message.ts: -------------------------------------------------------------------------------- 1 | import useChatStore from '@/store/modules/chat' 2 | import { formatDate } from '@/utils/helper/data' 3 | import { mock, Random } from 'mockjs' 4 | import querystring from 'querystring' 5 | import type { IMessageListVo } from '../types/message/message.vo.ts' 6 | 7 | const messageMock = () => { 8 | // 列表模拟接口 9 | let messageList: IMessageListVo[] = [] 10 | const getMessagePool = () => { 11 | const chatStore = useChatStore() 12 | const { user_id, avatar } = chatStore.activeChat! 13 | 14 | messageList = [] 15 | const count = 100 16 | for (let i = 0; i < count; i += 1) { 17 | const message = { 18 | id: Math.floor(Math.random() * 1000), 19 | user_id, 20 | content: Random.sentence(3, 5), 21 | receiver_id: Math.floor(Math.random() * 1000), 22 | } 23 | const extraInfo = { 24 | is_me: Math.random() > 0.5, 25 | status: Math.floor(Math.random()), 26 | send_time: formatDate(new Date()), 27 | create_time: mock('@datetime'), 28 | } 29 | 30 | messageList.push( 31 | mock({ 32 | ...message, 33 | avatar: avatar, 34 | ...extraInfo, 35 | }), 36 | ) 37 | } 38 | } 39 | mock(/\/api\/v1\/message\/list/, 'get', (request) => { 40 | getMessagePool() 41 | 42 | const { keywords = '', page = 1, pageSize = 20 } = querystring.parse(request.url) as any 43 | 44 | const mockList = messageList.filter((item) => { 45 | // 关键词搜索 46 | return !(keywords && item.content.indexOf(keywords) === -1) 47 | }) 48 | 49 | const pageList = mockList.filter((item, index) => { 50 | return index < page * pageSize && index >= (page - 1) * pageSize 51 | }) 52 | 53 | const count = mockList.length 54 | return { 55 | code: 200, 56 | message: '请求成功', 57 | data: { 58 | rows: pageList, 59 | count, 60 | }, 61 | } 62 | }) 63 | } 64 | 65 | export default messageMock 66 | -------------------------------------------------------------------------------- /server/controller/contact.js: -------------------------------------------------------------------------------- 1 | const contactService = require("../service/contact"); 2 | const { successResponse, failResponse } = require("../utils/common"); 3 | 4 | class ContactController { 5 | /** 6 | * 添加好友 7 | * @param {*} context 8 | */ 9 | create = async (context) => { 10 | const { reciver_id } = context.request.body; 11 | 12 | const { id: user_id } = context.state.user; 13 | 14 | const data = { 15 | user_id, 16 | reciver_id: String(reciver_id), 17 | }; 18 | 19 | const res = await contactService.createContact(data); 20 | 21 | context.body = res 22 | ? successResponse("添加好友成功!") 23 | : failResponse("添加好友失败!"); 24 | }; 25 | 26 | /** 27 | * 获取联系人列表 28 | * @param {*} context 29 | */ 30 | list = async (context) => { 31 | const { keywords = '', type = null, page = 1, limit = 10 } = context.query; 32 | const { id: user_id } = context.state.user; 33 | const data = { 34 | user_id, 35 | keywords, 36 | type, 37 | page: Number(page), 38 | limit: Number(limit), 39 | }; 40 | const res = await contactService.getContactList(data); 41 | 42 | context.body = res 43 | ? successResponse("获取联系人列表成功!", res) 44 | : failResponse("获取联系人列表失败!"); 45 | }; 46 | 47 | /** 48 | * 操作-删除联系人 49 | * @param {*} context 50 | */ 51 | delete = async (context) => { 52 | const { id } = context.query; 53 | const { id: user_id } = context.state.user; 54 | const data = { 55 | id, 56 | user_id, 57 | }; 58 | const res = await contactService.deleteContact(data); 59 | 60 | context.body = res 61 | ? successResponse("删除联系人成功!") 62 | : failResponse("删除联系人失败!"); 63 | }; 64 | 65 | /** 66 | * 联系人资料设置 67 | * @param {*} context 68 | */ 69 | setInfo = async (context) => { 70 | const { id, remark, desc } = context.request.body; 71 | 72 | const { id: user_id } = context.state.user; 73 | 74 | const data = { 75 | id: String(id), 76 | user_id: String(user_id), 77 | remark, 78 | desc, 79 | }; 80 | 81 | const res = await contactService.setInfo(data); 82 | 83 | context.body = res 84 | ? successResponse("联系人资料设置成功!") 85 | : failResponse("联系人资料设置失败!"); 86 | }; 87 | } 88 | 89 | module.exports = new ContactController(); 90 | -------------------------------------------------------------------------------- /src/view/chat/components/chat-item.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ 5 | $dataHelpers?.formatDate(message.send_time, 'HH:mm') 6 | }} 7 | 8 | 9 | 10 | 11 | 12 | 22 | {{ message.content }} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/assets/svg/weixin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/controller/user.js: -------------------------------------------------------------------------------- 1 | const userService = require('../service/user') 2 | const { successResponse, failResponse } = require('../utils/common') 3 | const { DEFAULT_AVATERS } = require('../config/user') 4 | const { sample } = require('lodash') 5 | 6 | class UserController { 7 | /** 8 | * 注册用户 9 | * @param {*} context 10 | */ 11 | register = async (context) => { 12 | const { account, password } = context.request.body 13 | 14 | console.log(account, password) 15 | 16 | const data = { 17 | avatar: `${sample(DEFAULT_AVATERS)}`, 18 | account, 19 | password 20 | } 21 | 22 | const res = await userService.createUser(data) 23 | 24 | context.body = res 25 | ? successResponse('注册成功!') 26 | : failResponse('注册失败!') 27 | } 28 | 29 | /** 30 | * 登录 31 | * @param {*} context 32 | */ 33 | login = async (context) => { 34 | const { account, password } = context.request.body 35 | const data = { 36 | account, 37 | password 38 | } 39 | console.log(data) 40 | const res = await userService.login(data) 41 | 42 | context.body = res 43 | ? successResponse('登录成功!', res) 44 | : failResponse('登录失败!') 45 | } 46 | 47 | /** 48 | * 上传图像文件 49 | * @param {*} data 50 | * @returns 51 | */ 52 | uploadAvater = async (context) => { 53 | const { file } = context.request.files 54 | const { id: user_id } = context.state.user; 55 | const data = { 56 | user_id, 57 | file, 58 | } 59 | const res = await userService.uploadAvater(data) 60 | 61 | context.body = res 62 | ? successResponse('上传图像成功!', res) 63 | : failResponse('上传图像失败!') 64 | } 65 | 66 | /** 67 | * 更新用户信息 68 | * @param {*} context 69 | */ 70 | updateInfo = async (context) => { 71 | const { sex, birthday } = context.request.body; 72 | 73 | const { id: user_id } = context.state.user; 74 | 75 | const data = { 76 | user_id, 77 | sex, 78 | birthday, 79 | }; 80 | 81 | const res = await userService.updateInfo(data); 82 | 83 | context.body = res 84 | ? successResponse("更新用户信息成功!") 85 | : failResponse("更新用户信息失败!"); 86 | }; 87 | 88 | /** 89 | * 搜索用户列表 90 | * @param {*} context 91 | */ 92 | searchUser = async (context) => { 93 | const { account, page = 1, limit = 10 } = context.query; 94 | const data = { 95 | account, 96 | page: Number(page), 97 | limit: Number(limit), 98 | }; 99 | const res = await userService.searchUser(data); 100 | 101 | context.body = res 102 | ? successResponse("获取用户列表成功!", res) 103 | : failResponse("获取用户列表失败!"); 104 | }; 105 | } 106 | 107 | module.exports = new UserController() 108 | -------------------------------------------------------------------------------- /server/validate/user.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * 用户相关表单数据校验规则rules 3 | */ 4 | 5 | // 用户注册校验规则 6 | const createUserRules = { 7 | account: [ 8 | { 9 | type: "string", 10 | required: true, 11 | message: { 12 | text: "用户名或密码错误", 13 | code: 100001001, 14 | }, 15 | }, 16 | { 17 | type: "string", 18 | pattern: "^[0-9a-zA-Z_]{6,20}$", 19 | message: { 20 | text: "用户名格式错误", 21 | code: 100001002, 22 | }, 23 | }, 24 | ], 25 | password: { 26 | type: "string", 27 | required: true, 28 | message: { 29 | text: "用户名或密码错误", 30 | code: 100001003, 31 | }, 32 | }, 33 | }; 34 | 35 | // 用户登录校验规则 36 | const userLoginRules = { 37 | account: { 38 | type: "string", 39 | required: true, 40 | message: { 41 | text: "用户名或密码错误", 42 | code: 100002001, 43 | }, 44 | }, 45 | password: { 46 | type: "string", 47 | required: true, 48 | message: { 49 | text: "用户名或密码错误", 50 | code: 100002002, 51 | }, 52 | }, 53 | }; 54 | 55 | // 用户图像上传校验规则 56 | const uploadAvaterRules = { 57 | file: [ 58 | { 59 | type: "object", 60 | required: true, 61 | message: { 62 | text: "图像必须", 63 | code: 100003001, 64 | }, 65 | }, 66 | { 67 | validator: (rule, value, callback) => { 68 | // 头像类型检测 69 | const allowFileTypes = ["image/jpeg", "image/png"]; 70 | if (!allowFileTypes.includes(value.mimetype)) { 71 | callback({ 72 | text: "图像格式需为jpg、png", 73 | code: 100003002, 74 | }); 75 | } 76 | 77 | // 头像大小检测 78 | const allowFileSize = 500 * 1024; 79 | if (value.size > allowFileSize) { 80 | callback({ 81 | text: `图片文件大小不超过${allowFileSize / 1024}kb`, 82 | code: 100003003, 83 | }); 84 | } 85 | 86 | callback(); 87 | }, 88 | }, 89 | ], 90 | }; 91 | 92 | // 更新用户信息校验规则 93 | const updateInfoRules = { 94 | sex: [{ 95 | type: 'string', 96 | required: true, 97 | message: { 98 | text: '性别必须', 99 | code: 100004001 100 | }, 101 | }], 102 | birthday: { 103 | type: 'string', 104 | required: true, 105 | message: { 106 | text: '生日必须', 107 | code: 100004002 108 | }, 109 | }, 110 | } 111 | 112 | // 搜索用户列表校验规则 113 | const searchUserRules = { 114 | account: { 115 | type: "string", 116 | required: true, 117 | message: { 118 | text: "请输入账号名称", 119 | code: 100005001, 120 | }, 121 | }, 122 | }; 123 | 124 | const userRules = { 125 | createUserRules, 126 | userLoginRules, 127 | uploadAvaterRules, 128 | updateInfoRules, 129 | searchUserRules, 130 | }; 131 | 132 | module.exports = userRules; 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llm-mini-client", 3 | "private": true, 4 | "version": "1.0.2", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "ts": "vue-tsc -b", 11 | "lint": "eslint src --fix --ext .js,.ts,.vue --report-unused-disable-directives --max-warnings 0", 12 | "prepare": "husky install", 13 | "commit": "git-cz", 14 | "release": "release-it", 15 | "storybook": "storybook dev -p 6006", 16 | "build-storybook": "storybook build" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "lint-staged", 21 | "commit-msg": "commitlint --config .commitlintrc.js -E HUSKY_GIT_PARAMS" 22 | } 23 | }, 24 | "lint-staged": { 25 | "src/**/*.{js,ts,vue,tsx}": [ 26 | "pnpm run lint", 27 | "prettier --write" 28 | ] 29 | }, 30 | "config": { 31 | "commitizen": { 32 | "path": "node_modules/cz-conventional-changelog" 33 | } 34 | }, 35 | "dependencies": { 36 | "@element-plus/icons-vue": "^2.3.1", 37 | "@tailwindcss/vite": "^4.0.9", 38 | "@vueuse/core": "^12.7.0", 39 | "axios": "^1.8.1", 40 | "element-plus": "^2.9.5", 41 | "lodash": "^4.17.21", 42 | "lodash-es": "^4.17.21", 43 | "mockjs": "^1.1.0", 44 | "moment": "^2.30.1", 45 | "pinia": "^3.0.1", 46 | "querystring": "^0.2.1", 47 | "rollup-plugin-visualizer": "^5.14.0", 48 | "socket.io-client": "^4.8.1", 49 | "tailwindcss": "^4.0.9", 50 | "vite-plugin-compression": "^0.5.1", 51 | "vue": "^3.5.13", 52 | "vue-router": "^4.5.0" 53 | }, 54 | "devDependencies": { 55 | "@commitlint/cli": "19.2.0", 56 | "@commitlint/config-conventional": "19.1.0", 57 | "@release-it/conventional-changelog": "^10.0.0", 58 | "@types/lodash-es": "^4.17.12", 59 | "@types/mockjs": "^1.0.10", 60 | "@types/node": "^22.13.8", 61 | "@typescript-eslint/eslint-plugin": "^6.19.0", 62 | "@typescript-eslint/parser": "^6.19.0", 63 | "@vitejs/plugin-legacy": "^6.0.2", 64 | "@vitejs/plugin-vue": "^5.2.1", 65 | "@vue/tsconfig": "^0.7.0", 66 | "commitizen": "^4.3.1", 67 | "cz-conventional-changelog": "^3.3.0", 68 | "eslint": "^8.39.0", 69 | "eslint-config-prettier": "^9.1.0", 70 | "eslint-plugin-prettier": "^5.1.3", 71 | "eslint-plugin-vue": "^9.11.0", 72 | "globals": "^16.0.0", 73 | "husky": "^9.1.7", 74 | "lint-staged": "^15.4.3", 75 | "prettier": "^3.2.4", 76 | "prettier-plugin-organize-imports": "^4.1.0", 77 | "prettier-plugin-tailwindcss": "^0.6.11", 78 | "release-it": "^18.1.2", 79 | "sass-embedded": "^1.85.1", 80 | "terser": "^5.39.0", 81 | "typescript": "~5.7.2", 82 | "unplugin-auto-import": "^19.1.1", 83 | "unplugin-vue-components": "^28.4.1", 84 | "vite": "^6.2.0", 85 | "vite-plugin-imagemin": "^0.6.1", 86 | "vue-tsc": "^2.2.4" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/view/contacts/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 15 | 16 | 21 | 22 | 28 | 29 | 30 | 31 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/assets/svg/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite' 2 | import legacy from '@vitejs/plugin-legacy' 3 | import vue from '@vitejs/plugin-vue' 4 | import path from 'path' 5 | import { visualizer } from 'rollup-plugin-visualizer' // 构建分析工具 6 | import AutoImport from 'unplugin-auto-import/vite' 7 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 8 | import Components from 'unplugin-vue-components/vite' 9 | import { defineConfig } from 'vite' 10 | import viteCompression from 'vite-plugin-compression' // Gzip压缩 11 | import viteImagemin from 'vite-plugin-imagemin' // 图片压缩 12 | 13 | export default defineConfig(({ mode }) => ({ 14 | server: { 15 | proxy: { 16 | '/api': { 17 | // target: 'http://49.232.248.93:3030/', 18 | target: 'http://localhost:3030', 19 | changeOrigin: true, 20 | }, 21 | }, 22 | }, 23 | plugins: [ 24 | vue(), 25 | tailwindcss(), 26 | AutoImport({ 27 | resolvers: [ElementPlusResolver()], 28 | }), 29 | Components({ 30 | resolvers: [ 31 | ElementPlusResolver({ 32 | importStyle: 'sass', // 按需加载样式 33 | directives: true, // 按需加载指令 34 | }), 35 | ], 36 | }), 37 | // Gzip/Brotli 压缩 38 | viteCompression({ 39 | verbose: true, // 显示压缩日志 40 | disable: false, 41 | threshold: 10240, // 大于10KB的文件才压缩 42 | algorithm: 'gzip', // 可选 'brotliCompress' 43 | ext: '.gz', 44 | }), 45 | // 构建产物分析 (打包后会生成 stats.html) 46 | visualizer({ 47 | open: true, 48 | gzipSize: true, 49 | brotliSize: true, 50 | }), 51 | viteImagemin({ 52 | gifsicle: { optimizationLevel: 3 }, 53 | optipng: { optimizationLevel: 5 }, 54 | mozjpeg: { quality: 75 }, 55 | pngquant: { quality: [0.8, 0.9] }, 56 | svgo: { 57 | plugins: [{ removeViewBox: false }], 58 | }, 59 | }), 60 | legacy({ 61 | targets: ['defaults', 'not IE 11'], 62 | }), 63 | ], 64 | resolve: { 65 | alias: { 66 | '@': path.resolve('./src'), 67 | }, 68 | }, 69 | build: { 70 | // 构建优化核心配置 71 | target: 'esnext', 72 | minify: 'terser', // 默认 esbuild,切换为 terser 以支持更多优化 73 | terserOptions: { 74 | compress: { 75 | drop_console: mode === 'production', // 生产环境移除 console 76 | drop_debugger: true, // 移除 debugger 77 | }, 78 | }, 79 | rollupOptions: { 80 | output: { 81 | // 代码分割策略 82 | manualChunks(id) { 83 | if (id.includes('node_modules')) { 84 | // 将大依赖包单独拆分 85 | if (id.includes('element-plus')) { 86 | return 'element' 87 | } 88 | if (id.includes('lodash')) { 89 | return 'lodash' 90 | } 91 | return 'vendor' 92 | } 93 | }, 94 | // 按入口分块 95 | entryFileNames: 'assets/[name]-[hash].js', 96 | chunkFileNames: 'assets/[name]-[hash].js', 97 | assetFileNames: 'assets/[name]-[hash][extname]', 98 | }, 99 | }, 100 | }, 101 | })) 102 | -------------------------------------------------------------------------------- /src/view/contacts/components/set-contect-info-dialog.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 取消 23 | 提交 24 | 25 | 26 | 27 | 28 | 29 | 135 | -------------------------------------------------------------------------------- /src/view/login/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | LightChat 14 | 15 | 16 | 17 | 26 | 27 | 28 | 37 | 38 | 39 | 40 | 47 | 立即登录 48 | 49 | 50 | 51 | 52 | 53 | 54 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/assets/svg/publish.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/List/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | {{ getObjectAttrValue(item, options.label) }} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 130 | 131 | 136 | -------------------------------------------------------------------------------- /src/view/register/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | LightChat 14 | 15 | 16 | 17 | 26 | 27 | 28 | 37 | 38 | 39 | 47 | 立即注册 48 | 49 | 50 | 已有账号,去登录 51 | 52 | 53 | 54 | 55 | 56 | 57 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/hooks/modules/usePageList.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 页面列表 3 | * */ 4 | import { HTTP_CODE } from '@/settings/config/http' 5 | import { isFunction } from '@/utils/helper/is' 6 | import { ElMessage } from 'element-plus' 7 | import type { Ref } from 'vue' 8 | import { computed, ref } from 'vue' 9 | 10 | // 定义接口和类型 11 | interface PageListOptions { 12 | getPageListApi: (params: any) => Promise 13 | searchParams: Ref 14 | dataAppend?: 'start' | 'end' 15 | } 16 | 17 | interface OriginData { 18 | list: T[] 19 | page: number 20 | limit: number 21 | count: number 22 | } 23 | 24 | // 使用泛型 T 来表示列表项的类型 25 | export const usePageList = (options: PageListOptions) => { 26 | // 默认值处理 27 | options = { 28 | dataAppend: 'end', 29 | ...options, 30 | } 31 | 32 | const { getPageListApi, searchParams, dataAppend } = options 33 | 34 | // 定义 appendData 函数 35 | const appendData = (() => { 36 | if (dataAppend === 'start') { 37 | return (rows: T[]) => { 38 | list.value = [...rows, ...list.value] 39 | } 40 | } else if (dataAppend === 'end') { 41 | return (rows: T[]) => { 42 | list.value = [...list.value, ...rows] 43 | } 44 | } 45 | })() 46 | 47 | if (!isFunction(getPageListApi)) { 48 | throw Error(`getPageListApi: ${getPageListApi},需要为函数`) 49 | } 50 | 51 | // 初始数据 52 | const originData: OriginData = { 53 | list: [], 54 | page: 1, 55 | limit: 10, 56 | count: 0, 57 | } 58 | 59 | // 初始化数据 60 | const initData = () => { 61 | list.value = [...originData.list] 62 | page.value = originData.page 63 | limit.value = originData.limit 64 | count.value = originData.count 65 | } 66 | 67 | // 定义响应式变量 68 | const list = ref([...originData.list]) as Ref 69 | const page = ref(originData.page) as Ref 70 | const limit = ref(originData.limit) as Ref 71 | const count = ref(originData.count) as Ref 72 | const loadding = ref(false) as Ref 73 | const refreshing = ref(false) as Ref 74 | 75 | // 获取分页列表数据 76 | const getPageList = async () => { 77 | loadding.value = true 78 | const params = { 79 | ...searchParams.value, 80 | page: page.value, 81 | limit: limit.value, 82 | } 83 | 84 | const res = await getPageListApi({ params }) 85 | .catch(() => { 86 | ElMessage({ 87 | type: 'error', 88 | message: '获取待办列表失败', 89 | duration: 3000, 90 | }) 91 | }) 92 | .finally(() => { 93 | loadding.value = false 94 | }) 95 | 96 | if (res) { 97 | const { code, data, message } = res.data 98 | const { count: total, rows } = data 99 | 100 | if (code === HTTP_CODE.HTTP_SUCCESS_CODE) { 101 | if (rows.length) { 102 | appendData?.(rows) 103 | page.value = page.value + 1 104 | count.value = total 105 | } 106 | } else { 107 | ElMessage({ 108 | type: 'error', 109 | message, 110 | duration: 3000, 111 | }) 112 | } 113 | } 114 | } 115 | 116 | // 刷新列表 117 | const refreshPageList = async () => { 118 | initData() 119 | await getPageList() 120 | } 121 | 122 | // 处理刷新 123 | const handleRefresh = async () => { 124 | refreshing.value = true 125 | await refreshPageList().finally(() => { 126 | refreshing.value = false 127 | }) 128 | } 129 | 130 | // 加载更多 131 | const loadMore = async () => { 132 | if (isNoMore.value) { 133 | return 134 | } 135 | await getPageList() 136 | } 137 | 138 | // 是否已无更多数据 139 | const isNoMore = computed(() => { 140 | return list.value.length >= count.value && page.value > 1 141 | }) 142 | 143 | // 是否无数据 144 | const isEmpty = computed(() => { 145 | return list.value.length === 0 && page.value > 1 146 | }) 147 | 148 | return { 149 | list, 150 | page, 151 | limit, 152 | count, 153 | loadding, 154 | getPageList, 155 | initData, 156 | refreshing, 157 | refreshPageList, 158 | handleRefresh, 159 | loadMore, 160 | isNoMore, 161 | isEmpty, 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/view/contacts/components/contact-detail.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | {{ contact.remark }} 76 | 77 | 78 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {{ contact.desc }} 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 昵称 105 | {{ contact.nickname }} 106 | 107 | 108 | 账号 109 | {{ contact.account }} 110 | 111 | 112 | 113 | 114 | 发消息 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /server/app/socket.js: -------------------------------------------------------------------------------- 1 | const { Server } = require('socket.io') 2 | const { verify } = require('jsonwebtoken') 3 | const { JWT_SECRET } = require('../config/app') 4 | const ContactModel = require('../model/contact') 5 | const ChatModel = require('../model/chat') 6 | const MessageModel = require('../model/message') 7 | const UserModel = require('../model/user') 8 | const chatService = require('../service/chat') 9 | const { Op, literal } = require('sequelize') 10 | const { flatObject } = require('../utils/common') 11 | const moment = require('moment') 12 | 13 | // socket服务器 14 | const io = new Server(3001, { 15 | // 跨域设置 16 | cors: { 17 | origin: '*', 18 | }, 19 | }) 20 | 21 | const socketUserMaps = new Map() 22 | 23 | // 监听客户端连接事件 24 | io.on('connection', (socket) => { 25 | const { auth } = socket.handshake 26 | const { token } = auth 27 | let user = null 28 | try { 29 | user = verify(token, JWT_SECRET) 30 | 31 | // 关联socket用户与网站用户信息 32 | socketUserMaps.set(socket.id, user) 33 | } catch (error) {} 34 | 35 | // client用户端发来消息 36 | socket.on('chat-1v1-to-server', async (message) => { 37 | const { reciver_id } = message 38 | const { id: user_id } = user 39 | 40 | // 1、判断两者是否好友关系 41 | const isFriend = await ContactModel.findOne({ 42 | where: { 43 | [Op.and]: { 44 | user_id, 45 | reciver_id, 46 | }, 47 | [Op.and]: { 48 | reciver_id: user_id, 49 | user_id: reciver_id, 50 | }, 51 | }, 52 | attributes: ['id'], 53 | }) 54 | if (isFriend === null) { 55 | sendSocketMessageToTargetUser(user_id, () => { 56 | socket.emit('server:global-error-message', { 57 | message: '不能发送消息给非好友用户!', 58 | }) 59 | }) 60 | return false 61 | } 62 | 63 | // 2、判断是否有此聊天chat 64 | 65 | // 3、判断是否对方有聊天记录,没有则自动生成一条 66 | // 一般出现在初次加好友后首次给其发送消息 67 | const reciverChat = { 68 | reciver_id: String(user_id), 69 | user_id: reciver_id, 70 | } 71 | const hasChatHistory = await ChatModel.findOne({ 72 | where: reciverChat, 73 | attributes: ['id'], 74 | }) 75 | if (hasChatHistory === null) { 76 | chatService.createChat(reciverChat) 77 | 78 | sendSocketMessageToTargetUser(reciver_id, (socket) => { 79 | socket.emit('server:auto-create-chat', { 80 | message: '聊天时自动生成一次聊天记录', 81 | }) 82 | }) 83 | } 84 | 85 | // 4、写入消息数据局库表 86 | const { content, type } = message 87 | 88 | console.log('message', message) 89 | 90 | debugger 91 | const messageBaseInfo = { 92 | user_id, 93 | reciver_id, 94 | content, 95 | type, 96 | send_time: moment(new Date()).format('YYYY-MM-DD HH:mm'), 97 | } 98 | 99 | const messageRes = await MessageModel.create(messageBaseInfo, { 100 | raw: true, 101 | }) 102 | const { id: message_id, send_time } = messageRes.toJSON() 103 | 104 | // 5、查询相关需要的用户信息组合完整的消息,为转发做准备 105 | UserModel.belongsTo(MessageModel, { 106 | foreignKey: 'id', 107 | targetKey: 'user_id', 108 | }) 109 | 110 | let messageExtraInfo = await UserModel.findOne({ 111 | where: { 112 | id: user_id, 113 | }, 114 | attributes: ['avatar'], 115 | include: { 116 | model: MessageModel, 117 | modelName: 'message', 118 | where: { 119 | id: message_id, 120 | }, 121 | attributes: [[literal("DATE_FORMAT(message.created_time, '%Y-%m-%d %H:%i:%s')"), 'created_time']], 122 | }, 123 | }) 124 | 125 | messageExtraInfo = messageExtraInfo.toJSON() 126 | messageExtraInfo = flatObject(messageExtraInfo, false) 127 | 128 | // 组合完整消息数据 129 | const messageFullInfo = { 130 | id: message_id, 131 | ...messageBaseInfo, 132 | ...messageExtraInfo, 133 | send_time, 134 | is_me: false, 135 | } 136 | 137 | // 6、如果reciver接收消息用户在线且存在socket登录记录信息中, 138 | // 进行socket实时转发到对应reciver接收消息用户 139 | sendSocketMessageToTargetUser(reciver_id, (socket) => { 140 | socket.emit('chat-1v1-to-client', messageFullInfo) 141 | }) 142 | }) 143 | 144 | // socket连接断开 145 | socket.on('disconnect', () => { 146 | // 清理socket用户 147 | socketUserMaps.delete(socket.id) 148 | }) 149 | }) 150 | 151 | // 为指定用户执行socket消息发送 152 | async function sendSocketMessageToTargetUser(id, callback) { 153 | const sockets = await io.fetchSockets() 154 | 155 | sockets.forEach((socket) => { 156 | socketUserMaps.forEach((user, socketId) => { 157 | if (socket.id === socketId && id === user.id) { 158 | callback(socket, user, socketId) 159 | } 160 | }) 161 | }) 162 | } 163 | 164 | module.exports = io 165 | 166 | module.exports.socketUserMaps = socketUserMaps 167 | -------------------------------------------------------------------------------- /src/view/contacts/components/contact-list.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 通讯录 137 | 138 | 139 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | {{ item.remark }} 157 | 158 | 159 | 在线 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 183 | -------------------------------------------------------------------------------- /src/view/contacts/components/add-contact-dialog.vue: -------------------------------------------------------------------------------- 1 | 129 | 130 | 131 | 139 | 140 | 141 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 177 | {{ item.nickname }} 178 | 179 | 180 | 181 | 182 | 加为好友 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 197 | -------------------------------------------------------------------------------- /server/service/message.js: -------------------------------------------------------------------------------- 1 | const { validateFormData, flatObject } = require("../utils/common"); 2 | const { 3 | getMessageListRules, 4 | deleteMessageRules, 5 | updateMessageReadRules, 6 | } = require("../validate/message"); 7 | const { ValidateError } = require("../middleware/errorHandler"); 8 | const MessageModel = require("../model/message"); 9 | const MessageReadModel = require("../model/message_read"); 10 | const MessageDelete = require("../model/message_delete"); 11 | const UserModel = require("../model/user"); 12 | const { Op, literal } = require("sequelize"); 13 | 14 | class Message { 15 | /** 16 | * 获取聊天消息列表 17 | * @param {*} data 18 | * @returns 19 | */ 20 | getMessageList = async (data) => { 21 | // 表单数据格式校验 22 | const { errors, message } = await validateFormData( 23 | data, 24 | getMessageListRules 25 | ); 26 | 27 | if (errors) { 28 | const error = new ValidateError(message.text); 29 | error.payload = message; 30 | throw error; 31 | } 32 | 33 | const { user_id, reciver_id, keywords, type, status, page, limit } = data; 34 | 35 | // 查询条件组装 36 | const where = { 37 | [Op.or]: [ 38 | { 39 | user_id, 40 | reciver_id, 41 | }, 42 | { 43 | user_id: reciver_id, 44 | reciver_id: user_id, 45 | }, 46 | ], 47 | }; 48 | 49 | if (keywords) { 50 | where[Op.or] = [ 51 | ...where[Op.or], 52 | { 53 | content: { 54 | [Op.like]: `%${keywords}%`, 55 | }, 56 | }, 57 | ]; 58 | } 59 | 60 | if (type) { 61 | where.type = type; 62 | } 63 | 64 | // if (status) { 65 | // where.status = status; 66 | // } 67 | 68 | // 查询用户已删除与reciver用户的消息 69 | const deleteMessageList = await MessageDelete.findAll({ 70 | where: { 71 | user_id, 72 | }, 73 | attributes: ["message_id"], 74 | raw: true, 75 | }); 76 | const deleteMessageIds = deleteMessageList.map((message) => message.message_id); 77 | 78 | if (deleteMessageIds.length) { 79 | where.id = { 80 | [Op.notIn]: deleteMessageIds 81 | }; 82 | } 83 | 84 | MessageModel.belongsTo(UserModel, { 85 | foreignKey: "user_id", 86 | targetKey: "id", 87 | }); 88 | 89 | MessageModel.belongsTo(MessageReadModel, { 90 | foreignKey: "id", 91 | targetKey: "message_id", 92 | }); 93 | 94 | const res = await MessageModel.findAndCountAll({ 95 | modelName: "message", 96 | where, 97 | attributes: [ 98 | "id", 99 | "content", 100 | "type", 101 | "send_time", 102 | [ 103 | literal( 104 | `CASE WHEN message.user_id = ${user_id} THEN TRUE ELSE FALSE END` 105 | ), 106 | "is_me", 107 | ], 108 | ], 109 | include: [ 110 | { 111 | model: UserModel, 112 | attributes: ["avatar"], 113 | }, 114 | { 115 | model: MessageReadModel, 116 | attributes: [ 117 | [ 118 | literal( 119 | `CASE WHEN message_read.user_id = ${user_id} THEN 1 ELSE 0 END` 120 | ), 121 | "read", 122 | ], // 添加一个虚拟字段,表示消息是否已读 123 | ], 124 | }, 125 | ], 126 | order: [["id", "DESC"]], 127 | offset: (page - 1) * limit, 128 | limit, 129 | }); 130 | 131 | res.rows = res.rows.map((contact) => contact.get({ plain: true })); 132 | 133 | if (res?.rows) { 134 | res.rows = res.rows.map((message) => { 135 | message.is_me = Boolean(message.is_me); 136 | return flatObject(message, true, "", ["status", "avatar", "read"]); 137 | }); 138 | 139 | res.rows = res.rows.reverse(); 140 | } 141 | 142 | return res; 143 | }; 144 | 145 | /** 146 | * 删除聊天消息 147 | * @param {*} data 148 | * @returns 149 | */ 150 | deleteMessage = async (data) => { 151 | // 表单数据格式校验 152 | const { errors, message } = await validateFormData( 153 | data, 154 | deleteMessageRules 155 | ); 156 | 157 | if (errors) { 158 | const error = new ValidateError(message.text); 159 | error.payload = message; 160 | throw error; 161 | } 162 | 163 | const { id, user_id } = data; 164 | 165 | const res = await MessageModel.update( 166 | { 167 | is_delete: "1", 168 | }, 169 | { 170 | where: { 171 | id, 172 | user_id, 173 | is_delete: "0", 174 | }, 175 | } 176 | ); 177 | 178 | return res[0]; 179 | }; 180 | 181 | /** 182 | * 更改聊天消息阅读状态 183 | * @param {*} data 184 | * @returns 185 | */ 186 | // updateMessageRead = async (data) => { 187 | // // 表单数据格式校验 188 | // const { errors, message } = await validateFormData( 189 | // data, 190 | // updateMessageReadRules 191 | // ); 192 | 193 | // if (errors) { 194 | // const error = new ValidateError(message.text); 195 | // error.payload = message; 196 | // throw error; 197 | // } 198 | 199 | // const { id, user_id, status, read_time } = data; 200 | 201 | // const res = await MessageModel.update( 202 | // { 203 | // status, 204 | // read_time, 205 | // }, 206 | // { 207 | // where: { 208 | // id, 209 | // user_id, 210 | // status: "0", 211 | // is_delete: "0", 212 | // }, 213 | // } 214 | // ); 215 | 216 | // return res[0]; 217 | // }; 218 | } 219 | 220 | module.exports = new Message(); 221 | -------------------------------------------------------------------------------- /server/service/contact.js: -------------------------------------------------------------------------------- 1 | const { validateFormData, flatObject } = require("../utils/common"); 2 | const { 3 | createContactRules, 4 | deleteContactRules, 5 | setInfoRules, 6 | } = require("../validate/contact"); 7 | const { ValidateError, DataError } = require("../middleware/errorHandler"); 8 | const ContactModel = require("../model/contact"); 9 | const UserModel = require("../model/user"); 10 | const { Op } = require("sequelize"); 11 | const db = require("../db"); 12 | 13 | class Contact { 14 | /** 15 | * 添加好友 16 | * @param {*} user 17 | * @returns 18 | */ 19 | createContact = async (user) => { 20 | // 表单数据格式校验 21 | const { errors, message } = await validateFormData( 22 | user, 23 | createContactRules 24 | ); 25 | 26 | if (errors) { 27 | const error = new ValidateError(message.text); 28 | error.payload = message; 29 | throw error; 30 | } 31 | 32 | // 检查是否添加自己为好友 33 | const isSelf = String(user.user_id) === String(user.reciver_id); 34 | if (isSelf) { 35 | const message = "不可添加自己为好友"; 36 | const error = new DataError(message); 37 | error.payload = { 38 | code: 300001002, 39 | text: message, 40 | }; 41 | throw error; 42 | } 43 | 44 | // 检查是否已经为好友 45 | const isFriend = await ContactModel.findOne({ 46 | where: user, 47 | attributes: ["id"], 48 | }); 49 | if (isFriend !== null) { 50 | const message = "不可重复添加为好友"; 51 | const error = new DataError(message); 52 | error.payload = { 53 | code: 300001003, 54 | text: message, 55 | }; 56 | throw error; 57 | } 58 | 59 | let res; 60 | try { 61 | res = await db.transaction(async (t) => { 62 | let result 63 | // 添加好友 64 | result = await ContactModel.create(user, { transaction: t }); 65 | 66 | // 如果对方与当前用户不存在好友关系(可能以前两人为好友,但当前用户单边删除过好友), 67 | // 为对方也自动添加当前用户为联系人 68 | const reciver = { 69 | user_id: user.reciver_id, 70 | reciver_id: user.user_id, 71 | } 72 | const isFriend = await ContactModel.findOne({ 73 | where: reciver, 74 | attributes: ["id"], 75 | }); 76 | if (isFriend === null) { 77 | result = await ContactModel.create( 78 | reciver, 79 | { transaction: t } 80 | ); 81 | } 82 | 83 | return result; 84 | }); 85 | } catch (error) { 86 | // 如果执行到此,则发生错误. 87 | // 该事务已由 Sequelize 自动回滚! 88 | } 89 | 90 | return res; 91 | }; 92 | 93 | /** 94 | * 获取联系人列表 95 | * @param {*} data 96 | * @returns 97 | */ 98 | getContactList = async (data) => { 99 | const { user_id, keywords, type, page, limit } = data; 100 | 101 | // 查询条件组装 102 | const where = { 103 | user_id, 104 | }; 105 | 106 | if (keywords) { 107 | where[Op.or] = [ 108 | { 109 | remark: { 110 | [Op.like]: `%${keywords}%`, 111 | }, 112 | }, 113 | ]; 114 | } 115 | 116 | if (type) { 117 | where.type = type; 118 | } 119 | 120 | ContactModel.belongsTo(UserModel, { 121 | foreignKey: "reciver_id", 122 | targetKey: "id", 123 | }); 124 | 125 | const res = await ContactModel.findAndCountAll({ 126 | where, 127 | attributes: ["id", "reciver_id", "remark", "desc"], 128 | include: [ 129 | { 130 | model: UserModel, 131 | attributes: ["account", "nickname", "avatar"], 132 | }, 133 | ], 134 | order: [["id", "DESC"]], 135 | offset: (page - 1) * limit, 136 | limit, 137 | }); 138 | 139 | res.rows = res.rows.map((contact) => contact.get({ plain: true })); 140 | 141 | if (res?.rows) { 142 | res.rows = res.rows.map((contact) => { 143 | return flatObject(contact, true, "", [ 144 | "account", 145 | "nickname", 146 | "avatar", 147 | "remark", 148 | ]); 149 | }); 150 | 151 | // 生成聊天最终显示的remark 152 | res.rows = res.rows.map((contact) => { 153 | if (["", null].includes(contact.remark)) { 154 | if (contact.nickname) { 155 | contact.remark = contact.nickname; 156 | } else if (contact.account) { 157 | contact.remark = contact.account; 158 | } 159 | } 160 | 161 | return contact; 162 | }); 163 | } 164 | 165 | return res; 166 | }; 167 | 168 | /** 169 | * 删除联系人 170 | * @param {*} data 171 | * @returns 172 | */ 173 | deleteContact = async (data) => { 174 | // 表单数据格式校验 175 | const { errors, message } = await validateFormData( 176 | data, 177 | deleteContactRules 178 | ); 179 | 180 | if (errors) { 181 | const error = new ValidateError(message.text); 182 | error.payload = message; 183 | throw error; 184 | } 185 | 186 | const { id, user_id } = data; 187 | 188 | const res = await ContactModel.destroy({ 189 | force: true, 190 | where: { 191 | id, 192 | user_id, 193 | }, 194 | }); 195 | 196 | return res; 197 | }; 198 | 199 | /** 200 | * 联系人资料设置 201 | * @param {*} info 202 | * @returns 203 | */ 204 | setInfo = async (info) => { 205 | // 表单数据格式校验 206 | const { errors, message } = await validateFormData(info, setInfoRules); 207 | 208 | if (errors) { 209 | const error = new ValidateError(message.text); 210 | error.payload = message; 211 | throw error; 212 | } 213 | 214 | const { id, user_id } = info; 215 | 216 | delete info.id, 217 | delete info.user_id 218 | 219 | const res = await ContactModel.update( 220 | info, 221 | { 222 | where: { 223 | id, 224 | user_id, 225 | }, 226 | } 227 | ); 228 | 229 | return res[0]; 230 | }; 231 | } 232 | 233 | module.exports = new Contact(); 234 | -------------------------------------------------------------------------------- /server/service/user.js: -------------------------------------------------------------------------------- 1 | const { validateFormData } = require("../utils/common"); 2 | const { 3 | createUserRules, 4 | userLoginRules, 5 | uploadAvaterRules, 6 | updateInfoRules, 7 | searchUserRules, 8 | } = require("../validate/user"); 9 | const { ValidateError, DataError } = require("../middleware/errorHandler"); 10 | const UserModel = require("../model/user"); 11 | const bcrypt = require("bcrypt"); 12 | const { sign } = require("jsonwebtoken"); 13 | const { JWT_SECRET } = require("../config/app"); 14 | const path = require("path"); 15 | const process = require("process"); 16 | const fs = require("fs"); 17 | const moment = require("moment"); 18 | 19 | class User { 20 | /** 21 | * 创建用户账号 22 | * @param {*} user 23 | * @returns 24 | */ 25 | createUser = async (user) => { 26 | // 表单数据格式校验 27 | const { errors, message } = await validateFormData(user, createUserRules); 28 | 29 | if (errors) { 30 | const error = new ValidateError(message.text); 31 | error.payload = message; 32 | throw error; 33 | } 34 | 35 | // 生成密码加密salt盐值 36 | const salt = bcrypt.genSaltSync(10); 37 | user.salt = salt; 38 | user.password = bcrypt.hashSync(user.password, salt); 39 | user.birthday = moment(new Date()).format("YYYY-MM-DD"); 40 | 41 | 42 | const res = await UserModel.create(user).catch((err) => { 43 | console.log(err); 44 | }); 45 | 46 | return res; 47 | }; 48 | 49 | /** 50 | * 用户登录 51 | * @param {*} user 52 | * @returns 53 | */ 54 | login = async (user) => { 55 | // 表单数据格式校验 56 | const { errors, message } = await validateFormData(user, userLoginRules); 57 | 58 | if (errors) { 59 | const error = new ValidateError(message.text); 60 | error.payload = message; 61 | throw error; 62 | } 63 | 64 | // 使用account查询数据库对应账号信息 65 | const userRecord = await UserModel.findOne({ 66 | where: { account: user.account }, 67 | }); 68 | if (!userRecord) { 69 | const message = "账号不存在"; 70 | const error = new DataError(message); 71 | error.payload = { 72 | code: 100002003, 73 | text: message, 74 | }; 75 | throw error; 76 | } 77 | 78 | const { id, account, password, avatar, sex, birthday, nickname } = userRecord; 79 | 80 | // 对用户输入密码进行加密转换后与数据库记录比较是否一致 81 | const matched = bcrypt.compareSync(user.password, password); 82 | if (!matched) { 83 | const message = "账号或密码错误"; 84 | const error = new DataError(message); 85 | error.payload = { 86 | code: 100002004, 87 | text: message, 88 | }; 89 | throw error; 90 | } 91 | 92 | // 生成token 93 | const info = { 94 | id, 95 | account, 96 | avatar, 97 | sex, 98 | birthday, 99 | nickname, 100 | }; 101 | const token = sign(info, JWT_SECRET, { expiresIn: "1d" }); 102 | 103 | const loginRes = { 104 | token, 105 | info, 106 | }; 107 | return loginRes; 108 | }; 109 | 110 | /** 111 | * 上传头像文件 112 | * @param {*} data 113 | * @returns 114 | */ 115 | uploadAvater = async (data) => { 116 | const { user_id, file } = data; 117 | // 表单数据格式校验 118 | const { errors, message } = await validateFormData(data, uploadAvaterRules); 119 | 120 | if (errors) { 121 | const error = new ValidateError(message.text); 122 | error.payload = message; 123 | // 清除不合理文件 124 | fs.promises 125 | .unlink(path.join(process.cwd(), `/upload/${file.newFilename}`)) 126 | .catch(() => {}); 127 | throw error; 128 | } 129 | 130 | // 保存文件 131 | try { 132 | await fs.promises.mkdir(path.join(process.cwd(), "/upload/avatar"), { 133 | recursive: true, 134 | }); 135 | } catch (error) { 136 | throw new Error("上传头像失败!"); 137 | } 138 | const uploadDir = path.join(process.cwd(), "/upload/avatar"); 139 | const targetPath = path.join(uploadDir, file.newFilename); 140 | fs.renameSync(file.filepath, targetPath); 141 | 142 | const avaterPath = `/upload/avatar/${file.newFilename}`; 143 | 144 | // 更新数据库信息 145 | const res = await UserModel.update( 146 | { 147 | avatar: avaterPath, 148 | }, 149 | { 150 | where: { 151 | id: user_id, 152 | status: "0", 153 | }, 154 | } 155 | ); 156 | 157 | if (!res[0]) { 158 | // 清除文件 159 | fs.promises.unlink(path.join(process.cwd(), avaterPath)).catch(() => {}); 160 | 161 | throw new Error("更新头像信息失败!"); 162 | } 163 | 164 | return avaterPath; 165 | }; 166 | 167 | /** 168 | * 更新用户信息 169 | * @param {*} info 170 | * @returns 171 | */ 172 | updateInfo = async (info) => { 173 | // 表单数据格式校验 174 | const { errors, message } = await validateFormData(info, updateInfoRules); 175 | 176 | if (errors) { 177 | const error = new ValidateError(message.text); 178 | error.payload = message; 179 | throw error; 180 | } 181 | 182 | const { user_id } = info; 183 | 184 | const res = await UserModel.update(info, { 185 | where: { 186 | id: user_id, 187 | status: "0", 188 | is_delete: "0", 189 | }, 190 | }); 191 | 192 | return res[0]; 193 | }; 194 | 195 | /** 196 | * 搜索用户列表 197 | * @param {*} data 198 | * @returns 199 | */ 200 | searchUser = async (data) => { 201 | // 表单数据格式校验 202 | const { errors, message } = await validateFormData(data, searchUserRules); 203 | 204 | if (errors) { 205 | const error = new ValidateError(message.text); 206 | error.payload = message; 207 | throw error; 208 | } 209 | 210 | const { account, page, limit } = data; 211 | 212 | // 查询条件组装 213 | const where = { 214 | account, 215 | status: "0", 216 | is_delete: "0", 217 | }; 218 | 219 | const res = await UserModel.findAndCountAll({ 220 | where, 221 | attributes: ["id", "account", "nickname", "avatar"], 222 | offset: (page - 1) * limit, 223 | limit, 224 | }); 225 | 226 | if (res?.rows) { 227 | res.rows = res.rows.map((user) => { 228 | if (["", null].includes(user.nickname)) { 229 | user.nickname = user.account; 230 | } 231 | 232 | return user; 233 | }); 234 | } 235 | 236 | return res; 237 | }; 238 | } 239 | 240 | module.exports = new User(); 241 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.2 (2025-03-10) 4 | 5 | * build: 🔨️打包构建进行图片压缩 ([560b7e4](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/560b7e4)) 6 | * build: 🔨️首屏 JS 体积1.2M->350KB ([b1bdb65](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/b1bdb65)) 7 | * build: 🔨️优化打包构建核心配置,采用代码分割策略 ([0fc061e](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/0fc061e)) 8 | * build: 🔨️sass依赖缺失 ([438cef1](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/438cef1)) 9 | * feat: ✨️初始化笔记模块mock ([f5fd7d2](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/f5fd7d2)) 10 | * feat: ✨️添加朋友圈原型 ([392a6ef](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/392a6ef)) 11 | * feat: ✨️新增应用设置原型 ([3eddf25](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/3eddf25)) 12 | * feat: ✨️新增用户信息设置以及展示的原型 ([430be93](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/430be93)) 13 | * feat: ✨️svg图标渲染组件,确定项目图标使用方案 ([a42d10c](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/a42d10c)) 14 | * ci: 📌️vercel添加API重写规则指向服务器地址 ([de484e5](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/de484e5)) 15 | * Update README.md ([622f658](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/622f658)) 16 | * Update README.md ([13a79e0](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/13a79e0)) 17 | * fix: 🪲️用户头像显示异常 ([2eaac54](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/2eaac54)) 18 | * test: 🧪️测试后端服务器接口成功 ([4992f8e](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/4992f8e)) 19 | 20 | ## 1.0.1 (2025-03-07) 21 | 22 | * feat: ✨侧边栏开发 ([781a45a](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/781a45a)) 23 | * feat: ✨️初始化 v1 版本相关接口(入参、反参类型暂时 any 处理) ([149449d](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/149449d)) 24 | * feat: ✨️初始化跟容器以及子容器基础样式 ([d4b1b7b](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/d4b1b7b)) 25 | * feat: ✨️代理转发,联调接口 ([e25de60](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/e25de60)) 26 | * feat: ✨️登录注册开发 ([d7872e3](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/d7872e3)) 27 | * feat: ✨️定制C端分栏组件 ([6ed201b](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/6ed201b)) 28 | * feat: ✨️构建路由组织模式,区分权限路由及白名单路由 ([0a99a1a](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/0a99a1a)) 29 | * feat: ✨️简单初始化axios配置,携带token ([d64a1cb](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/d64a1cb)) 30 | * feat: ✨️开发历史聊天对话模块, 调试分页的上拉加载功能 ([6c934a6](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/6c934a6)) 31 | * feat: ✨️开发添加好友,好友详情,详情修改功能 ([67d386b](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/67d386b)) 32 | * feat: ✨️开发用户列表,封装异步组件hooks ([e3ec547](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/e3ec547)) 33 | * feat: ✨️联调 socket 发送消息接受消息 ([b644543](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/b644543)) 34 | * feat: ✨️联调通讯录 ([4843f20](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/4843f20)) 35 | * feat: ✨️联调socket,登录后连接 socket ([014ffd7](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/014ffd7)) 36 | * feat: ✨️添加聊天相关模拟接口及类型定义 ([47c3f19](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/47c3f19)) 37 | * feat: ✨️添加用户注册和登录的模拟接口实现,包含随机token生成和用户信息返回。 ([01a1267](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/01a1267)) 38 | * feat: ✨️新增表情选择组件 ([abf9a17](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/abf9a17)) 39 | * feat: ✨️新增获取当前实例、下拉加载更多、分页拉取数据钩子,新增列表组件 ([c5b17ad](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/c5b17ad)) 40 | * feat: ✨️新增聊天记录存储模块,添加通用工具和数据处理工具,完善数据类型检测工具,新增消息模拟接口 ([2db6f38](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/2db6f38)) 41 | * feat: ✨️新增通讯录模拟接口 ([9314d5c](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/9314d5c)) 42 | * feat: ✨️新增主题切换钩子(做缓存),基于Tailwindcss(.dark)方案,优先走缓存 ([59c94a2](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/59c94a2)) 43 | * feat: ✨️选择表情组件触发过程中 input 输入框确保不要失去焦点 ([431a0aa](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/431a0aa)) 44 | * feat: 🎯️新增 .release-it.json 配置,集成 conventional-changelog ([858a0ae](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/858a0ae)) 45 | * chore-✨️初始化项目 ([a04c093](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/a04c093)) 46 | * Update README.md ([075bae9](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/075bae9)) 47 | * fix: 🪲️插件命名与vue组件实例$data冲突,导致无法正常使用 ([a0a11b1](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/a0a11b1)) 48 | * fix: 🪲️可以在文本任意激活光标处插入表情 ([2233e79](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/2233e79)) 49 | * fix: 🪲️提取缓存token回存store ([b28991d](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/b28991d)) 50 | * fix: 🪲️修复返回聊天未激活上一次聊天窗口问题 ([ae53d91](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/ae53d91)) 51 | * fix: 🪲️修复样式显示问题 ([4b76908](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/4b76908)) 52 | * fix: 🪲️暂时修复分栏组件的ElScrollbar的渲染失效问题(原因暂时未知),完成历史会话加载开发 ([50dc3dc](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/50dc3dc)) 53 | * perf: 📜️(#1) 处理CR中的将原生的 CSS 语法转换为 Tailwindcss ([b65ac39](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/b65ac39)), closes [#1](https://github.com/ZRMYDYCG/llm-go-chat-client/issues/1) 54 | * perf: 📜️(#1)处理CR中箭头函数命名问题 ([16d25e4](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/16d25e4)), closes [#1](https://github.com/ZRMYDYCG/llm-go-chat-client/issues/1) 55 | * perf: 📜️(#1)支持白天黑夜模式切换 ([7f87b1c](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/7f87b1c)), closes [#1](https://github.com/ZRMYDYCG/llm-go-chat-client/issues/1) 56 | * test: 🎯️测试@vueuse的colorMode切换钩子 ([1cfb02d](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/1cfb02d)) 57 | * ci: 📌️修改 deploy.yml ([0881978](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/0881978)) 58 | * ci: 📌️修改deploy.yml ([83c9a79](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/83c9a79)) 59 | * ci: 📌️修改deploy.yml ([804eb00](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/804eb00)) 60 | * ci: 📌️CI/CD 初始化 ([0a64376](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/0a64376)) 61 | * docs: 📖️Update README ([d58dac8](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/d58dac8)) 62 | * chore: 🎯️构建插件化模式 ([5a9fa11](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/5a9fa11)) 63 | * build: 🔨️搭建开发架构 ([46a4a82](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/46a4a82)) 64 | * build: 🔨️集成一些开发依赖 ([eb1322c](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/eb1322c)) 65 | * build: 🔨️Tailwind & ESlint & Prettier & Husky & lint-staged & commitlint 统一团队开发风格 ([a9d7272](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/a9d7272)) 66 | * other: 🎯️确定使用的前端组件库 ([04d7ea2](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/04d7ea2)) 67 | * other: 🎯️添加项目样式方案以及类型定义 ([5c90962](https://github.com/ZRMYDYCG/llm-go-chat-client/commit/5c90962)) 68 | -------------------------------------------------------------------------------- /server/init.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Source Server : localhost 5 | Source Server Version : 50562 6 | Source Host : localhost:3306 7 | Source Database : go_chat 8 | 9 | Target Server Type : MYSQL 10 | Target Server Version : 50562 11 | File Encoding : 65001 12 | 13 | Date: 2024-11-07 09:04:19 14 | */ 15 | 16 | SET FOREIGN_KEY_CHECKS=0; 17 | 18 | -- ---------------------------- 19 | -- Table structure for `gc_chat` 20 | -- ---------------------------- 21 | DROP TABLE IF EXISTS `gc_chat`; 22 | CREATE TABLE `gc_chat` ( 23 | `id` int(11) NOT NULL AUTO_INCREMENT, 24 | `user_id` int(11) NOT NULL COMMENT '用户id', 25 | `reciver_id` int(11) NOT NULL COMMENT '接收用户或群id', 26 | `type` enum('0','1') NOT NULL DEFAULT '0' COMMENT '聊天类型(01v1 1群聊)', 27 | `created_time` datetime NOT NULL, 28 | `update_time` datetime NOT NULL, 29 | PRIMARY KEY (`id`) 30 | ) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8; 31 | 32 | -- ---------------------------- 33 | -- Records of gc_chat 34 | -- ---------------------------- 35 | 36 | -- ---------------------------- 37 | -- Table structure for `gc_contact` 38 | -- ---------------------------- 39 | DROP TABLE IF EXISTS `gc_contact`; 40 | CREATE TABLE `gc_contact` ( 41 | `id` int(11) NOT NULL AUTO_INCREMENT, 42 | `user_id` int(11) NOT NULL COMMENT '用户id', 43 | `reciver_id` int(11) NOT NULL COMMENT '联系人或群id', 44 | `remark` char(50) DEFAULT NULL COMMENT '联系人备注名', 45 | `desc` char(150) DEFAULT NULL COMMENT '联系人描述信息', 46 | `created_time` datetime NOT NULL, 47 | `update_time` datetime NOT NULL, 48 | PRIMARY KEY (`id`) 49 | ) ENGINE=InnoDB AUTO_INCREMENT=58 DEFAULT CHARSET=utf8; 50 | 51 | -- ---------------------------- 52 | -- Records of gc_contact 53 | -- ---------------------------- 54 | 55 | -- ---------------------------- 56 | -- Table structure for `gc_message` 57 | -- ---------------------------- 58 | DROP TABLE IF EXISTS `gc_message`; 59 | CREATE TABLE `gc_message` ( 60 | `id` int(11) NOT NULL AUTO_INCREMENT, 61 | `user_id` int(11) NOT NULL COMMENT '用户id', 62 | `reciver_id` int(11) NOT NULL COMMENT '接收用户或群id', 63 | `content` text NOT NULL COMMENT '消息内容', 64 | `type` enum('0','1','2','3') NOT NULL DEFAULT '0' COMMENT '消息类型(0文本 1图片 2语音 3视频)', 65 | `send_time` datetime DEFAULT NULL COMMENT '发送时间', 66 | `created_time` datetime NOT NULL, 67 | `update_time` datetime NOT NULL, 68 | PRIMARY KEY (`id`) 69 | ) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8; 70 | 71 | -- ---------------------------- 72 | -- Records of gc_message 73 | -- ---------------------------- 74 | 75 | -- ---------------------------- 76 | -- Table structure for `gc_message_delete` 77 | -- ---------------------------- 78 | DROP TABLE IF EXISTS `gc_message_delete`; 79 | CREATE TABLE `gc_message_delete` ( 80 | `id` int(11) NOT NULL AUTO_INCREMENT, 81 | `message_id` int(11) NOT NULL COMMENT '聊天消息id', 82 | `user_id` int(11) NOT NULL COMMENT '用户id', 83 | `is_delete` enum('0','1') NOT NULL DEFAULT '0' COMMENT '是否删除(0正常 1软删除)', 84 | `delete_time` datetime NOT NULL COMMENT '删除时间', 85 | `created_time` datetime NOT NULL, 86 | `update_time` datetime NOT NULL, 87 | PRIMARY KEY (`id`) 88 | ) ENGINE=InnoDB AUTO_INCREMENT=146 DEFAULT CHARSET=utf8; 89 | 90 | -- ---------------------------- 91 | -- Records of gc_message_delete 92 | -- ---------------------------- 93 | 94 | -- ---------------------------- 95 | -- Table structure for `gc_message_read` 96 | -- ---------------------------- 97 | DROP TABLE IF EXISTS `gc_message_read`; 98 | CREATE TABLE `gc_message_read` ( 99 | `id` int(11) NOT NULL AUTO_INCREMENT, 100 | `message_id` int(11) NOT NULL COMMENT '聊天消息id', 101 | `user_id` int(11) NOT NULL COMMENT '用户id', 102 | `read_time` datetime DEFAULT NULL COMMENT '阅读时间', 103 | `created_time` datetime NOT NULL, 104 | `update_time` datetime NOT NULL, 105 | PRIMARY KEY (`id`) 106 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 107 | 108 | -- ---------------------------- 109 | -- Records of gc_message_read 110 | -- ---------------------------- 111 | 112 | -- ---------------------------- 113 | -- Table structure for `gc_user` 114 | -- ---------------------------- 115 | DROP TABLE IF EXISTS `gc_user`; 116 | CREATE TABLE `gc_user` ( 117 | `id` int(11) NOT NULL AUTO_INCREMENT, 118 | `account` char(50) NOT NULL COMMENT '账号', 119 | `password` char(64) NOT NULL COMMENT '密码', 120 | `salt` char(100) NOT NULL COMMENT '密码盐', 121 | `avatar` char(255) DEFAULT NULL COMMENT '用户头像', 122 | `nickname` char(30) DEFAULT NULL COMMENT '名称(昵称)', 123 | `sex` enum('0','1','2') NOT NULL DEFAULT '0' COMMENT '待办完成状态(0女 1男 2未知)', 124 | `birthday` date NOT NULL COMMENT '生日', 125 | `status` enum('0','1') NOT NULL DEFAULT '0' COMMENT '账号状态(0正常 1已冻结)', 126 | `is_delete` enum('0','1') NOT NULL DEFAULT '0' COMMENT '是否删除(0正常 1软删除)', 127 | `created_time` datetime NOT NULL, 128 | `update_time` datetime NOT NULL, 129 | PRIMARY KEY (`id`), 130 | UNIQUE KEY `account` (`account`) 131 | ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8; 132 | 133 | -- ---------------------------- 134 | -- Records of gc_user 135 | -- ---------------------------- 136 | INSERT INTO `gc_user` VALUES ('1', 'chat_6666', '$2b$10$oipF0MErMFMbjtk4MgNH2O5X2aAyvn5NDYA9xUnhZ9eSRGdfl2Bri', '$2b$10$oip', '/upload/avatar/_W-I5camAjSIMXEg8_0rJcGW31t1dxN7snrSpvDNbJQ.jpg', '唐三藏', '0', '2024-10-07', '0', '0', '2024-10-07 17:48:36', '2024-10-07 17:48:36'); 137 | INSERT INTO `gc_user` VALUES ('2', 'chat_5555', '$2b$10$/7QUexGWhp9hR4bo1slsReNwXsZgHS5csFOt7e98jJlNEz49cO0kC', '$2b$10$/7Q', '/upload/avatar/ATCvEupT4mf6aVM244AyMR9yhCic7uQK4zMlboWqvk0.jpg', null, '0', '2024-10-07', '0', '0', '2024-10-07 17:48:42', '2024-10-07 17:48:42'); 138 | INSERT INTO `gc_user` VALUES ('3', 'chat_4444', '$2b$10$rjEt74qcVRa8SGMGYamVuu7IrZbmHpugGJ7uD5PCm7lXHgSe9rY9S', '$2b$10$rjE', '/upload/avatar/hyXg68FDYLRVi29OP2kDqhWh-zX_ihWi_DSSHlP4GiI.jpg', '白龙马', '0', '2024-10-07', '0', '0', '2024-10-07 17:48:45', '2024-10-07 17:48:45'); 139 | INSERT INTO `gc_user` VALUES ('4', 'chat_3333', '$2b$10$0nludai3vprGUmP8d0zEk.IofXzli6n8fo2AdgxeSRjCvjxj8vWH6', '$2b$10$0nl', '/upload/avatar/IzUpQGGzP8m9Ekn6ht3L8lBkeHe_iB_9HPy4-Qwufyo.jpg', '沙悟净', '0', '2024-10-07', '0', '0', '2024-10-07 17:48:51', '2024-10-07 17:48:51'); 140 | INSERT INTO `gc_user` VALUES ('5', 'chat_2222', '$2b$10$rKoH2y8EA57VPhzCHrLLxe/FqL0fx9AlLRKcpjF2RUbC1q.4czXQS', '$2b$10$rKo', '/upload/avatar/KJLFvLdFOF_htMTjjxTt-JEovGOAPCIl2p2eBPetKuQ.jpg', '猪悟能', '0', '2024-10-07', '0', '0', '2024-10-07 17:48:55', '2024-10-07 17:48:55'); 141 | INSERT INTO `gc_user` VALUES ('6', 'chat_1111', '$2b$10$jC0REAld8qDo/kfF4TOkI.IUDv41c8HOvVhPYHyCB8HSdhvIF2OQy', '$2b$10$jC0', '/upload/avatar/o4EA0jGx7AwD7D5-ucGi474kmn2fHAuAKU_Ouog8sT4.jpg', '孙悟空', '0', '2024-10-07', '0', '0', '2024-10-07 17:49:00', '2024-10-07 17:49:00'); 142 | 143 | \! touch /var/lib/mysql/.initialized 144 | -------------------------------------------------------------------------------- /src/view/chat/components/chat-history.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 聊天记录 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{ item.remark }} 38 | {{ 39 | $dataHelpers.formatDate(item.send_time, 'HH:mm') 40 | }} 41 | 42 | 43 | 44 | 45 | {{ item.last_message }} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 置顶聊天 62 | 删除 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 220 | 221 | 234 | --------------------------------------------------------------------------------
156 | {{ item.remark }} 157 |
在线
177 | {{ item.nickname }} 178 |
{{ item.remark }}
45 | {{ item.last_message }} 46 |