系统信息
36 |├── .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 | [](https://camo.githubusercontent.com/3342c0573ddaa3bf466e29415ee025c30adab987f6f3d589ef436324e8390803/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630312e706e67)[](https://camo.githubusercontent.com/77a92d724b7fc41d7cb0efb0c0bab5be537e19f63b3c93296dbde2731eb8549e/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343631352e706e67) 38 | 39 | [](https://camo.githubusercontent.com/a34e640fc4264d324f30394e059717601fa2ff1b22f1f781d7f42731a3b4b586/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630332e706e67)[](https://camo.githubusercontent.com/e8a2a0b91eb47b9e87ba75091ed7dbd00efb9ea670abffe1b41389054bd1aae6/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343630322e706e67) 40 | 41 | [](https://camo.githubusercontent.com/4195eb1410798b0612daec35de73befc3015d4529a097844562dcef71f58fd67/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f7878796472722f6d795f7069632f696d672f32303231303530353133343631372e706e67)[](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 |
<%= error.stack %>4 | -------------------------------------------------------------------------------- /backend/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
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 |{{ InfoList.nickname }}
12 |添加您为好友
14 |申请加入{{ InfoList.groupName }}群
15 |{{ friendsInfo.nickname }}
8 |9 | {{ friendsInfo.province }}{{ friendsInfo.city }}{{ friendsInfo.gender }}{{ friendsInfo.age }} 10 |
11 |9 | {{ friendsInfo.city }} {{ friendsInfo.gender }} {{ friendsInfo.age }}岁 10 |
11 |12 | {{ friendsInfo.mobilePhone }} 13 |
14 |账号: {{ userInfo.userName }}
10 |手机号: {{ userInfo.mobilePhone }}
11 |邮箱: {{ userInfo.email }}
12 |备忘录
22 | 23 |账户安全
29 | 30 |系统信息
36 |{{ friendsInfo.userName }}
18 |{{ friendsInfo.nickname }}
20 |{{ groupInfo.userName }}
18 |{{ groupInfo.nickname }}
20 |{{ friendsInfo.nickname }}
23 |25 | {{ friendsInfo.province }} {{ friendsInfo.city }}{{ friendsInfo.gender }}{{ friendsInfo.age }} 26 |
27 |{{ groupInfo.title }}
23 |25 | {{ groupInfo.desc }} 26 |
27 |{{ item.nickname }}
14 | {{ item.time }} 15 |{{ item.mes }}
17 |