├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── backend ├── app.js ├── bin │ └── www ├── controller │ ├── friendly.js │ ├── group.js │ ├── message.js │ └── user.js ├── init.js ├── middleware │ └── tokenCheck.js ├── models │ ├── accountBase.js │ ├── expression.js │ ├── friendly.js │ ├── group.js │ ├── groupUser.js │ ├── message.js │ ├── mobilePhone.js │ └── user.js ├── package-lock.json ├── package.json ├── public │ ├── img │ │ ├── avatar.jpg │ │ └── group.jpg │ ├── stylesheets │ │ └── style.css │ └── uploads │ │ └── 2021-06-10 │ │ ├── file-1623305062726-QQ图片20210312002220.png │ │ ├── file-1623305634180-QQ图片20210312002220.png │ │ └── file-1623305648842-基于vue.docx ├── routes │ ├── friendly.js │ ├── group.js │ ├── index.js │ ├── message.js │ ├── upload.js │ └── user.js ├── service │ ├── initData.js │ ├── picCode.js │ └── socket.js ├── utils │ ├── connect.js │ ├── jwt.js │ ├── tools.js │ └── upload.js └── views │ ├── error.ejs │ └── index.ejs └── frontend ├── .browserslistrc ├── .env ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.js ├── build └── index.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── friendly.js │ ├── group.js │ ├── index.js │ ├── modules │ │ ├── friendly.js │ │ ├── group.js │ │ ├── upload.js │ │ ├── url.js │ │ └── user.js │ ├── upload.js │ ├── url.js │ └── user.js ├── assets │ ├── css │ │ └── icon.css │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.svg │ │ ├── icomoon.ttf │ │ └── icomoon.woff │ ├── imgs │ │ ├── Nofifications and Sounds.png │ │ ├── Privacy and Storage.png │ │ ├── Saved Message.png │ │ ├── bgi.jpg │ │ ├── error-img.png │ │ └── file.png │ ├── logo.png │ └── scss │ │ ├── color.scss │ │ ├── common-split.scss │ │ ├── mixin.scss │ │ └── my-vant.scss ├── components │ ├── footerNav.vue │ └── loading.vue ├── main.js ├── plugins │ └── Socket.io.js ├── router │ └── index.js ├── store │ ├── index.js │ └── types.js ├── utils │ ├── area.js │ ├── cache.js │ ├── emoji.json │ ├── http.js │ ├── tools.js │ └── validate.js └── views │ ├── AccountSafe │ └── accountSafe.vue │ ├── ApplyDetail │ └── applyDetail.vue │ ├── Chats │ └── chats.vue │ ├── Contacts │ └── contacts.vue │ ├── CreateGroup │ └── createGroup.vue │ ├── Edit │ └── edit.vue │ ├── EditAge │ └── editAge.vue │ ├── EditEmail │ └── editEmail.vue │ ├── EditGender │ └── editGender.vue │ ├── EditName │ └── editName.vue │ ├── EditPassword │ └── editPassword.vue │ ├── EditPhone │ └── editPhone.vue │ ├── EditRemark │ └── editRemark.vue │ ├── FriendDetail │ └── friendDetail.vue │ ├── FriendsInfo │ └── friendsInfo.vue │ ├── GroupDetail │ └── groupDetail.vue │ ├── GroupInfo │ └── groupInfo.vue │ ├── Login │ └── login.vue │ ├── Manager │ └── manager.vue │ ├── MesPanel │ ├── Emoji │ │ └── emoji.vue │ ├── FooterSend │ │ └── footerSend.vue │ ├── MesList │ │ └── mesList.vue │ ├── MessageItem │ │ └── messageItem.vue │ └── mesPanel.vue │ ├── SearchFriend │ └── searchFriend.vue │ ├── SearchGroup │ └── searchGroup.vue │ ├── SearchLocal │ └── searchLocal.vue │ ├── SendFriendValidate │ └── sendFriendValidate.vue │ ├── SendGroupValidate │ └── sendGroupValidate.vue │ └── SysMes │ └── sysMes.vue └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/*.log 8 | 9 | 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | *.local 20 | 21 | package-lock.json 22 | yarn.lock 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.autoSave": "off", 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | "vue" 7 | ], 8 | "eslint.run": "onSave", 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": true 11 | }, 12 | "editor.formatOnSave": false, 13 | 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Reese Wellin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Chat 2 | 3 | 项目完成于2020年未,已经不再维护! 4 | 5 | ## 项目启动 6 | 7 | 启动环境:node、mongodb 8 | 9 | ```bash 10 | git clone https://github.com/xxydrr/vue-koa-chat 11 | 12 | cd backend 13 | npm install //安装后端依赖 14 | mongod //启动数据库 15 | npm run init //初始化一个系统账号 16 | npm run dev //启动服务器 17 | cd ../ 18 | cd frontend 19 | npm run serve 20 | ``` 21 | 22 | ## 实现功能 23 | 24 | - 登录/注册/登出 25 | - 消息类型(文本/表情/图片/文件) 26 | - 好友(添加/删除/修改备注/模糊搜索好友) 27 | - 群组(普通群/广播群)/创建群/加入群/退群/模糊搜索添加的群 28 | - 未读消息统计/标为已读 29 | - 分组(未读/群组会话分组) 30 | - 添加好友/加群校验 31 | - 设置修改个人信息(密码/头像/年龄/手机号码/性别/邮箱/城市/昵称) 32 | - 查看好友/群组信息 33 | - 持续完善... 34 | 35 | ## 项目截图 36 | 37 | [![img](https://camo.githubusercontent.com/3342c0573ddaa3bf466e29415ee025c30adab987f6f3d589ef436324e8390803/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630312e706e67)](https://camo.githubusercontent.com/3342c0573ddaa3bf466e29415ee025c30adab987f6f3d589ef436324e8390803/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630312e706e67)[![img](https://camo.githubusercontent.com/77a92d724b7fc41d7cb0efb0c0bab5be537e19f63b3c93296dbde2731eb8549e/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343631352e706e67)](https://camo.githubusercontent.com/77a92d724b7fc41d7cb0efb0c0bab5be537e19f63b3c93296dbde2731eb8549e/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343631352e706e67) 38 | 39 | [![img](https://camo.githubusercontent.com/a34e640fc4264d324f30394e059717601fa2ff1b22f1f781d7f42731a3b4b586/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630332e706e67)](https://camo.githubusercontent.com/a34e640fc4264d324f30394e059717601fa2ff1b22f1f781d7f42731a3b4b586/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630332e706e67)[![img](https://camo.githubusercontent.com/e8a2a0b91eb47b9e87ba75091ed7dbd00efb9ea670abffe1b41389054bd1aae6/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630322e706e67)](https://camo.githubusercontent.com/e8a2a0b91eb47b9e87ba75091ed7dbd00efb9ea670abffe1b41389054bd1aae6/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630322e706e67) 40 | 41 | [![img](https://camo.githubusercontent.com/4195eb1410798b0612daec35de73befc3015d4529a097844562dcef71f58fd67/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343631372e706e67)](https://camo.githubusercontent.com/4195eb1410798b0612daec35de73befc3015d4529a097844562dcef71f58fd67/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343631372e706e67)[![img](https://camo.githubusercontent.com/c614a937581f9e8898395e335a83835a58018c254ffa087c58659cca578e36b2/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630342e706e67)](https://camo.githubusercontent.com/c614a937581f9e8898395e335a83835a58018c254ffa087c58659cca578e36b2/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630342e706e67) 42 | 43 | ## 项目功能扩展想法 44 | 45 | - 主题皮肤设置 46 | - better scroll + 分页查询 47 | - 我的页面加入游戏功能 48 | - 我的页面加入stories功能 49 | - 视频语音webRTC(暂时没有思路) 50 | 51 | ## 说明 52 | 53 | ======= 欢迎有对项目有扩展想法的伙伴能参与到这个项目来❤️❤️❤️ 54 | 55 | 如果项目对您有帮助,希望您能 "Star" 支持一下 感谢!🌹🌹🌹🌹🌹🌹 56 | 57 | 我的vue2 + vuex 聊天系统入门项目。[地址](https://github.com/xxydrr/vue-telegram) 58 | 59 | 该项目代码不再维护,欢迎关注我的新项目[story](https://github.com/xxydrr/story) 60 | 61 | ## 参考资料 62 | 63 | - [MongoDB](https://docs.mongodb.com/manual/reference/) 64 | - [Mongoose](https://mongoosejs.com/docs/guide.html) 65 | - [vue-telegram](https://github.com/xxydrr/vue-telegram) 66 | - [socket.io](https://www.w3cschool.cn/socket/socket-buvk2eib.html) 67 | - [Vchat](https://github.com/wuyawei/Vchat) 68 | 69 | ## License 70 | 71 | [MIT](https://github.com/xxydrr/vue-koa-vue/blob/main/LICENSE) 72 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require("koa"); 2 | // const app = new Koa() 3 | const Router = require("koa-router"); 4 | 5 | const views = require("koa-views"); 6 | const json = require("koa-json"); 7 | const onerror = require("koa-onerror"); 8 | const bodyparser = require("koa-bodyparser"); 9 | const session = require("koa-session"); 10 | 11 | const logger = require("koa-logger"); 12 | const cors = require("koa2-cors"); // 解决跨域的中间件 koa2-cors 13 | 14 | const index = require("./routes/index"); 15 | const user = require("./routes/user"); 16 | const friendly = require("./routes/friendly"); 17 | const upload = require("./routes/upload"); 18 | const group = require("./routes/group"); 19 | 20 | const {authJwt} = require("./utils/jwt") 21 | // 导入数据库连接文件 22 | const { connect } = require("./utils/connect"); 23 | 24 | const app = new Koa(); 25 | 26 | const router = new Router(); 27 | 28 | // error handler 29 | onerror(app); 30 | // session 配置 31 | app.use( 32 | cors({ 33 | origin: function (ctx) { 34 | return ctx.header.origin; 35 | }, // 允许发来请求的域名 36 | allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], // 设置所允许的 HTTP请求方法 37 | credentials: true, // 标示该响应是合法的 38 | }) 39 | ); 40 | 41 | const CONFIG = { 42 | key: "sessionId", 43 | maxAge: 1000 * 60, // cookie 的过期时间 60000ms => 60s => 1min 44 | httpOnly: true, // true 表示只有服务器端可以获取 cookie 45 | }; 46 | app.keys = ["session secret"]; // 设置签名的 Cookie 密钥 47 | app.use(session(CONFIG, app)); 48 | 49 | 50 | 51 | // middlewares 52 | app.use( 53 | bodyparser({ 54 | enableTypes: ["json", "form", "text"], 55 | }) 56 | ); 57 | app.use(json()); 58 | app.use(logger()); 59 | app.use(require("koa-static")(__dirname + "/public")); 60 | 61 | app.use( 62 | views(__dirname + "/views", { 63 | extension: "ejs", 64 | }) 65 | ); 66 | 67 | /**中间件使用 */ 68 | // app.use(authJwt()); 69 | // logger 70 | app.use(async (ctx, next) => { 71 | const start = new Date(); 72 | await next(); 73 | const ms = new Date() - start; 74 | console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); 75 | }); 76 | 77 | // routes 78 | router.use("/api/user", user.routes()); // 用户相关 79 | router.use("/api/friendly", authJwt, friendly.routes()); 80 | router.use("/api/upload", authJwt, upload.routes()); 81 | router.use("/api/group", authJwt, group.routes()); 82 | 83 | router.use("/", index.routes()); 84 | // 加载路由中间件 85 | app.use(router.routes()).use(router.allowedMethods()); 86 | 87 | // appSocket(server); 88 | 89 | // error-handling 90 | app.on("error", (err, ctx) => { 91 | console.error("server error", err, ctx); 92 | }); 93 | // 立即执行函数 94 | (async () => { 95 | await connect(); // 执行连接数据库任务 96 | })(); 97 | module.exports = app; 98 | -------------------------------------------------------------------------------- /backend/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const appSocket = require("../service/socket"); 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | var app = require("../app"); 9 | var debug = require("debug")("demo:server"); 10 | var http = require("http"); 11 | 12 | /** 13 | * Get port from environment and store in Express. 14 | */ 15 | 16 | var port = normalizePort(process.env.PORT || "3000"); 17 | // app.set('port', port); 18 | 19 | /** 20 | * Create HTTP server. 21 | */ 22 | 23 | var server = http.createServer(app.callback()); 24 | 25 | /** 26 | * Listen on provided port, on all network interfaces. 27 | */ 28 | appSocket(server); 29 | server.listen(port); 30 | server.on("error", onError); 31 | server.on("listening", onListening); 32 | 33 | /** 34 | * Normalize a port into a number, string, or false. 35 | */ 36 | 37 | function normalizePort(val) { 38 | var port = parseInt(val, 10); 39 | 40 | if (isNaN(port)) { 41 | // named pipe 42 | return val; 43 | } 44 | 45 | if (port >= 0) { 46 | // port number 47 | return port; 48 | } 49 | 50 | return false; 51 | } 52 | 53 | /** 54 | * Event listener for HTTP server "error" event. 55 | */ 56 | 57 | function onError(error) { 58 | if (error.syscall !== "listen") { 59 | throw error; 60 | } 61 | 62 | var bind = typeof port === "string" ? "Pipe " + port : "Port " + port; 63 | 64 | // handle specific listen errors with friendly messages 65 | switch (error.code) { 66 | case "EACCES": 67 | console.error(bind + " requires elevated privileges"); 68 | process.exit(1); 69 | break; 70 | case "EADDRINUSE": 71 | console.error(bind + " is already in use"); 72 | process.exit(1); 73 | break; 74 | default: 75 | throw error; 76 | } 77 | } 78 | 79 | /** 80 | * Event listener for HTTP server "listening" event. 81 | */ 82 | 83 | function onListening() { 84 | var addr = server.address(); 85 | var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; 86 | debug("Listening on " + bind); 87 | } 88 | -------------------------------------------------------------------------------- /backend/controller/friendly.js: -------------------------------------------------------------------------------- 1 | const FriendlyModel = require("../models/friendly"); 2 | 3 | const checkIsFriends = async (ctx) => { 4 | const { roomId } = ctx.request.body; 5 | try { 6 | const result = await FriendlyModel.find({ 7 | roomId, 8 | }); 9 | 10 | if (result.length === 0) { 11 | return (ctx.body = { 12 | code: 200, 13 | data: { isFriends: false }, 14 | }); 15 | } 16 | ctx.body = { 17 | code: 200, 18 | data: { isFriends: true }, 19 | }; 20 | } catch (error) { 21 | console.log(error); 22 | } 23 | }; 24 | // 查看我的好友列表 25 | 26 | const findMyFriendsList = async (ctx) => { 27 | const { userId } = ctx.query; 28 | try { 29 | const self = await FriendlyModel.findFriendBySelf(userId); 30 | const other = await FriendlyModel.findFriendByOther(userId); 31 | let data = []; 32 | // 重新组合文档 33 | self.forEach((item) => { 34 | data.push({ 35 | createDate: item.createDate, 36 | nickname: item.other.nickname, 37 | photo: item.other.photo, 38 | signature: item.other.signature, 39 | id: item.other._id, 40 | roomId: userId + "-" + item.other._id, 41 | }); 42 | }); 43 | other.forEach((item) => { 44 | data.push({ 45 | createDate: item.createDate, 46 | nickname: item.self.nickname, 47 | photo: item.self.photo, 48 | signature: item.self.signature, 49 | id: item.self._id, 50 | roomId: item.self._id + "-" + userId, 51 | }); 52 | }); 53 | // console.log(data); 54 | ctx.body = { 55 | code: 200, 56 | data: data, 57 | }; 58 | } catch (error) { 59 | console.log(error); 60 | } 61 | }; 62 | 63 | const addFriend = async (obj) => { 64 | // const userObj = { self, other } 65 | const { self, other, friendRoom } = obj; 66 | try { 67 | const result = await FriendlyModel.findOne({ 68 | roomId: friendRoom, 69 | }); 70 | 71 | if (result) 72 | return { 73 | data: -1, 74 | msg: "你们已经是好友了", 75 | }; 76 | const newFriend = new FriendlyModel({ 77 | self, 78 | other, 79 | roomId: friendRoom, 80 | }); 81 | await newFriend.save(); 82 | return { 83 | code: 200, 84 | self: newFriend.self, 85 | other: newFriend.other, 86 | }; 87 | } catch (error) { 88 | console.log(error); 89 | } 90 | }; 91 | 92 | const deleteFriend = async (ctx) => { 93 | const { roomId } = ctx.request.body; 94 | try { 95 | const result = await FriendlyModel.findOneAndDelete({ roomId }); 96 | ctx.body = { 97 | code: 200, 98 | msg: "删除成功", 99 | }; 100 | } catch (error) { 101 | console.log(error); 102 | } 103 | }; 104 | 105 | module.exports = { 106 | checkIsFriends, 107 | findMyFriendsList, 108 | addFriend, 109 | deleteFriend, 110 | }; 111 | -------------------------------------------------------------------------------- /backend/controller/group.js: -------------------------------------------------------------------------------- 1 | const UserModel = require("../models/user"); 2 | const GroupModel = require("../models/group"); 3 | const GroupUserModel = require("../models/groupUser"); 4 | const { log } = require("debug"); 5 | // 获取我的群聊 6 | const getMyGroup = async (ctx) => { 7 | const { userName } = ctx.query; 8 | try { 9 | const groupUserDoc = await GroupUserModel.findGroupByUserName(userName); 10 | console.log(groupUserDoc); 11 | // if (!groupUserDoc.length) return (ctx.body = { code: -1, msg: "查找失败" }); 12 | ctx.body = { code: 200, data: groupUserDoc }; 13 | } catch (error) { 14 | console.log(error); 15 | } 16 | }; 17 | // 获取群聊详情 18 | const getGroupInfo = async (ctx) => { 19 | const { id } = ctx.query; 20 | try { 21 | const groupResult = await GroupModel.findById(id); 22 | const groupUser = await GroupUserModel.findGroupUsersByGroupId(id); 23 | if (groupResult === null || groupUser.length === 0) { 24 | ctx.body = { code: -1, msg: "查找失败" }; 25 | } 26 | ctx.body = { code: 200, data: groupResult, users: groupUser }; 27 | } catch (error) { 28 | console.log(error); 29 | } 30 | }; 31 | // 搜索群聊 32 | const huntGroups = async (ctx) => { 33 | const { keyword } = ctx.query; // 关键字,页数 34 | 35 | try { 36 | const groupDoc = await GroupModel.findOne({ 37 | groupCode: keyword, 38 | }); 39 | 40 | if (groupDoc === null) 41 | return (ctx.body = { code: -1, msg: "该群组不存在" }); 42 | ctx.body = { 43 | code: 200, 44 | data: { 45 | userName: groupDoc.title, 46 | signature: groupDoc.desc, 47 | avatar: groupDoc.img, 48 | id: groupDoc._id, 49 | type: groupDoc.type, 50 | }, 51 | msg: "查找成功", 52 | }; 53 | } catch (error) { 54 | console.log(error); 55 | } 56 | }; 57 | // 群聊添加新成员 58 | const InsertGroupUsers = async (obj) => { 59 | const { groupId, userName, self } = obj; 60 | try { 61 | const hasGroupUser = await GroupUserModel.find({ 62 | groupId, 63 | userId: self, 64 | }); 65 | if (hasGroupUser.length) { 66 | return { code: -1, msg: "此用户已经存在该群了" }; 67 | } 68 | 69 | const newGroupUser = new GroupUserModel({ 70 | groupId: groupId, 71 | userName: userName, 72 | userId: self, 73 | }); 74 | await newGroupUser.save(); 75 | // 群组的人员数量+1 76 | const result = await GroupModel.updateOne( 77 | { _id: groupId }, 78 | { $inc: { userNum: 1 } } 79 | ); 80 | if (result.nModified > 0) { 81 | return { code: 200, msg: "添加成功", user: newGroupUser }; 82 | } 83 | return { code: -1, msg: "添加失败" }; 84 | } catch (error) { 85 | console.log(error); 86 | } 87 | }; 88 | // 创建群聊 89 | const createGroup = async (ctx) => { 90 | const { 91 | groupName, 92 | groupDesc, 93 | groupImage, 94 | userName, 95 | groupCode, 96 | type, 97 | } = ctx.request.body; 98 | try { 99 | const result = await GroupModel.find({ groupCode }); 100 | if (result.length) return (ctx.body = { code: -1, msg: "该群id已存在" }); 101 | const newGroup = new GroupModel({ 102 | title: groupName, 103 | desc: groupDesc, 104 | img: groupImage, 105 | userNum: 1, 106 | holderName: userName, 107 | groupCode, 108 | type, 109 | }); 110 | await newGroup.save(); 111 | const userResult = await UserModel.findOne({ userName }); 112 | const newGroupUser = new GroupUserModel({ 113 | userName, 114 | userId: userResult._id, 115 | manager: 0, 116 | holder: 1, 117 | groupId: newGroup._id, 118 | }); 119 | newGroupUser.save(); 120 | ctx.body = { code: 200, data: newGroup }; 121 | } catch (error) { 122 | GroupModel.deleteOne({ _id: newGroup._id }); 123 | console.log(error); 124 | } 125 | }; 126 | 127 | // 查找指定群聊成员 128 | 129 | const getGroupUsers = async (ctx) => { 130 | const { groupId } = ctx.query; 131 | try { 132 | const groupUserDoc = await GroupUserModel.findGroupUsersByGroupId(groupId); 133 | if (groupUserDoc === null) 134 | return (ctx.body = { code: -1, msg: "查找失败" }); 135 | ctx.body = { code: 200, data: groupUserDoc }; 136 | } catch (error) { 137 | console.log(error); 138 | } 139 | }; 140 | 141 | const quitGroup = async (ctx) => { 142 | const { userId, groupId } = ctx.request.body; 143 | try { 144 | await GroupUserModel.findOneAndDelete({ userId }); 145 | await UserModel.findOneAndUpdate( 146 | { 147 | _id: userId, 148 | }, 149 | { 150 | $pull: { 151 | conversationsList: { 152 | id: groupId, 153 | }, 154 | }, 155 | }, 156 | { 157 | new: true, 158 | } 159 | ); 160 | ctx.body = { code: 200, msg: "退群成功" }; 161 | } catch (error) { 162 | console.log(error); 163 | } 164 | }; 165 | 166 | // 获取所有群聊 167 | module.exports = { 168 | createGroup, 169 | getMyGroup, 170 | getGroupUsers, 171 | huntGroups, 172 | getGroupInfo, 173 | InsertGroupUsers, 174 | quitGroup, 175 | }; 176 | -------------------------------------------------------------------------------- /backend/controller/message.js: -------------------------------------------------------------------------------- 1 | const mesModel = require("../models/message"); 2 | // 保存消息 3 | const saveMessage = async (obj) => { 4 | try { 5 | const mesDoc = await new mesModel(obj); 6 | await mesDoc.save(); 7 | return { 8 | code: 200, 9 | data: "ok", 10 | msg: "ok", 11 | }; 12 | } catch (error) { 13 | return { code: -1, msg: "err" }; 14 | } 15 | }; 16 | // 删除消息 17 | const deleteMessage = async (ctx) => { 18 | const data = ctx.request.body; 19 | try { 20 | await mesModel.deleteOne(data); 21 | ctx.body = { code: 200, mes: "删除成功" }; 22 | } catch (error) { 23 | console.log(error); 24 | } 25 | }; 26 | 27 | const getMessage = async (obj, count = 0) => { 28 | try { 29 | let result; 30 | await mesModel 31 | .find({ roomId: obj.roomId }) 32 | .populate({ path: "self", select: "signature avatar nickname" }) 33 | .skip((obj.offset - 1) * obj.limit) 34 | .limit(obj.limit) 35 | .sort({ time: -1 }) 36 | .then((r) => { 37 | r.forEach((v) => { 38 | // 防止用户修改资料后,信息未更新 39 | if (v.userM) { 40 | v.nickname = v.userM.nickname; 41 | v.avatar = v.userM.photo; 42 | v.signature = v.userM.signature; 43 | } 44 | }); 45 | r.reverse(); 46 | result = { code: 200, data: r, count: count }; 47 | }) 48 | .catch((err) => { 49 | console.log(err); 50 | result = { code: -1 }; 51 | }); 52 | // console.log("result", result); 53 | return result; 54 | } catch (error) { 55 | console.log(error); 56 | } 57 | }; 58 | 59 | const getHistoryMessage = async (obj, reverse) => { 60 | try { 61 | if (reverse === 2) { 62 | const count = await mesModel.countDocuments({ roomId: obj.roomId }); 63 | 64 | return count > 0 65 | ? getMessage({ obj, count }) 66 | : { code: 200, count: 0, data: [] }; 67 | } else if (reverse === 1) { 68 | return getMessage(obj); 69 | } else if (reverse === -1) { 70 | const mesDoc = await mesModel 71 | .find({ roomId: obj.roomId }) 72 | .skip((obj.offset - 1) * obj.limit) 73 | .limit(obj.limit) 74 | .sort({ time: -1 }); 75 | return { code: 200, data: mesDoc }; 76 | } 77 | } catch (error) { 78 | console.log(error); 79 | } 80 | }; 81 | 82 | const loadMoreMessages = (ctx) => { 83 | const data = ctx.query; 84 | getHistoryMessage(data, 2, (item) => { 85 | if (item.code !== 200) 86 | return (ctx.body = { 87 | code: -1, 88 | data: "获取失败", 89 | }); 90 | ctx.body = item; 91 | }); 92 | }; 93 | // 设置消息的状态 94 | const setReadStatus = async (obj) => { 95 | const { roomId, userName } = obj; 96 | try { 97 | const mesList = await mesModel.find({ roomId }); 98 | mesList.forEach((item) => { 99 | if (item.read.indexOf(userName) === -1) { 100 | item.read.push(userName); 101 | item.save(); 102 | } 103 | }); 104 | console.log(userName); 105 | 106 | return { code: 200, msg: "ok" }; 107 | } catch (error) { 108 | console.log(error); 109 | } 110 | }; 111 | 112 | const setMessageStatus = async (obj) => { 113 | const { self, status } = obj; 114 | try { 115 | const result = await mesModel.find({ self }, (err, doc) => { 116 | if (!err) { 117 | doc.forEach((item) => { 118 | if ( 119 | item.type === "validate" && 120 | (item.state === "friend" || item.state === "group") 121 | ) { 122 | item.status = status; 123 | item.save(); 124 | } 125 | }); 126 | } else { 127 | return { code: -1, msg: "err" }; 128 | } 129 | }); 130 | 131 | return { code: 200, msg: "ok", data: result }; 132 | } catch (error) { 133 | console.log(error); 134 | } 135 | }; 136 | 137 | const updateMesStatus = async (obj) => { 138 | const { _id, status } = obj; 139 | try { 140 | const mesDoc = await mesModel.updateOne({ _id }, { status }); 141 | if (mesDoc.nModified > 0) return { code: 200, msg: "ok" }; 142 | return { code: -1, msg: "更新失败" }; 143 | } catch (error) { 144 | console.log(error); 145 | } 146 | }; 147 | 148 | module.exports = { 149 | saveMessage, 150 | deleteMessage, 151 | loadMoreMessages, 152 | getHistoryMessage, 153 | setReadStatus, 154 | setMessageStatus, 155 | updateMesStatus, 156 | }; 157 | -------------------------------------------------------------------------------- /backend/init.js: -------------------------------------------------------------------------------- 1 | const { connect } = require("./utils/connect"); 2 | const initData = require("./service/initData"); 3 | 4 | (async () => { 5 | await connect(); // 执行连接数据库任务 6 | await initData(); 7 | })(); 8 | -------------------------------------------------------------------------------- /backend/middleware/tokenCheck.js: -------------------------------------------------------------------------------- 1 | 2 | const tokenCheck = function () { 3 | return async function (ctx, next) { 4 | if (ctx.state.user) { 5 | // 如果携带有效 Token 就对 Token 进行检查(由 kow-jwt 检查 Token 有效性) 6 | let result = true 7 | // check here 8 | if (result) { 9 | await next() 10 | } else { 11 | ctx.body = { 12 | msg: "Token 检查未通过" 13 | } 14 | } 15 | } else { 16 | // 如果没有携带 Token 就跳过检查 17 | await next() 18 | } 19 | } 20 | } 21 | 22 | module.exports = tokenCheck 23 | -------------------------------------------------------------------------------- /backend/models/accountBase.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const accountBaseSchema = new Schema({ 6 | code: String, 7 | status: String, // 1 已使用 0 未使用 8 | special: String, 9 | type: String, // 1 用户 2 群聊 10 | random: Number, 11 | }); 12 | 13 | //发布模型 14 | module.exports = mongoose.model("accountBase", accountBaseSchema); 15 | -------------------------------------------------------------------------------- /backend/models/expression.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const expressionSchema = new Schema({ 5 | name: String, // 表情包名称 6 | info: String, // 描述 7 | list: Array, // 表情列表 8 | }); 9 | 10 | module.exports = mongoose.model("expression", expressionSchema); 11 | -------------------------------------------------------------------------------- /backend/models/friendly.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const friendlySchema = new Schema({ 6 | self: { 7 | type: Schema.Types.ObjectId, 8 | ref: "user", 9 | }, 10 | other: { 11 | type: Schema.Types.ObjectId, 12 | ref: "user", 13 | }, 14 | roomId: { 15 | type: String, 16 | default: "", 17 | }, 18 | createDate: { type: Date, default: Date.now() }, // 加好友时间 19 | }); 20 | 21 | friendlySchema.statics.findFriendBySelf = function (userId, cb) { 22 | // 联表查询 select:关联表的部分属性 23 | return this.find({ self: userId }) 24 | .populate({ path: "other", select: "signature avatar nickname" }) 25 | .exec(cb); 26 | }; 27 | friendlySchema.statics.findFriendByOther = function (userId, cb) { 28 | return this.find({ other: userId }) 29 | .populate({ path: "self", select: "signature avatar nickname" }) 30 | .exec(cb); 31 | }; 32 | 33 | module.exports = mongoose.model("friendly", friendlySchema); 34 | -------------------------------------------------------------------------------- /backend/models/group.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const groupSchema = new Schema({ 6 | groupCode: { type: String, unique: true }, 7 | title: { 8 | type: String, 9 | required: true, 10 | }, // 群名称 11 | desc: { 12 | type: String, 13 | default: "", 14 | }, // 群的描述 15 | img: { 16 | type: String, 17 | default: "/img/zwsj5.png", 18 | }, // 群图片 19 | 20 | userNum: { 21 | type: Number, 22 | default: 1, 23 | }, // 群成员数量,避免某些情况需要多次联表查找,如搜索;所以每次加入一人,数量加一 24 | type: { type: String, default: "group" }, // group/channel 25 | createDate: { type: Date, default: Date.now }, // 建群时间 26 | grades: { type: String, default: "1" }, // 群等级,备用 27 | holderName: String, // 群主账号,在user实体中对应name字段 28 | }); 29 | 30 | //发布模型 31 | module.exports = mongoose.model("group", groupSchema); 32 | -------------------------------------------------------------------------------- /backend/models/groupUser.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | // 群成员的集合 6 | const groupUserSchema = new Schema({ 7 | groupId: { 8 | type: Schema.Types.ObjectId, 9 | ref: "group", 10 | }, 11 | userId: { 12 | type: Schema.Types.ObjectId, 13 | ref: "user", 14 | }, 15 | userName: { type: String }, 16 | manager: { type: Number, default: 0 }, // 是否是管理员,默认0,不是,1是 17 | holder: { type: Number, default: 0 }, // 是否是群主,默认0,不是,1是 18 | card: String, // 群名片 19 | }); 20 | 21 | groupUserSchema.statics.findGroupByUserName = function (userName, cb) { 22 | // 根据用户名查找所在群聊 23 | return this.find({ userName: userName }).populate("groupId").exec(cb); 24 | }; 25 | groupUserSchema.statics.findGroupUsersByGroupId = function (groupId, cb) { 26 | // 通过groupId查找群成员 27 | return this.find({ groupId: groupId }) 28 | .populate({ path: "userId", select: "signature avatar nickname " }) 29 | .exec(cb); 30 | }; 31 | 32 | module.exports = mongoose.model("groupUser", groupUserSchema); 33 | -------------------------------------------------------------------------------- /backend/models/message.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const messagesSchema = new Schema({ 6 | roomId: String, // 房间id 7 | userName: String, // 用户登录名 8 | nickname: String, // 用户昵称 9 | time: String, // 时间 10 | avatar: String, // 用户头像 11 | mes: String, // 消息 12 | read: Array, // 是否已读 13 | signature: String, // 个性签名 14 | emoji: String, // 表情地址 15 | style: String, // 消息类型 emoji/mess/img/file 16 | groupId: String, // 加入群聊id 17 | groupName: String, // 加入群聊名称 18 | groupPhoto: String, //加入群聊头像 19 | self: { 20 | type: Schema.Types.ObjectId, 21 | ref: "user", 22 | }, // 申请人id、消息发送人 23 | other: String, // 好友id 24 | otherName: String, // 好友昵称 25 | otherUserName: String, 26 | otherAvatar: String, // 好友头像 27 | otherLoginName: String, // 好友登录名 28 | friendRoom: String, // 好友房间 29 | state: String, // group/ friend 30 | type: String, // validate 31 | status: String, // 0 未操作 1 同意 2 拒绝 32 | }); 33 | 34 | module.exports = mongoose.model("messages", messagesSchema); 35 | -------------------------------------------------------------------------------- /backend/models/mobilePhone.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | // 手机号数据模型 (用于发送验证码) 6 | const mobilePhoneSchema = new Schema({ 7 | mobilePhone: { type: String, unique: true }, // 手机号 8 | clientIp: { type: String, default: "" }, // 客户端 ip 9 | sendCount: Number, // 发送次数 10 | curDate: String, // 当前日期 11 | sendTimestamp: { type: String, default: +new Date() }, // 短信发送的时间戳 12 | }); 13 | 14 | module.exports = mongoose.model("mobilePhone", mobilePhoneSchema); 15 | -------------------------------------------------------------------------------- /backend/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require("bcryptjs"); // 用于密码哈希的加密算法 4 | const SALT_WORK_FACTOR = 10; // 定义加密密码计算强度 5 | // 用户数据模型 6 | const userSchema = new Schema({ 7 | // Schema 8 | userName: { type: String, unique: true }, 9 | password: String, 10 | mobilePhone: { type: String, unique: true }, // 手机号码 11 | avatar: { type: String, default: "/img/avatar.jpg" }, // 默认头像 12 | signature: { type: String, default: "这个人很懒,暂时没有签名哦!" }, 13 | nickname: { type: String, default: +new Date() }, 14 | email: { type: String, default: "" }, 15 | province: { type: String, default: "广东省" }, // 省 16 | city: { type: String, default: "广州市" }, // 市 17 | gender: { type: String, default: "男" }, // 0 男 1 女 3 保密 18 | signUpTime: { type: Date, default: +new Date() }, // 注册时间 19 | lastLoginTime: { type: Date, default: +new Date() }, // 最后一次登录 20 | conversationsList: Array, // 会话列表 * name 会话名称 * photo 会话头像 * id 会话id * type 会话类型 group/ frend/me 21 | emoji: Array, // 表情包 22 | age: { type: Number, default: 18 }, 23 | friendsGroup: { 24 | type: Object, 25 | default: { name: "我的好友" }, 26 | }, 27 | }); 28 | 29 | // 对密码进行加盐 30 | // 使用 pre 中间件在用户信息存储前执行 31 | userSchema.pre("save", function (next) { 32 | //产生密码hash当密码有更改的时候(或者是新密码) 33 | 34 | // 进行加密 | 产生一个 salt 35 | bcrypt.genSalt(SALT_WORK_FACTOR, (err, salt) => { 36 | if (err) return next(err); 37 | // 结合 salt 产生新的hash 38 | bcrypt.hash(this.password, salt, (err, hash) => { 39 | if (err) return next(err); 40 | // 使用 hash 覆盖明文密码 41 | this.password = hash; 42 | next(); 43 | }); 44 | }); 45 | }); 46 | 47 | // 密码比对的方法 48 | // 第一个参数:客户端传递的; 第二个参数:数据库的 49 | 50 | userSchema.methods.comparePassword = (_password, password) => { 51 | return new Promise((resolve, reject) => { 52 | bcrypt.compare(_password, password, (error, result) => { 53 | !error ? resolve(result) : reject(error); 54 | }); 55 | }); 56 | }; 57 | 58 | //发布模型 59 | module.exports = mongoose.model("user", userSchema); 60 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-server", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node bin/www", 7 | "dev": "./node_modules/.bin/nodemon bin/www", 8 | "init": "node init.js", 9 | "prd": "pm2 start bin/www", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "dependencies": { 13 | "@koa/multer": "^3.0.0", 14 | "bcryptjs": "^2.4.3", 15 | "debug": "^4.1.1", 16 | "ejs": "^2.7.4", 17 | "jsonwebtoken": "^8.5.1", 18 | "koa": "^2.7.0", 19 | "koa-bodyparser": "^4.2.1", 20 | "koa-convert": "^1.2.0", 21 | "koa-json": "^2.0.2", 22 | "koa-jwt": "^4.0.0", 23 | "koa-logger": "^3.2.0", 24 | "koa-onerror": "^4.1.0", 25 | "koa-router": "^7.4.0", 26 | "koa-session": "^6.1.0", 27 | "koa-static": "^5.0.0", 28 | "koa-views": "^6.2.0", 29 | "koa2-cors": "^2.0.6", 30 | "mongoose": "^5.10.13", 31 | "multer": "^1.4.2", 32 | "silly-datetime": "^0.1.2", 33 | "socket.io": "^4.5.4", 34 | "svg-captcha": "^1.4.0" 35 | }, 36 | "devDependencies": { 37 | "nodemon": "^2.0.20" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/public/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/backend/public/img/avatar.jpg -------------------------------------------------------------------------------- /backend/public/img/group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/backend/public/img/group.jpg -------------------------------------------------------------------------------- /backend/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /backend/public/uploads/2021-06-10/file-1623305062726-QQ图片20210312002220.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/backend/public/uploads/2021-06-10/file-1623305062726-QQ图片20210312002220.png -------------------------------------------------------------------------------- /backend/public/uploads/2021-06-10/file-1623305634180-QQ图片20210312002220.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/backend/public/uploads/2021-06-10/file-1623305634180-QQ图片20210312002220.png -------------------------------------------------------------------------------- /backend/public/uploads/2021-06-10/file-1623305648842-基于vue.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/backend/public/uploads/2021-06-10/file-1623305648842-基于vue.docx -------------------------------------------------------------------------------- /backend/routes/friendly.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router"); 2 | 3 | const router = new Router(); 4 | const api = require("../controller/friendly"); 5 | 6 | router.post("/checkIsFriends", api.checkIsFriends); 7 | router.get("/findMyFriendsList", api.findMyFriendsList); 8 | router.post("/deleteFriend", api.deleteFriend); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /backend/routes/group.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router"); 2 | 3 | const router = new Router(); 4 | 5 | const api = require("../controller/group"); 6 | 7 | router.post("/createGroup", api.createGroup); // 新建群 8 | router.get("/getMyGroup", api.getMyGroup); // 查找我的群聊 9 | router.get("/getGroupUsers", api.getGroupUsers); // 查找指定群聊成员 10 | router.get("/huntGroups", api.huntGroups); // 搜索聊天群(名称/code) 11 | router.get("/getGroupInfo", api.getGroupInfo); // 查找群详细信息 12 | router.post("/quitGroup", api.quitGroup); // 退出群聊 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /backend/routes/index.js: -------------------------------------------------------------------------------- 1 | const router = require('koa-router')() 2 | 3 | router.get('/', async (ctx, next) => { 4 | await ctx.render('index', { 5 | title: 'Hello Koa 2!' 6 | }) 7 | }) 8 | 9 | router.get('/string', async (ctx, next) => { 10 | ctx.body = 'koa2 string' 11 | }) 12 | 13 | router.get('/json', async (ctx, next) => { 14 | ctx.body = { 15 | title: 'koa2 json' 16 | } 17 | }) 18 | 19 | module.exports = router 20 | -------------------------------------------------------------------------------- /backend/routes/message.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router"); 2 | 3 | const router = new Router(); 4 | const api = require("../controller/message"); 5 | 6 | router.post("./deleteMessage", api.deleteMessage); 7 | router.get("/loadMoreMessages", api.loadMoreMessages); 8 | -------------------------------------------------------------------------------- /backend/routes/upload.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router"); 2 | 3 | const router = new Router(); 4 | 5 | const uploads = require("../utils/upload"); // 上传js 6 | const tools = require("../utils/tools"); 7 | 8 | // f 前端文件上传name必须为f 9 | // router.post('/uploadInmage', upload.single('f'), uploadInmage); // 第一种上传方案所需 10 | router.post("/uploadFile", uploads.single("file"), async (ctx) => { 11 | // 第二种上传方案 12 | 13 | const date = tools.formatTime(new Date()).split(" ")[0]; 14 | 15 | ctx.body = { 16 | code: 200, 17 | data: "/uploads/" + date + "/" + ctx.request.file.filename, 18 | }; 19 | }); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /backend/routes/user.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router"); 2 | const { authJwt } = require("../utils/jwt") 3 | const router = new Router(); 4 | const api = require("./../controller/user"); 5 | const sendPicCode = require("../service/picCode"); 6 | // 用户注册 7 | router.post("/register", api.register); 8 | // 用户登录 9 | router.post("/login", api.login); 10 | // 发送短信验证码 11 | router.post("/sendSMSCode", api.sendSMSCode); 12 | 13 | // 发送图片验证码 14 | router.get("/sendPicCode", sendPicCode); 15 | // 测试接口 16 | // router.get("/getUser", api.); 17 | // 更新个人信息 18 | router.put("/updateUserInfo", authJwt, api.updateUserInfo); 19 | // 获取用户详细信息 20 | router.get("/getUserInfo", authJwt, api.getUserInfo); 21 | // 获取官方账号的信息 22 | router.get("/getOfficialInfo", authJwt, api.getOfficialInfo); 23 | // 获取个人以及好友列表信息 分组状态 24 | router.get("/previewUser", authJwt, api.previewUser); 25 | // 添加会话 26 | router.post("/addConversationList", authJwt, api.addConversationList); 27 | // 移除会话 28 | router.post("/removeConversationList", authJwt, api.removeConversationList); 29 | // 搜索用户 30 | router.get("/searchFriends", authJwt, api.searchFriends); 31 | // router.post("/modifyFriendRemark", api.modifyFriendRemark); 32 | 33 | router.put("/updatedUserPhone", authJwt, api.updatedUserPhone); 34 | router.put("/updatedUserPassword", authJwt, api.updatedUserPassword); 35 | 36 | router.put("/modifyFriendRemark", api.modifyFriendRemark); 37 | router.put("/updateUserConversations", api.updateUserConversations); 38 | router.post("/deleteDialog", api.deleteDialog); 39 | 40 | // 检查是是否是自己的好友 41 | // router.post("/checkIsFriends", api.checkIsFriends); 42 | 43 | module.exports = router; 44 | -------------------------------------------------------------------------------- /backend/service/initData.js: -------------------------------------------------------------------------------- 1 | const UserModel = require("../models/user"); 2 | 3 | const initUser = async () => { 4 | try { 5 | const officialAccount = await new UserModel({ 6 | userName: "vueChat", 7 | password: "666666", 8 | avatar: "/img/vchat.png", 9 | signature: "官方", 10 | nickname: "官方推送", 11 | }); 12 | await officialAccount.save(); 13 | console.log("初始化官方账号成功"); 14 | } catch (error) { 15 | console.log(error); 16 | } 17 | }; 18 | module.exports = initUser; 19 | -------------------------------------------------------------------------------- /backend/service/picCode.js: -------------------------------------------------------------------------------- 1 | const tools = require("../utils/tools"); 2 | 3 | const sendPicCode = async (ctx) => { 4 | const picCode = tools.createCaptcha(); 5 | // 将验证码保存到session中 6 | ctx.session.picCode = picCode.text; 7 | // 指定返回类型 8 | ctx.set("Content-Type", "image/svg+xml"); 9 | ctx.body = picCode.data; 10 | }; 11 | 12 | module.exports = sendPicCode; 13 | -------------------------------------------------------------------------------- /backend/utils/connect.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const db = "mongodb://127.0.0.1/vue3_js_chat"; 3 | // 导出一个方法 4 | exports.connect = () => { 5 | // 连接数据库 6 | mongoose.connect(db, { 7 | useCreateIndex: true, 8 | useNewUrlParser: true, 9 | useUnifiedTopology: true, 10 | useFindAndModify: false, 11 | autoIndex: false, 12 | }); 13 | let maxConnectTimes = 0; 14 | 15 | return new Promise((resolve, reject) => { 16 | // 连接成功操作 17 | mongoose.connection.once("open", () => { 18 | console.log("Mongodb 数据库连接成功."); 19 | resolve(); 20 | }); 21 | // 连接断开操作 22 | mongoose.connection.on("disconnected", () => { 23 | console.log("*********** 数据库断开 ***********"); 24 | if (maxConnectTimes < 3) { 25 | maxConnectTimes++; 26 | mongoose.connect(db); 27 | } else { 28 | reject(new Error("数据库连接失败")); 29 | throw new Error("数据库连接失败"); 30 | } 31 | }); 32 | // 连接失败操作 33 | mongoose.connection.on("error", (error) => { 34 | console.log("*********** 数据库错误 ***********"); 35 | if (maxConnectTimes < 3) { 36 | maxConnectTimes++; 37 | mongoose.connect(db); 38 | } else { 39 | reject(error); 40 | throw new Error("数据库连接失败"); 41 | } 42 | }); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /backend/utils/jwt.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | // 密钥 4 | const JWT_SECRET = "chat_jwt"; 5 | 6 | // 创建 Token 7 | 8 | const createToken = (userInfo) => { 9 | // JWT 格式 token | 有效时间 1 小时 10 | return jwt.sign(userInfo, JWT_SECRET, { expiresIn: "24h" }); 11 | }; 12 | 13 | // 验证 token 结果 (验证 secret 和 检查有效期 exp) 14 | 15 | const authJwt = async (ctx,next) => { 16 | const token = ctx.header.authorization || '' 17 | if (token.startsWith('Bearer ')) { 18 | const tokenStr = token.substring(7) 19 | try { 20 | const user = await jwt.verify(tokenStr, JWT_SECRET) 21 | ctx.state.user = user 22 | } 23 | catch (err) { 24 | ctx.throw(401, 'Invalid token') 25 | } 26 | } 27 | else { 28 | ctx.throw(401, 'Invalid token') 29 | } 30 | await next() 31 | } 32 | 33 | 34 | module.exports = { 35 | createToken, 36 | authJwt, 37 | }; 38 | -------------------------------------------------------------------------------- /backend/utils/tools.js: -------------------------------------------------------------------------------- 1 | const sd = require("silly-datetime"); 2 | const svgCaptcha = require("svg-captcha"); // 生成 svg 格式的验证码 3 | 4 | // 工具封装 5 | 6 | // 格式化当前时间 7 | const getCurDate = (format = "YYYYMMDD") => { 8 | // 默认返回格式:20190925 9 | return sd.format(new Date(), format); 10 | }; 11 | 12 | // 生成 svg格式验证码 13 | const createCaptcha = () => { 14 | const captcha = svgCaptcha.create({ 15 | size: 4, 16 | noise: 1, 17 | fontSize: 35, 18 | width: 100, 19 | height: 35, 20 | background: "#e9e9e9", 21 | }); 22 | 23 | return captcha; 24 | }; 25 | const formatTime = (date) => { 26 | const year = date.getFullYear(); 27 | const month = date.getMonth() + 1; 28 | const day = date.getDate(); 29 | const hour = date.getHours(); 30 | const minute = date.getMinutes(); 31 | const second = date.getSeconds(); 32 | 33 | return ( 34 | [year, month, day].map(formatNumber).join("-") + 35 | " " + 36 | [hour, minute, second].map(formatNumber).join(":") 37 | ); 38 | }; 39 | const formatDate = (date) => { 40 | const year = date.getFullYear(); 41 | const month = date.getMonth() + 1; 42 | const day = date.getDate(); 43 | const a = [year, month, day].map(formatNumber); 44 | return a[0] + "年" + a[1] + "月" + a[2] + "日"; 45 | }; 46 | 47 | const formatNumber = (n) => { 48 | n = n.toString(); 49 | return n[1] ? n : "0" + n; 50 | }; 51 | module.exports = { 52 | getCurDate, 53 | createCaptcha, 54 | formatTime, 55 | formatDate, 56 | }; 57 | -------------------------------------------------------------------------------- /backend/utils/upload.js: -------------------------------------------------------------------------------- 1 | const tools = require("./tools"); 2 | const fs = require("fs"); 3 | const multer = require("@koa/multer"); 4 | const storage = multer.diskStorage({ 5 | //设置上传后文件路径, // 路径写成函数需要自己创建文件夹,字符串会自动创建。 6 | destination: function (req, file, cb) { 7 | let date = tools.formatTime(new Date()).split(" ")[0]; 8 | let path = "./public/uploads"; 9 | let pathDate = "./public/uploads/" + date; 10 | let stat = fs.existsSync(path); 11 | if (!stat) { 12 | // 不存在就创建 13 | fs.mkdirSync(path); 14 | } 15 | let statDate = fs.existsSync(pathDate); 16 | if (!statDate) { 17 | // 不存在就创建 18 | fs.mkdirSync(pathDate); 19 | } 20 | cb(null, pathDate); 21 | }, 22 | //给上传文件重命名,获取添加后缀名 23 | filename: function (req, file, cb) { 24 | cb(null, file.fieldname + "-" + Date.now() + "-" + file.originalname); 25 | }, 26 | }); 27 | 28 | //如需其他设置,请参考multer的limits,使用方法如下。 29 | //var upload = multer({ 30 | // storage, 31 | // limits 32 | // }); 33 | const upload = multer({ 34 | storage, 35 | }); 36 | module.exports = upload; 37 | -------------------------------------------------------------------------------- /backend/views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /backend/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | 6 | 7 | 8 |

<%= title %>

9 |

EJS Welcome to <%= title %>

10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/.env -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VUE_APP_IMG_URL = "http://localhost:3000" 2 | VUE_APP_BASE_API = "/api" 3 | VUE_APP_SOCKET_API = "http://localhost:3000" 4 | -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | VUE_APP_IMG_URL = "http://119.23.209.109:3001" 2 | VUE_APP_BASE_API = "/api" 3 | VUE_APP_SOCKET_API = "http://119.23.209.109:3001" 4 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | "vue/max-attributes-per-line": [2, { 18 | "singleline": 10, 19 | "multiline": { 20 | "max": 1, 21 | "allowFirstLine": false 22 | } 23 | }], 24 | "vue/singleline-html-element-content-newline": "off", 25 | "vue/multiline-html-element-content-newline": "off", 26 | "vue/name-property-casing": ["error", "PascalCase"], 27 | "vue/no-v-html": "off", 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': ["error", "always", { "null": "ignore" }], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 0, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | 'quotes': [2, 'single', { 166 | 'avoidEscape': true, 167 | 'allowTemplateLiterals': true 168 | }], 169 | 'semi': [2, 'never'], 170 | 'semi-spacing': [2, { 171 | 'before': false, 172 | 'after': true 173 | }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 2, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/*.log 8 | 9 | 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | *.local 19 | 20 | package-lock.json 21 | yarn.lock 22 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "semi": false, 7 | "wrap_line_length": 120, 8 | "wrap_attributes": "auto", 9 | "proseWrap": "always", 10 | "arrowParens": "avoid", 11 | "bracketSpacing": false, 12 | "jsxBracketSameLine": true, 13 | "useTabs": false, 14 | "overrides": [ 15 | { 16 | "files": ".prettierrc", 17 | "options": { 18 | "parser": "json" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # vue3-koa-js 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | 'env': { 6 | 'development': { 7 | 'plugins': ['dynamic-import-node'] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-koa-js", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "preview": "node build/index.js --preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "core-js": "^3.6.5", 14 | "socket.io-client": "^4.1.2", 15 | "vant": "^3.0.18", 16 | "vue": "^3.0.0", 17 | "vue-router": "^4.0.0-0", 18 | "vue-socket.io": "^3.0.10", 19 | "vuex": "^4.0.0-0" 20 | }, 21 | "devDependencies": { 22 | "@vue/cli-plugin-babel": "~4.5.0", 23 | "@vue/cli-plugin-eslint": "~4.5.0", 24 | "@vue/cli-plugin-router": "~4.5.0", 25 | "@vue/cli-plugin-vuex": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "@vue/compiler-sfc": "^3.0.0", 28 | "@vue/eslint-config-prettier": "^6.0.0", 29 | "babel-eslint": "^10.1.0", 30 | "babel-plugin-dynamic-import-node": "2.3.3", 31 | "connect": "^3.7.0", 32 | "eslint": "^6.7.2", 33 | "eslint-plugin-prettier": "^3.3.1", 34 | "eslint-plugin-vue": "^7.0.0", 35 | "html-webpack-plugin": "3.2.0", 36 | "prettier": "^2.2.1", 37 | "runjs": "^4.4.2", 38 | "sass": "^1.26.5", 39 | "sass-loader": "^8.0.2", 40 | "script-ext-html-webpack-plugin": "2.1.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 101 | 102 | 134 | 135 | -------------------------------------------------------------------------------- /frontend/src/api/friendly.js: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | export function checkIsFriends(data) { 4 | return http.post(`/friendly/checkIsFriends`, data) 5 | } 6 | export function findMyFriendsList(params) { 7 | return http({ 8 | url: `/friendly/findMyFriendsList`, 9 | method: 'get', 10 | params 11 | }) 12 | // return http.post(`/friendly/findMyFriendsList`, data) 13 | } 14 | export function deleteFriend(data) { 15 | return http.post(`/friendly/deleteFriend`, data) 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/api/group.js: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | export function createGroup(data) { 4 | // 新建群 5 | return http.post(`/group/createGroup`, data) 6 | } 7 | export function findMyGroupList(params) { 8 | // 获取我的群 9 | // return http.post(`/group/getMyGroup`, data) 10 | return http({ 11 | url: `/group/getMyGroup`, 12 | method: 'get', 13 | params 14 | }) 15 | } 16 | export function getGroupUsers(params) { 17 | return http({ 18 | url: `/group/getGroupUsers`, 19 | method: 'get', 20 | params 21 | }) 22 | // return http.post(`/group/getGroupUsers`, data) 23 | } 24 | export function huntGroups(params) { 25 | return http({ 26 | url: `/group/huntGroups`, 27 | method: 'get', 28 | params 29 | }) 30 | // return http.post(`/group/huntGroups`, data) 31 | } 32 | export function getGroupInfo(params) { 33 | return http({ 34 | url: `/group/getGroupInfo`, 35 | method: 'get', 36 | params 37 | }) 38 | // return http.post(`/group/getGroupInfo`, data) 39 | } 40 | export function quitGroup(data) { 41 | return http.post(`/group/quitGroup`, data) 42 | } 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/api/index.js: -------------------------------------------------------------------------------- 1 | import user from './modules/user' 2 | import friendly from './modules/friendly' 3 | import upload from './modules/upload' 4 | import group from './modules/group' 5 | 6 | export default { 7 | ...user, 8 | ...friendly, 9 | ...upload, 10 | ...group 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/api/modules/friendly.js: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | import url from './url' 3 | 4 | const friendly = { 5 | checkIsFriends(data) { 6 | return http.post(`${url.friendly}/checkIsFriends`, data) 7 | }, 8 | findMyFriendsList(data) { 9 | return http.post(`${url.friendly}/findMyFriendsList`, data) 10 | }, 11 | deleteFriend(data) { 12 | return http.post(`${url.friendly}/deleteFriend`, data) 13 | } 14 | } 15 | 16 | export default friendly 17 | -------------------------------------------------------------------------------- /frontend/src/api/modules/group.js: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | import url from './url' 3 | const group = { 4 | createGroup(data) { 5 | // 新建群 6 | return http.post(`${url.group}/createGroup`, data) 7 | }, 8 | findMyGroupList(data) { 9 | // 获取我的群 10 | return http.post(`${url.group}/getMyGroup`, data) 11 | }, 12 | getGroupUsers(data) { 13 | return http.post(`${url.group}/getGroupUsers`, data) 14 | }, 15 | huntGroups(data) { 16 | return http.post(`${url.group}/huntGroups`, data) 17 | }, 18 | getGroupInfo(data) { 19 | return http.post(`${url.group}/getGroupInfo`, data) 20 | }, 21 | quitGroup(data) { 22 | return http.post(`${url.group}/quitGroup`, data) 23 | } 24 | } 25 | 26 | export default group 27 | -------------------------------------------------------------------------------- /frontend/src/api/modules/upload.js: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | import url from './url' 3 | 4 | const upload = { 5 | uploadFile(data) { 6 | return http.post(`${url.upload}/uploadFile`, data) 7 | } 8 | } 9 | 10 | export default upload 11 | -------------------------------------------------------------------------------- /frontend/src/api/modules/url.js: -------------------------------------------------------------------------------- 1 | const base = { 2 | user: 'http://localhost:3000/api/user', 3 | friendly: 'http://localhost:3000/api/friendly', 4 | upload: 'http://localhost:3000/api/upload', 5 | group: 'http://localhost:3000/api/group' 6 | } 7 | 8 | export default base 9 | -------------------------------------------------------------------------------- /frontend/src/api/modules/user.js: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | import url from './url' 3 | 4 | const user = { 5 | // 注册 | POST 6 | register(data) { 7 | return http.post(`${url.user}/register`, data) 8 | }, 9 | // 登录 | POST 10 | login(data) { 11 | return http.post(`${url.user}/login`, data) 12 | }, 13 | // 发送短信验证码 | POST 14 | sendSMSCode(mobilePhone) { 15 | return http.post(`${url.user}/sendSMSCode`, mobilePhone) 16 | }, 17 | // 发送图形验证码 | GET 18 | sendPicCode() { 19 | return `${url.user}/sendPicCode?mt=${Math.random()}` 20 | }, 21 | // 获取用户个人信息 22 | getUserInfo() { 23 | return http.get(`${url.user}/getUserInfo`) 24 | }, 25 | 26 | // 获取官方账号信息 27 | getOfficialInfo() { 28 | return http.get(`${url.user}/getOfficialInfo`) 29 | }, 30 | // 预览账号信息 31 | previewUser(data) { 32 | return http.post(`${url.user}/previewUser`, data) 33 | }, 34 | // 更新用户信息 | POST 35 | updateUserInfo(data) { 36 | return http.post(`${url.user}/updateUserInfo`, data) 37 | }, 38 | // 搜索用户 39 | addFriends(data) { 40 | return http.post(`${url.user}/searchFriends`, data) 41 | }, 42 | // 添加至用户会话列表 43 | addConversationList(data) { 44 | return http.post(`${url.user}/addConversationList`, data) 45 | }, 46 | // 更新用户手机号码 47 | updatedUserPhone(data) { 48 | return http.post(`${url.user}/updatedUserPhone`, data) 49 | }, 50 | // 更新用户密码 51 | updatedUserPassword(data) { 52 | return http.post(`${url.user}/updatedUserPassword`, data) 53 | }, 54 | // 修改好友备注 55 | modifyFriendRemark(data) { 56 | return http.post(`${url.user}/modifyFriendRemark`, data) 57 | }, 58 | // 更新用户会话列表 59 | updateUserConversations(data) { 60 | return http.post(`${url.user}/updateUserConversations`, data) 61 | }, 62 | // 删除用户会话列表的某个对话 63 | deleteDialog(data) { 64 | return http.post(`${url.user}/deleteDialog`, data) 65 | } 66 | } 67 | export default user 68 | -------------------------------------------------------------------------------- /frontend/src/api/upload.js: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | 3 | export function uploadFile(data) { 4 | return http.post(`/upload/uploadFile`, data) 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/api/url.js: -------------------------------------------------------------------------------- 1 | const base = { 2 | user: 'http://localhost:3000/api/user', 3 | friendly: 'http://localhost:3000/api/friendly', 4 | upload: 'http://localhost:3000/api/upload', 5 | group: 'http://localhost:3000/api/group' 6 | } 7 | 8 | export default base 9 | -------------------------------------------------------------------------------- /frontend/src/api/user.js: -------------------------------------------------------------------------------- 1 | import http from '@/utils/http' 2 | // 注册 | POST 3 | export function register(data) { 4 | return http.post(`/user/register`, data) 5 | } 6 | // 登录 | POST 7 | export function login(data) { 8 | return http.post(`/user/login`, data) 9 | } 10 | // 发送短信验证码 | POST 11 | export function sendSMSCode(mobilePhone) { 12 | return http.post(`/user/sendSMSCode`, mobilePhone) 13 | } 14 | // 发送图形验证码 | GET 15 | export function sendPicCode() { 16 | return http({ 17 | url: '/user/sendPicCode', 18 | method: 'get', 19 | params: { mt: Math.random() }, 20 | responseType: '' 21 | }) 22 | } 23 | // 获取用户个人信息 24 | export function getUserInfo() { 25 | return http.get(`/user/getUserInfo`) 26 | } 27 | // 获取官方账号信息 28 | export function getOfficialInfo() { 29 | return http.get(`/user/getOfficialInfo`) 30 | } 31 | // 预览账号信息 32 | export function previewUser(params) { 33 | return http({ 34 | url: '/user/previewUser', 35 | method: 'get', 36 | params 37 | }) 38 | // return http.post(`/user/previewUser`, data) 39 | } 40 | // 更新用户信息 | POST 41 | export function updateUserInfo(data) { 42 | return http.put(`/user/updateUserInfo`, data) 43 | } 44 | // 搜索用户 45 | export function addFriends(params) { 46 | return http({ 47 | url: `/user/searchFriends`, 48 | method: 'get', 49 | params 50 | }) 51 | // return http.post(`/user/searchFriends`, data) 52 | } 53 | // 添加至用户会话列表 54 | export function addConversationList(data) { 55 | return http.post(`/user/addConversationList`, data) 56 | } 57 | // 更新用户手机号码 58 | export function updatedUserPhone(data) { 59 | return http.put(`/user/updatedUserPhone`, data) 60 | } 61 | // 更新用户密码 62 | export function updatedUserPassword(data) { 63 | return http.put(`/user/updatedUserPassword`, data) 64 | } 65 | // 修改好友备注 66 | export function modifyFriendRemark(data) { 67 | return http.put(`/user/modifyFriendRemark`, data) 68 | } 69 | // 更新用户会话列表 70 | export function updateUserConversations(data) { 71 | return http.put(`/user/updateUserConversations`, data) 72 | } 73 | // 删除用户会话列表的某个对话 74 | export function deleteDialog(data) { 75 | return http.post(`/user/deleteDialog`, data) 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/assets/css/icon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('../fonts/icomoon.eot?hgkmhj'); 4 | src: url('../fonts/icomoon.eot?hgkmhj#iefix') format('embedded-opentype'), 5 | url('../fonts/icomoon.ttf?hgkmhj') format('truetype'), 6 | url('../fonts/icomoon.woff?hgkmhj') format('woff'), 7 | url('../fonts/icomoon.svg?hgkmhj#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | font-display: block; 11 | } 12 | 13 | [class^="icon-"], 14 | [class*=" icon-"] { 15 | /* use !important to prevent issues with browser extensions that change fonts */ 16 | font-family: 'icomoon' !important; 17 | speak: never; 18 | font-style: normal; 19 | font-weight: normal; 20 | font-variant: normal; 21 | text-transform: none; 22 | line-height: 1; 23 | 24 | /* Better Font Rendering =========== */ 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | } 28 | 29 | .icon-arrow-up-alt1:before { 30 | content: "\e900"; 31 | } 32 | 33 | .icon-bubbles2:before { 34 | content: "\e96d"; 35 | } 36 | 37 | .icon-attachment:before { 38 | content: "\e9cd"; 39 | } 40 | 41 | .icon-circle-down:before { 42 | content: "\ea43"; 43 | } 44 | 45 | .icon-shield:before { 46 | content: "\f132"; 47 | } 48 | 49 | .icon-group_add:before { 50 | content: "\e901"; 51 | } 52 | 53 | .icon-person_add:before { 54 | content: "\e905"; 55 | } 56 | 57 | .icon-volume:before { 58 | content: "\e902"; 59 | } -------------------------------------------------------------------------------- /frontend/src/assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /frontend/src/assets/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /frontend/src/assets/imgs/Nofifications and Sounds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/imgs/Nofifications and Sounds.png -------------------------------------------------------------------------------- /frontend/src/assets/imgs/Privacy and Storage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/imgs/Privacy and Storage.png -------------------------------------------------------------------------------- /frontend/src/assets/imgs/Saved Message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/imgs/Saved Message.png -------------------------------------------------------------------------------- /frontend/src/assets/imgs/bgi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/imgs/bgi.jpg -------------------------------------------------------------------------------- /frontend/src/assets/imgs/error-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/imgs/error-img.png -------------------------------------------------------------------------------- /frontend/src/assets/imgs/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/imgs/file.png -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reeswell/vue-chat/ddead9210e567f878570f63adc2b3727ead2d191/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/assets/scss/color.scss: -------------------------------------------------------------------------------- 1 | 2 | $blue: #007aff; 3 | $gray: #e9e9e9; 4 | $graySplit: #efeff4; 5 | $grayTabBgc: #1c1c1e; 6 | $border: #d2d1d5; 7 | $title: #676767; 8 | $newsBgc: #f1f1f4; 9 | $lightGray: #b4b8b9d3; 10 | $grayTab: #929292; 11 | $backgroundColor: #f6f7f9; 12 | $friendPre: #f5f6f9; 13 | $friendSplit: #f5f6fa; 14 | // $otherMsgColor: #f3f3f3; 15 | // $myMsgColor: #daf4fe; 16 | // $warnRed: #e44545; 17 | // $lightBruce: #f3f8ff; 18 | $lightBgc: #1c1c1e; 19 | $deepBgc: #0f0f0f; 20 | $blueBgc: #3477f5; 21 | $fontGray: #a4a3a8; 22 | $fontWhite: #fff; 23 | $fontBlack: #000; 24 | $redBadge: #ee0a24; -------------------------------------------------------------------------------- /frontend/src/assets/scss/common-split.scss: -------------------------------------------------------------------------------- 1 | .split-a, 2 | .split-b { 3 | background-color: #efeff4; 4 | position: relative; 5 | box-sizing: border-box; 6 | width: 100%; 7 | height: 55px; 8 | padding: 8px 16px; 9 | p { 10 | line-height: 18px; 11 | color: #7d7d82; 12 | span { 13 | color: #26a2ff; 14 | } 15 | } 16 | } 17 | .split-a { 18 | height: 55px; 19 | .specail-a { 20 | position: absolute; 21 | bottom: 0; 22 | } 23 | } 24 | .split-b { 25 | height: 95px; 26 | .last-p { 27 | position: absolute; 28 | bottom: 0; 29 | } 30 | } -------------------------------------------------------------------------------- /frontend/src/assets/scss/mixin.scss: -------------------------------------------------------------------------------- 1 | 2 | @mixin border-1px($color) { 3 | position: relative; 4 | &:after { 5 | content: ''; 6 | position: absolute; 7 | bottom: 0; 8 | left: 0; 9 | display: block; 10 | width: 100%; 11 | border: 1px solid $color; 12 | } 13 | } 14 | @mixin border-none() { 15 | &::after { 16 | display: none; 17 | } 18 | &::before { 19 | display: none; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/assets/scss/my-vant.scss: -------------------------------------------------------------------------------- 1 | @import './color.scss'; 2 | 3 | 4 | .van-hairline::after { 5 | border-color: #d2d1d5; 6 | } 7 | .van-nav-bar__content{ 8 | height: 40px; 9 | } 10 | .van-nav-bar__title{ 11 | color: #fbfbfc; 12 | 13 | } 14 | .van-tab{ 15 | color: $fontGray; 16 | } 17 | .van-tab--active{ 18 | color: $blueBgc; 19 | } 20 | .van-tabs__line{ 21 | background-color: $blueBgc; 22 | } 23 | // [class='van-hairline--bottom']::after{ 24 | // border: 1px solid $fontGray; 25 | // } 26 | .van-badge{ 27 | background-color: $blueBgc; 28 | } 29 | 30 | .van-badge--dot{ 31 | background-color: $blueBgc; 32 | } 33 | .van-search{ 34 | background-color: $lightBgc; 35 | } 36 | .van-nav-bar{ 37 | background-color: $lightBgc; 38 | } 39 | .van-field__control{ 40 | color: $fontWhite; 41 | } 42 | .van-dialog{ 43 | background-color: $lightBgc; 44 | color: $fontGray; 45 | .van-button { 46 | background-color: $lightBgc; 47 | color: $fontGray; 48 | } 49 | } 50 | 51 | .van-field__left-icon { 52 | color: $fontGray; 53 | } 54 | 55 | 56 | 57 | .van-field__left-icon{ 58 | color: $fontGray; 59 | } 60 | .van-search{ 61 | background-color: $lightBgc; 62 | } 63 | .van-search__content{ 64 | background-color: $deepBgc; 65 | } 66 | .van-field__control::placeholder{ 67 | color: $fontGray; 68 | } 69 | .van-search__action{ 70 | color: $fontWhite; 71 | } 72 | input { 73 | border: 0; 74 | } -------------------------------------------------------------------------------- /frontend/src/components/footerNav.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 93 | 94 | 129 | -------------------------------------------------------------------------------- /frontend/src/components/loading.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import api from './api/index' // 导入ajax接口 6 | import { 7 | Form, 8 | Lazyload, 9 | Toast, 10 | Badge, 11 | Button, 12 | Icon, 13 | Cell, 14 | Popup, 15 | NavBar, 16 | Search, 17 | Sticky, 18 | Tab, 19 | Tabs, 20 | SwipeCell, 21 | PullRefresh, 22 | Image as VanImage, 23 | Overlay, 24 | RadioGroup, 25 | Radio, 26 | Uploader, 27 | Area, 28 | Field, 29 | ActionSheet, 30 | Loading 31 | } from 'vant' 32 | 33 | import Socketio from '@/plugins/Socket.io' 34 | import 'vant/lib/index.css' // 全局引入样式 35 | 36 | import '@/assets/css/icon.css' 37 | import '@/assets/scss/my-vant.scss' 38 | 39 | const app = createApp(App) // 创建实例 40 | 41 | app.use(Socketio, { 42 | connection: process.env.VUE_APP_SOCKET_API, 43 | options: { 44 | transports: ['websocket'], 45 | debug: true, 46 | vuex: {} 47 | } 48 | }) 49 | 50 | app.config.globalProperties.$api = api // 将ajax挂载到vue的原型上 51 | app.config.productionTip = false 52 | 53 | app 54 | .use(Form) 55 | .use(Lazyload) 56 | .use(Toast) 57 | .use(Badge) 58 | .use(Button) 59 | .use(Icon) 60 | .use(Cell) 61 | .use(Popup) 62 | .use(NavBar) 63 | .use(Search) 64 | .use(Sticky) 65 | .use(Tab) 66 | .use(Tabs) 67 | .use(SwipeCell) 68 | .use(PullRefresh) 69 | .use(VanImage) 70 | .use(Overlay) 71 | .use(RadioGroup) 72 | .use(Radio) 73 | .use(Uploader) 74 | .use(Area) 75 | .use(Field) 76 | .use(ActionSheet) 77 | .use(Loading) 78 | 79 | app.use(Lazyload, { 80 | preLoad: 1, // proportion of pre-loading height, 个人理解是图片加载前目标元素位置范围 81 | error: require('./assets/imgs/error-img.png'), 82 | loading: require('./assets/imgs/error-img.png'), 83 | attempt: 3 // 下载图片时错误重连次数 84 | }) 85 | 86 | app.use(store).use(router).mount('#app') 87 | -------------------------------------------------------------------------------- /frontend/src/plugins/Socket.io.js: -------------------------------------------------------------------------------- 1 | import {io} from 'socket.io-client' 2 | 3 | export default { 4 | install: (app, {connection, options}) => { 5 | const socket = io(connection, options) 6 | app.config.globalProperties.$socket = socket 7 | 8 | app.provide('socket', socket) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import store from '../store/index' 3 | 4 | const routes = [ 5 | { 6 | path: '/', 7 | redirect: 'Chat' 8 | }, 9 | { 10 | path: '/chat', 11 | name: 'Chat', 12 | component: () => import('@/views/Chats/chats'), 13 | meta: { 14 | keepAlive: false, 15 | index: 2, 16 | requiresAuth: true, 17 | tag: 'chat' 18 | } 19 | }, 20 | { 21 | path: '/chat/searchLocal', 22 | name: 'SearchLocal', 23 | component: () => import('@/views/SearchLocal/searchLocal'), 24 | meta: { 25 | keepAlive: true, 26 | index: 4, 27 | requiresAuth: true, 28 | tag: 'search' 29 | } 30 | }, 31 | { 32 | path: '/chat/searchFriend', 33 | name: 'SearchFriend', 34 | component: () => import('@/views/SearchFriend/searchFriend'), 35 | meta: { 36 | keepAlive: true, 37 | index: 6, 38 | requiresAuth: true 39 | } 40 | }, 41 | { 42 | path: '/chat/friendDetail/:id', 43 | name: 'FriendDetail', 44 | component: () => import('@/views/FriendDetail/friendDetail'), 45 | meta: { 46 | keepAlive: true, 47 | index: 7, 48 | requiresAuth: true 49 | } 50 | }, 51 | { 52 | path: '/chat/SendFriendValidate/:id', 53 | name: 'SendFriendValidate', 54 | component: () => import('@/views/SendFriendValidate/sendFriendValidate'), 55 | meta: { 56 | keepAlive: false, 57 | index: 8, 58 | requiresAuth: true 59 | } 60 | }, 61 | { 62 | path: '/chat/searchGroup', 63 | name: 'SearchGroup', 64 | component: () => import('@/views/SearchGroup/searchGroup'), 65 | meta: { 66 | keepAlive: true, 67 | index: 6, 68 | requiresAuth: true 69 | } 70 | }, 71 | { 72 | path: '/chat/sendGroupValidate/:id', 73 | name: 'SendGroupValidate', 74 | component: () => import('@/views/SendGroupValidate/sendGroupValidate'), 75 | meta: { 76 | keepAlive: false, 77 | index: 6, 78 | requiresAuth: true 79 | } 80 | }, 81 | { 82 | path: '/chat/groupDetail/:id', 83 | name: 'GroupDetail', 84 | component: () => import('@/views/GroupDetail/groupDetail'), 85 | meta: { 86 | keepAlive: true, 87 | index: 6, 88 | requiresAuth: true 89 | } 90 | }, 91 | { 92 | path: '/chat/createGroup', 93 | name: 'CreateGroup', 94 | component: () => import('@/views/CreateGroup/createGroup'), 95 | meta: { 96 | keepAlive: false, 97 | index: 6, 98 | requiresAuth: true 99 | } 100 | }, 101 | 102 | { 103 | path: '/contact', 104 | name: 'Contact', 105 | component: () => import('@/views/Contacts/contacts'), 106 | meta: { 107 | keepAlive: false, 108 | 109 | index: 2, 110 | requiresAuth: true, 111 | tag: 'contact' 112 | } 113 | }, 114 | { 115 | path: '/manager', 116 | name: 'Manager', 117 | component: () => import('@/views/Manager/manager'), 118 | meta: { 119 | keepAlive: false, 120 | index: 2, 121 | requiresAuth: true 122 | } 123 | }, 124 | { 125 | path: '/manager/accountSafe', 126 | name: 'AccountSafe', 127 | component: () => import('@/views/AccountSafe/accountSafe'), 128 | meta: { 129 | keepAlive: false, 130 | index: 4, 131 | requiresAuth: true 132 | } 133 | }, 134 | { 135 | path: '/manager/accountSafe/editPhone', 136 | name: 'EditPhone', 137 | meta: { 138 | keepAlive: false, 139 | index: 5, 140 | requiresAuth: true 141 | }, 142 | component: () => import('@/views/EditPhone/editPhone') 143 | }, 144 | { 145 | path: '/manager/accountSafe/editPassword', 146 | name: 'EditPassword', 147 | meta: { 148 | keepAlive: false, 149 | index: 5, 150 | requiresAuth: true 151 | }, 152 | component: () => import('@/views/EditPassword/editPassword') 153 | }, 154 | { 155 | path: '/manager/sysMes', 156 | name: 'SysMes', 157 | component: () => import('@/views/SysMes/sysMes'), 158 | meta: { 159 | keepAlive: false, 160 | index: 4, 161 | requiresAuth: true 162 | } 163 | }, 164 | { 165 | path: '/manager/sysMes/applyDetail/:id', 166 | name: 'ApplyDetail', 167 | component: () => import('@/views/ApplyDetail/applyDetail'), 168 | meta: { 169 | keepAlive: false, 170 | index: 5, 171 | requiresAuth: true 172 | } 173 | }, 174 | { 175 | path: '/manager/edit', 176 | name: 'Edit', 177 | meta: { 178 | keepAlive: false, 179 | index: 4, 180 | requiresAuth: true 181 | }, 182 | component: () => import('@/views/Edit/edit') 183 | }, 184 | 185 | { 186 | path: '/manager/edit/editName', 187 | name: 'EditName', 188 | meta: { 189 | keepAlive: false, 190 | index: 5, 191 | requiresAuth: true 192 | }, 193 | component: () => import('@/views/EditName/editName') 194 | }, 195 | { 196 | path: '/manager/edit/editGender', 197 | name: 'EditGender', 198 | meta: { 199 | keepAlive: false, 200 | index: 5, 201 | requiresAuth: true 202 | }, 203 | component: () => import('@/views/EditGender/editGender') 204 | }, 205 | { 206 | path: '/manager/edit/editEmail', 207 | name: 'EditEmail', 208 | meta: { 209 | keepAlive: false, 210 | index: 5, 211 | requiresAuth: true 212 | }, 213 | component: () => import('@/views/EditEmail/editEmail') 214 | }, 215 | { 216 | path: '/manager/edit/editAge', 217 | name: 'EditAge', 218 | meta: { 219 | keepAlive: false, 220 | index: 5, 221 | requiresAuth: true 222 | }, 223 | component: () => import('@/views/EditAge/editAge') 224 | }, 225 | { 226 | path: '/chat/mesPanel/:id', 227 | name: 'MesPanel', 228 | meta: { 229 | keepAlive: true, 230 | index: 5, 231 | requiresAuth: true 232 | }, 233 | component: () => import('@/views/MesPanel/mesPanel') 234 | }, 235 | { 236 | path: '/chat/mesPanel/friendsInfo/:id', 237 | name: 'FriendsInfo', 238 | meta: { 239 | keepAlive: false, 240 | index: 6, 241 | requiresAuth: true 242 | }, 243 | component: () => import('@/views/FriendsInfo/friendsInfo') 244 | }, 245 | { 246 | path: '/chat/mesPanel/groupInfo/:id', 247 | name: 'GroupInfo', 248 | meta: { 249 | keepAlive: false, 250 | index: 6, 251 | requiresAuth: true 252 | }, 253 | component: () => import('@/views/GroupInfo/groupInfo') 254 | }, 255 | { 256 | path: '/chat/mesPanel/friendsInfo/editRemark/:id', 257 | name: 'EditRemark', 258 | meta: { 259 | keepAlive: false, 260 | index: 7, 261 | requiresAuth: true 262 | }, 263 | component: () => import('@/views/EditRemark/editRemark') 264 | }, 265 | { 266 | path: '/login', 267 | name: 'Login', 268 | component: () => import('@/views/Login/login'), 269 | meta: { 270 | keepAlive: false, 271 | index: 20 272 | } 273 | } 274 | ] 275 | 276 | const router = createRouter({ 277 | history: createWebHistory(), 278 | routes, 279 | scrollBehavior: () => ({ left: 0, top: 0 }) 280 | 281 | }) 282 | // 未登录用户路由拦截 283 | router.beforeEach((to, from, next) => { 284 | const loginStatus = store.state.loginStatus 285 | 286 | if (to.meta.requiresAuth) { 287 | if (loginStatus) { 288 | next() 289 | } else { 290 | store.dispatch('logOut') 291 | next({ 292 | path: '/login', 293 | query: { 294 | redirect: to.fullPath 295 | } 296 | }) 297 | } 298 | } else { 299 | next() 300 | } 301 | }) 302 | 303 | export default router 304 | -------------------------------------------------------------------------------- /frontend/src/store/types.js: -------------------------------------------------------------------------------- 1 | export const TOKEN = 'TOKEN' // 登录 2 | export const USER_INFO = 'USER_INFO' // 用户信息 3 | export const SYS_INFO = 'SYS_INFO' 4 | export const LOGIN_STATUS = 'LOGIN_STATUS' // 登录状态 5 | export const FRIENDS_INFO = 'FRIENDS_INFO' 6 | export const GROUP_INFO = 'GROUP_INFO' 7 | export const CONVERSATIONS_LIST = 'CONVERSATIONS_LIST' 8 | export const ONLINE_USER = 'ONLINE_USER' 9 | export const UNREAD = 'UNREAD' 10 | export const SYS_NEWS_LIST = 'SYS_NEWS_LIST' 11 | export const FRIENDS_LIST = 'FRIENDS_LIST' 12 | export const GROUPS_LIST = 'GROUPS_LIST' 13 | export const ALL_CHAT_LIST = 'ALL_CHAT_LIST' 14 | -------------------------------------------------------------------------------- /frontend/src/utils/cache.js: -------------------------------------------------------------------------------- 1 | const storage = window.localStorage 2 | 3 | const USER_TOKEN = 'user_token' // token key 4 | const USER_INFO = 'user_info' // token key 5 | const SYS_INFO = 'sys_info' 6 | const CONVERSATIONS_LIST = 'conversations_list' 7 | const SYS_NEWS_LIST = 'sysNewsList' 8 | const FRIENDS_LIST = 'friendsList' 9 | const GROUPS_LIST = 'groupsList' 10 | const ALL_CHAT_LIST = 'allChatList' 11 | /** S 12 | * token 缓存 13 | */ 14 | const tokenCache = { 15 | setCache(userToken = '') { 16 | storage.setItem(USER_TOKEN, JSON.stringify(userToken)) 17 | return userToken 18 | }, 19 | getCache() { 20 | return storage.getItem(USER_TOKEN) ? JSON.parse(storage.getItem(USER_TOKEN)) : '' 21 | }, 22 | deleteCache() { 23 | storage.removeItem(USER_TOKEN) 24 | return '' 25 | } 26 | } 27 | // 个人信息缓存 28 | const userInfoCache = { 29 | setCache(userInfo = {}) { 30 | storage.setItem(USER_INFO, JSON.stringify(userInfo)) 31 | return userInfo 32 | }, 33 | getCache() { 34 | return storage.getItem(USER_INFO) ? JSON.parse(storage.getItem(USER_INFO)) : {} 35 | }, 36 | deleteCache() { 37 | storage.removeItem(USER_INFO) 38 | return {} 39 | } 40 | } 41 | // 保存官方账号信息 42 | const sysInfoCache = { 43 | setCache(sysInfo = {}) { 44 | storage.setItem(SYS_INFO, JSON.stringify(sysInfo)) 45 | return sysInfo 46 | }, 47 | getCache() { 48 | return storage.getItem(SYS_INFO) ? JSON.parse(storage.getItem(SYS_INFO)) : {} 49 | }, 50 | deleteCache() { 51 | storage.removeItem(SYS_INFO) 52 | return {} 53 | } 54 | } 55 | const conversationsListCache = { 56 | setCache(conversationsList = []) { 57 | storage.setItem(CONVERSATIONS_LIST, JSON.stringify(conversationsList)) 58 | return conversationsList 59 | }, 60 | getCache() { 61 | return storage.getItem(CONVERSATIONS_LIST) ? JSON.parse(storage.getItem(CONVERSATIONS_LIST)) : [] 62 | }, 63 | deleteCache() { 64 | storage.removeItem(CONVERSATIONS_LIST) 65 | return [] 66 | } 67 | } 68 | const sysNewsListCache = { 69 | setCache(sysNewsList = []) { 70 | storage.setItem(SYS_NEWS_LIST, JSON.stringify(sysNewsList)) 71 | return sysNewsList 72 | }, 73 | getCache() { 74 | return storage.getItem(SYS_NEWS_LIST) ? JSON.parse(storage.getItem(SYS_NEWS_LIST)) : [] 75 | }, 76 | deleteCache() { 77 | storage.removeItem(SYS_NEWS_LIST) 78 | return [] 79 | } 80 | } 81 | const friendsListCache = { 82 | setCache(friendsList = []) { 83 | storage.setItem(FRIENDS_LIST, JSON.stringify(friendsList)) 84 | return friendsList 85 | }, 86 | getCache() { 87 | return storage.getItem(FRIENDS_LIST) ? JSON.parse(storage.getItem(FRIENDS_LIST)) : [] 88 | }, 89 | deleteCache() { 90 | storage.removeItem(FRIENDS_LIST) 91 | return [] 92 | } 93 | } 94 | const groupsListCache = { 95 | setCache(groupsList = []) { 96 | storage.setItem(GROUPS_LIST, JSON.stringify(groupsList)) 97 | return groupsList 98 | }, 99 | getCache() { 100 | return storage.getItem(GROUPS_LIST) ? JSON.parse(storage.getItem(GROUPS_LIST)) : [] 101 | }, 102 | deleteCache() { 103 | storage.removeItem(GROUPS_LIST) 104 | return [] 105 | } 106 | } 107 | const allChatListCache = { 108 | setCache(friendsList = []) { 109 | storage.setItem(ALL_CHAT_LIST, JSON.stringify(friendsList)) 110 | return friendsList 111 | }, 112 | getCache() { 113 | return storage.getItem(ALL_CHAT_LIST) ? JSON.parse(storage.getItem(ALL_CHAT_LIST)) : [] 114 | }, 115 | deleteCache() { 116 | storage.removeItem(ALL_CHAT_LIST) 117 | return [] 118 | } 119 | } 120 | export { 121 | tokenCache, // token 缓存 122 | userInfoCache, 123 | sysInfoCache, 124 | conversationsListCache, 125 | sysNewsListCache, 126 | allChatListCache, 127 | friendsListCache, 128 | groupsListCache 129 | } 130 | -------------------------------------------------------------------------------- /frontend/src/utils/emoji.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": "😀,😁,😂,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,😇,😐,😑,😶,😏,😣,😥,😮,😯,😪,😫,😴,😌,😛,😜,😝,😒,😓,😔,😕,😲,😷,😖,😞,😟,😤,😢,😭,😦,😧,😨,😬,😰,😱,😳,😵,😡,😠,💘,❤,💓,💔,💕,💖,💗,💙,💚,💛,💜,💝,💞,💟,❣,💪,👈,👉,☝,👆,👇,✌,✋,👌,👍,👎,✊,👊,👋,👏,👐,✍,🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🍅,🍆,🌽,🍄,🌰,🍞,🍖,🍗,🍔,🍟,🍕,🍳,🍲,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🍫,🍬,🍭,🍮,🍯,🍼,☕,🍵,🍶,🍷,🍸,🍹,🍺,🍻,🍴,🌹,🍀,🍎,💰,📱,🌙,🍁,🍂,🍃,🌷,💎,🔪,🔫,🏀,⚽,⚡,👄,👍,🔥,🙈,🙉,🙊,🐵,🐒,🐶,🐕,🐩,🐺,🐱,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🐈,🐯,🐅,🐆,🐴,🐎,🐮,🐂,🐃,🐄,🐷,🐖,🐗,🐽,🐏,🐑,🐐,🐪,🐫,🐘,🐭,🐁,🐀,🐹,🐰,🐇,🐻,🐨,🐼,🐾,🐔,🐓,🐣,🐤,🐥,🐦,🐧,🐸,🐊,🐢,🐍,🐲,🐉,🐳,🐋,🐬,🐟,🐠,🐡,🐙,🐚,🐌,🐛,🐜,🐝,🐞,🦋,😈,👿,👹,👺,💀,☠,👻,👽,👾,💣" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/utils/http.js: -------------------------------------------------------------------------------- 1 | /** 2 | * axios封装 3 | * 请求拦截、响应拦截、错误统一处理 4 | */ 5 | import axios from 'axios' 6 | import { Toast } from 'vant' 7 | import { tokenCache, userInfoCache, sysInfoCache, conversationsListCache, sysNewsListCache } from '@/utils/cache' 8 | /** 9 | * 提示函数 10 | * 禁止点击蒙层、显示一秒后关闭 11 | */ 12 | 13 | const tip = (msg, type = '') => { 14 | if (type === 'success') { 15 | return Toast({ 16 | type: 'success', 17 | message: msg, 18 | duration: 1000, 19 | forbidClick: true 20 | }) 21 | } 22 | Toast({ 23 | type: 'fail', 24 | message: msg, 25 | duration: 1000, 26 | forbidClick: true 27 | }) 28 | } 29 | 30 | /** 31 | * 跳转登录页 32 | * 携带当前页面路由,以期在登录页面完成登录后返回当前页面 33 | */ 34 | const toLogin = () => { 35 | window.location.href = '/#/login' 36 | } 37 | 38 | /** 39 | * 请求失败后的错误统一处理 40 | * @param {Number} status 请求失败的状态码 41 | */ 42 | const errorHandle = (status, other) => { 43 | // 状态码判断 44 | switch (status) { 45 | case 401: 46 | tip('登录过期,请重新登录') 47 | tokenCache.deleteCache() 48 | userInfoCache.deleteCache() 49 | sysInfoCache.deleteCache() 50 | conversationsListCache.deleteCache() 51 | sysNewsListCache.deleteCache() 52 | setTimeout(() => { 53 | toLogin() 54 | }, 1000) 55 | break 56 | case 500: 57 | tip('服务器错误') 58 | break 59 | default: 60 | console.error(other) 61 | } 62 | } 63 | 64 | // 创建axios实例 65 | const instance = axios.create({ 66 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 67 | timeout: 1000 * 12, 68 | withCredentials: true 69 | }) 70 | // 设置post请求头 71 | // instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; 72 | /** 73 | * 请求拦截器 74 | * 每次请求前,如果存在token则在请求头中携带token 75 | */ 76 | instance.interceptors.request.use( 77 | config => { 78 | // 登录流程控制中,根据本地是否存在token判断用户的登录情况 79 | // 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token 80 | // 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码 81 | // 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。 82 | const token = tokenCache.getCache() 83 | // console.log(token); 84 | // token && (config.headers.Authorization = token); 85 | token && (config.headers['Authorization'] = `Bearer ${token}`) 86 | return config 87 | }, 88 | error => Promise.error(error) 89 | ) 90 | 91 | // 响应拦截器 92 | instance.interceptors.response.use( 93 | response => { 94 | if (response.status === 200) { 95 | // 你只需改动的是这个 succeeCode ,因为每个项目的后台返回的code码各不相同 96 | 97 | if (response.data.code === 200) { 98 | return response.data 99 | } else { 100 | tip(response.data.msg) 101 | return Promise.reject(response) 102 | } 103 | } else { 104 | return Promise.reject(response) 105 | } 106 | }, 107 | // 请求失败 108 | error => { 109 | const { response } = error 110 | if (response) { 111 | // 请求已发出,但是不在2xx的范围 112 | errorHandle(response.status, response.data.message) 113 | return Promise.reject(response) 114 | } else { 115 | // 处理断网的情况 116 | // eg:请求超时或断网时,更新state的network状态 117 | // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏 118 | // 关于断网组件中的刷新重新获取数据,会在断网组件中说明 119 | if (!window.navigator.onLine) { 120 | tip('你的网络已断开,请检查网络') 121 | } else { 122 | return Promise.reject(error) 123 | } 124 | } 125 | } 126 | ) 127 | 128 | export default instance 129 | -------------------------------------------------------------------------------- /frontend/src/utils/tools.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 函数节流方法 3 | * @param {Function} fn 延时调用函数 4 | * @param {Number} delay 延迟多长时间 5 | * @param {Number} atleast 至少多长时间触发一次 6 | * @return {Function} 延迟执行的方法 7 | */ 8 | function throttle(fn, delay, atleast = 0) { 9 | let timer = null // 记录定时器 10 | let previous = 0 // 记录上一次执行时间 11 | 12 | return (...args) => { 13 | const now = +new Date() // 当前时间戳 14 | if (!previous) previous = now // 赋值开始时间 15 | 16 | if (atleast && now - previous > atleast) { 17 | fn.apply(this, args) 18 | // 重置上一次开始时间为本次结束时间 19 | previous = now 20 | timer && clearTimeout(timer) 21 | } else { 22 | timer && clearTimeout(timer) // 清除上次定时器 23 | timer = setTimeout(() => { 24 | fn.apply(this, args) 25 | console.log('else') 26 | previous = 0 27 | }, delay) 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * 搜索关键词高亮显示 34 | * @param String str 要替换的关键词 35 | * @param String value 搜索框里面的内容 36 | */ 37 | const keyword = (str, value) => { 38 | const replaceReg = new RegExp(value, 'g') 39 | const replaceString = `${value}` 40 | str = str.replace(replaceReg, replaceString) 41 | return str 42 | } 43 | 44 | const formatTime = date => { 45 | const year = date.getFullYear() 46 | const month = date.getMonth() + 1 47 | const day = date.getDate() 48 | const hour = date.getHours() 49 | const minute = date.getMinutes() 50 | const second = date.getSeconds() 51 | 52 | return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':') 53 | } 54 | const formatDate = date => { 55 | const year = date.getFullYear() 56 | const month = date.getMonth() + 1 57 | const day = date.getDate() 58 | const a = [year, month, day].map(formatNumber) 59 | return a[0] + '年' + a[1] + '月' + a[2] + '日' 60 | } 61 | 62 | const formatNumber = n => { 63 | n = n.toString() 64 | return n[1] ? n : '0' + n 65 | } 66 | export { 67 | throttle, // 函数节流 68 | keyword, // 搜索关键词高亮显示 69 | formatTime, 70 | formatDate, 71 | formatNumber 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /* 合法手机号*/ 2 | export function checkPhone(value) { 3 | const reg = /^1[34578]\d{9}$/ 4 | return reg.test(value) 5 | } 6 | 7 | export function param2Obj(url) { 8 | const search = url.split('?')[1] 9 | if (!search) { 10 | return {} 11 | } 12 | return JSON.parse( 13 | '{"' + 14 | decodeURIComponent(search) 15 | .replace(/"/g, '\\"') 16 | .replace(/&/g, '","') 17 | .replace(/=/g, '":"') + 18 | '"}' 19 | ) 20 | } 21 | 22 | export function trim(str) { 23 | return str.replace(/(^\s*)|(\s*$)/g, '') 24 | } 25 | 26 | export function check_user_name(str) { 27 | let str2 = '该用户名合法' 28 | if (str === '') { 29 | str2 = '用户名为空' 30 | return str2 31 | } else if (str.length < 6 || str.length > 20) { 32 | str2 = '用户名必须为6 ~ 20位' 33 | return str2 34 | } else if (check_other_char(str)) { 35 | str2 = '不能含有特殊字符' 36 | return str2 37 | } 38 | return str2 39 | } 40 | // 验证用户名是否含有特殊字符 41 | function check_other_char(str) { 42 | const arr = ['&', '\\', '/', '*', '>', '<', '@', '!'] 43 | for (let i = 0; i < arr.length; i++) { 44 | for (let j = 0; j < str.length; j++) { 45 | if (arr[i] === str.charAt(j)) { 46 | return true 47 | } 48 | } 49 | } 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/views/AccountSafe/accountSafe.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 40 | 41 | 61 | -------------------------------------------------------------------------------- /frontend/src/views/ApplyDetail/applyDetail.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 89 | 90 | 172 | -------------------------------------------------------------------------------- /frontend/src/views/Contacts/contacts.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 91 | 92 | 165 | -------------------------------------------------------------------------------- /frontend/src/views/CreateGroup/createGroup.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 118 | 119 | 141 | -------------------------------------------------------------------------------- /frontend/src/views/Edit/edit.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 199 | 200 | 252 | -------------------------------------------------------------------------------- /frontend/src/views/EditAge/editAge.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 59 | 60 | 78 | -------------------------------------------------------------------------------- /frontend/src/views/EditEmail/editEmail.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 65 | 66 | 84 | -------------------------------------------------------------------------------- /frontend/src/views/EditGender/editGender.vue: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 58 | 59 | 83 | -------------------------------------------------------------------------------- /frontend/src/views/EditName/editName.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 53 | 54 | 72 | -------------------------------------------------------------------------------- /frontend/src/views/EditPassword/editPassword.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 86 | 87 | 123 | -------------------------------------------------------------------------------- /frontend/src/views/EditPhone/editPhone.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 134 | 135 | 225 | -------------------------------------------------------------------------------- /frontend/src/views/EditRemark/editRemark.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 76 | 77 | 94 | -------------------------------------------------------------------------------- /frontend/src/views/FriendDetail/friendDetail.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 68 | 69 | 116 | -------------------------------------------------------------------------------- /frontend/src/views/FriendsInfo/friendsInfo.vue: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 123 | 124 | 180 | 181 | -------------------------------------------------------------------------------- /frontend/src/views/GroupDetail/groupDetail.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 89 | 90 | 144 | -------------------------------------------------------------------------------- /frontend/src/views/Manager/manager.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 99 | 100 | 183 | -------------------------------------------------------------------------------- /frontend/src/views/MesPanel/Emoji/emoji.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /frontend/src/views/MesPanel/MesList/mesList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 33 | 34 | 69 | 70 | -------------------------------------------------------------------------------- /frontend/src/views/MesPanel/MessageItem/messageItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 77 | 78 | 253 | -------------------------------------------------------------------------------- /frontend/src/views/MesPanel/mesPanel.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 196 | 244 | -------------------------------------------------------------------------------- /frontend/src/views/SearchFriend/searchFriend.vue: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 90 | 91 | 143 | -------------------------------------------------------------------------------- /frontend/src/views/SearchGroup/searchGroup.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 72 | 73 | 125 | -------------------------------------------------------------------------------- /frontend/src/views/SearchLocal/searchLocal.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 58 | 59 | 92 | -------------------------------------------------------------------------------- /frontend/src/views/SendFriendValidate/sendFriendValidate.vue: -------------------------------------------------------------------------------- 1 |