├── vue-chat-client ├── static │ ├── .gitkeep │ ├── notice.mp3 │ └── wechat.ico ├── config │ ├── prod.env.js │ ├── dev.env.js │ └── index.js ├── src │ ├── assets │ │ ├── bg.jpg │ │ ├── logo.png │ │ ├── font-icon │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.css │ │ │ ├── demo_fontclass.html │ │ │ ├── demo_unicode.html │ │ │ ├── iconfont.svg │ │ │ ├── demo_symbol.html │ │ │ ├── demo.css │ │ │ └── iconfont.js │ │ └── styles │ │ │ └── css_initialize.css │ ├── store │ │ ├── face.js │ │ ├── index.js │ │ ├── state.js │ │ ├── mutations.js │ │ └── actions.js │ ├── common │ │ ├── events.js │ │ ├── install.js │ │ ├── socket.js │ │ └── ws.js │ ├── main.js │ ├── App.vue │ └── components │ │ ├── avatar-reset.vue │ │ ├── signinAndSignup.vue │ │ ├── sider-left.vue │ │ └── sider-right.vue ├── .editorconfig ├── .postcssrc.js ├── build │ ├── dev-client.js │ ├── vue-loader.conf.js │ ├── build.js │ ├── webpack.dev.conf.js │ ├── check-versions.js │ ├── webpack.base.conf.js │ ├── utils.js │ ├── dev-server.js │ └── webpack.prod.conf.js ├── .babelrc ├── README.md ├── index.html └── package.json ├── install.bat ├── start.bat ├── stop.bat ├── restart.bat ├── vue-chat-document ├── API.doc ├── 架构图.ddd ├── wechat.ddd └── function.doc ├── vue-chat-server ├── static │ └── paopao │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.png │ │ ├── 04.png │ │ ├── 05.png │ │ ├── 06.png │ │ ├── 07.png │ │ ├── 08.png │ │ ├── 09.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 15.png │ │ ├── 16.png │ │ ├── 17.png │ │ ├── 18.png │ │ ├── 19.png │ │ ├── 20.png │ │ ├── 21.png │ │ ├── 22.png │ │ └── face.png ├── models │ ├── index.js │ ├── message.js │ ├── group.js │ ├── user.js │ └── avatar.js ├── README.md ├── events │ ├── addFriend.js │ ├── removeFriend.js │ ├── getFriends.js │ ├── getUser.js │ ├── getGroups.js │ ├── searchGroups.js │ ├── updateUser.js │ ├── searchUsers.js │ ├── signIn.js │ ├── createGroup.js │ ├── signUp.js │ ├── getGroupMember.js │ ├── addGroup.js │ ├── removeGroup.js │ ├── uploadImg.js │ ├── modifyAvatar.js │ ├── pushMsg.js │ └── pullMsg.js ├── eventsMap │ └── index.js ├── package.json └── app.js ├── .gitignore └── README.md /vue-chat-client/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | cd ./vue-chat-server && npm i && cd ../vue-chat-client && npm i -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | cd ./vue-chat-server && npm start && cd ../vue-chat-client && npm start -------------------------------------------------------------------------------- /stop.bat: -------------------------------------------------------------------------------- 1 | cd ./vue-chat-server && npm run stop && cd ../vue-chat-client && npm run stop -------------------------------------------------------------------------------- /restart.bat: -------------------------------------------------------------------------------- 1 | cd ./vue-chat-server && npm run restart && cd ../vue-chat-client && npm run restart -------------------------------------------------------------------------------- /vue-chat-client/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /vue-chat-document/API.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-document/API.doc -------------------------------------------------------------------------------- /vue-chat-document/架构图.ddd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-document/架构图.ddd -------------------------------------------------------------------------------- /vue-chat-document/wechat.ddd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-document/wechat.ddd -------------------------------------------------------------------------------- /vue-chat-document/function.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-document/function.doc -------------------------------------------------------------------------------- /vue-chat-client/src/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-client/src/assets/bg.jpg -------------------------------------------------------------------------------- /vue-chat-client/static/notice.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-client/static/notice.mp3 -------------------------------------------------------------------------------- /vue-chat-client/static/wechat.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-client/static/wechat.ico -------------------------------------------------------------------------------- /vue-chat-client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-client/src/assets/logo.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/01.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/02.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/03.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/04.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/05.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/06.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/07.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/08.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/09.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/10.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/11.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/12.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/13.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/14.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/15.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/16.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/17.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/18.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/19.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/20.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/21.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/22.png -------------------------------------------------------------------------------- /vue-chat-server/static/paopao/face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-server/static/paopao/face.png -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-client/src/assets/font-icon/iconfont.eot -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-client/src/assets/font-icon/iconfont.ttf -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SWPUliusong/vue-chat/HEAD/vue-chat-client/src/assets/font-icon/iconfont.woff -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # 图片 9 | temp/ 10 | 11 | # 日志文件 12 | error*.log 13 | access*.log -------------------------------------------------------------------------------- /vue-chat-client/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /vue-chat-client/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /vue-chat-client/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vue-chat-server/models/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose") 2 | mongoose.Promise = require("bluebird") 3 | mongoose.connect('mongodb://localhost:27017/wechat') 4 | 5 | module.exports = { 6 | User: require("./user"), 7 | Group: require("./group"), 8 | Message: require("./message") 9 | } -------------------------------------------------------------------------------- /vue-chat-client/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /vue-chat-client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vue-chat-client/src/store/face.js: -------------------------------------------------------------------------------- 1 | const prefix = '/img/paopao/' 2 | 3 | let arr = [], temp 4 | 5 | for (let i = 1; i < 23; i++) { 6 | temp = i 7 | if (i < 10) { 8 | temp = '0' + temp 9 | } 10 | 11 | temp = `${prefix}${temp}.png` 12 | 13 | arr.push(temp) 14 | } 15 | 16 | export default arr -------------------------------------------------------------------------------- /vue-chat-client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import state from './state' 5 | import mutations from './mutations' 6 | import actions from './actions' 7 | 8 | Vue.use(Vuex) 9 | 10 | export default new Vuex.Store({ 11 | state, 12 | mutations, 13 | actions 14 | }) -------------------------------------------------------------------------------- /vue-chat-server/README.md: -------------------------------------------------------------------------------- 1 | ## 目录结构 2 | ``` 3 | |-events socket通讯的各个事件 4 | |-eventsMap 自动映射包,将events下的事件映射到socket上 5 | |-models 数据库文档结构 6 | ㄴapp.js 启动文件 7 | ``` 8 | ## 安装依赖 9 | > npm i 10 | ## 启动服务器 11 | > npm test 12 | ## 后台启动 13 | > npm start 14 | ## 重启后台服务 15 | > npm run restart 16 | ## 关闭后台服务 17 | > npm run stop -------------------------------------------------------------------------------- /vue-chat-client/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /vue-chat-client/src/store/state.js: -------------------------------------------------------------------------------- 1 | import faces from './face' 2 | 3 | export default { 4 | user: '', // 当前用户 5 | list: '', // 聊天菜单列表 6 | activeList: 'friends', // 当前聊天菜单类型 7 | currentOne: '', // 当前聊天对象 8 | result: '', // 搜索结果 9 | isChange: true, // 决定消息的更新方式是增加还是重新赋值 10 | messages: [], // 消息记录 11 | count: '', // 消息个数 12 | faces, // 表情 13 | facesShow: false // 是否显示泡泡表情 14 | } -------------------------------------------------------------------------------- /vue-chat-server/models/message.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose") 2 | const Schema = mongoose.Schema 3 | 4 | let ObjectId = Schema.Types.ObjectId 5 | 6 | let Message = new Schema({ 7 | toGroup: ObjectId, 8 | from: ObjectId, 9 | toUser: ObjectId, 10 | content: String, 11 | createAt: { 12 | type: Date, 13 | default: Date.now 14 | }, 15 | }) 16 | 17 | module.exports = mongoose.model('message', Message) -------------------------------------------------------------------------------- /vue-chat-server/events/addFriend.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | 3 | module.exports = (socket, io) => ({ from, friendId }) => { 4 | 5 | Promise.all([ 6 | User.update({_id: from}, {$addToSet: {friends: friendId}}), 7 | User.update({_id: friendId}, {$addToSet: {friends: from}}) 8 | ]) 9 | .then(() => { 10 | io.sockets.emit('addFriend', { friendId }) 11 | }) 12 | .catch(err => { 13 | socket.emit('addFriend', err) 14 | }) 15 | 16 | } -------------------------------------------------------------------------------- /vue-chat-server/events/removeFriend.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | 3 | module.exports = (socket, io) => ({ from, friendId }) => { 4 | 5 | Promise.all([ 6 | User.update({_id: from}, {$pull: {friends: friendId}}), 7 | User.update({_id: friendId}, {$pull: {friends: from}}) 8 | ]) 9 | .then(() => { 10 | io.sockets.emit('removeFriend', { from, friendId }) 11 | }) 12 | .catch(err => { 13 | socket.emit('removeFriend', err) 14 | }) 15 | 16 | } -------------------------------------------------------------------------------- /vue-chat-server/events/getFriends.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | 3 | module.exports = socket => ({ from }) => { 4 | User 5 | .findOne({_id: from}) 6 | .select('friends') 7 | .exec() 8 | .then(doc => { 9 | if (doc.friends.length < 1) return [] 10 | return User.find({_id: {$in: doc.friends}}).select('_id name avatar').exec() 11 | }) 12 | .then(list => { 13 | socket.emit('getFriends', list) 14 | }) 15 | } -------------------------------------------------------------------------------- /vue-chat-server/events/getUser.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | 3 | module.exports = socket => ({ from }) => { 4 | User 5 | .findById(from) 6 | .exec() 7 | .then(user => { 8 | if(!user) { 9 | return Promise.reject({status: 404, msg: '该用户不存在'}) 10 | } else { 11 | socket.emit('getUser', user.toObject()) 12 | } 13 | }) 14 | .catch(err => { 15 | socket.emit('getUser', err) 16 | }) 17 | } -------------------------------------------------------------------------------- /vue-chat-server/events/getGroups.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | const Group = require("../models").Group 3 | 4 | module.exports = socket => ({ from }) => { 5 | User 6 | .findOne({_id: from}) 7 | .select('groups') 8 | .exec() 9 | .then(doc => { 10 | if (doc.groups.length < 1) return [] 11 | return Group.find({_id: {$in: doc.groups}}).select('_id name avatar').exec() 12 | }) 13 | .then(list => { 14 | socket.emit('getGroups', list) 15 | }) 16 | } -------------------------------------------------------------------------------- /vue-chat-client/README.md: -------------------------------------------------------------------------------- 1 | # vue-chat 2 | 3 | > A Vue.js chat tool like wechatVue 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /vue-chat-server/models/group.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose") 2 | const Schema = mongoose.Schema 3 | 4 | let ObjectId = Schema.Types.ObjectId 5 | 6 | let avatar = require("./avatar") 7 | let Group = new Schema({ 8 | name: String, 9 | avatar: { 10 | type: String, 11 | default() { 12 | let num = Math.floor(Math.random() * 10) 13 | return avatar.group[num] 14 | } 15 | }, 16 | member: [ObjectId], 17 | createAt: { 18 | type: Date, 19 | default: Date.now 20 | } 21 | }) 22 | 23 | module.exports = mongoose.model('group', Group) -------------------------------------------------------------------------------- /vue-chat-server/events/searchGroups.js: -------------------------------------------------------------------------------- 1 | const Group = require("../models").Group 2 | 3 | module.exports = socket => ({ keyword }) => { 4 | Group 5 | .find({ 6 | name: {$regex: new RegExp(keyword)} 7 | }) 8 | .limit(5) 9 | .select('_id name avatar') 10 | .exec() 11 | .then(groups => { 12 | if (!groups) return [] 13 | return groups 14 | }) 15 | .then(list => { 16 | socket.emit('searchGroups', list) 17 | }) 18 | .catch(err => { 19 | socket.emit('searchGroups', err) 20 | }) 21 | } -------------------------------------------------------------------------------- /vue-chat-server/events/updateUser.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | 3 | module.exports = socket => ({ from, data }) => { 4 | if (!from) { 5 | socket.emit('updateUser', { status: 400, msg: '参数from缺失' }) 6 | } 7 | 8 | User 9 | .findByIdAndUpdate(from, _.pick(data, ['name', 'password'])) 10 | .exec() 11 | .then(user => { 12 | let body = _.assign(user.toObject(), _.pick(data, ['name', 'password'])) 13 | 14 | socket.emit('updateUser', body) 15 | }) 16 | .catch(err => { 17 | socket.emit('updateUser', err) 18 | }) 19 | } -------------------------------------------------------------------------------- /vue-chat-server/eventsMap/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | 4 | module.exports = function(dir) { 5 | let map = {} 6 | let eventNames = fs.readdirSync(dir).map(item => item.replace(/\.js$/, '')) 7 | 8 | eventNames.forEach(event => { 9 | let filePath = path.resolve(dir, event) 10 | let fn = require(filePath) 11 | if (typeof fn !== 'function') fn = () => () =>{} 12 | map[event] = fn 13 | }) 14 | return (socket, io) => { 15 | _.forEach(map, (fn, eventName) => { 16 | socket.on(eventName, fn(socket, io)) 17 | }) 18 | } 19 | } -------------------------------------------------------------------------------- /vue-chat-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-chat 6 | 7 | 8 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /vue-chat-server/events/searchUsers.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | 3 | module.exports = socket => ({ from, keyword }) => { 4 | User 5 | .find({ 6 | name: {$regex: new RegExp(keyword)} 7 | }) 8 | .limit(5) 9 | .select('_id name avatar') 10 | .exec() 11 | .then(users => { 12 | if (!users) return [] 13 | return users.filter(item => item._id.toString() !== from) 14 | }) 15 | .then(list => { 16 | socket.emit('searchUsers', list) 17 | }) 18 | .catch(err => { 19 | socket.emit('searchUsers', err) 20 | }) 21 | } -------------------------------------------------------------------------------- /vue-chat-server/events/signIn.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | 3 | module.exports = socket => ({ data }) => { 4 | User 5 | .findOne({email: data.email}) 6 | .exec() 7 | .then(user => { 8 | if(!user) { 9 | return Promise.reject({status: 401, msg: '该用户不存在'}) 10 | } else if (user.password !== data.password) { 11 | return Promise.reject({status: 403, msg: '密码错误'}) 12 | } else { 13 | socket.emit('signIn', user.toObject()) 14 | } 15 | }) 16 | .catch(err => { 17 | socket.emit('signIn', err) 18 | }) 19 | } -------------------------------------------------------------------------------- /vue-chat-server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose") 2 | const Schema = mongoose.Schema 3 | 4 | let ObjectId = Schema.Types.ObjectId 5 | 6 | let avatar = require("./avatar") 7 | let User = new Schema({ 8 | name: String, 9 | email: String, 10 | password: String, 11 | friends: [ObjectId], 12 | groups: [ObjectId], 13 | createAt: { 14 | type: Date, 15 | default: Date.now 16 | }, 17 | avatar: { 18 | type: String, 19 | default() { 20 | let num = Math.floor(Math.random() * 10) 21 | return avatar.user[num] 22 | } 23 | } 24 | }) 25 | 26 | module.exports = mongoose.model('user', User) -------------------------------------------------------------------------------- /vue-chat-server/events/createGroup.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | const Group = require("../models").Group 3 | 4 | module.exports = socket => ({ from, data }) => { 5 | Group 6 | .create({ 7 | name: data.name, 8 | member: [from] 9 | }) 10 | .then(group => { 11 | return User 12 | .update({ _id: from }, { $addToSet: { groups: group._id } }) 13 | .then(() => group._id) 14 | }) 15 | .then(groupId => { 16 | socket.emit('createGroup', { groupId }) 17 | }) 18 | .catch(err => { 19 | socket.emit('createGroup', err) 20 | }) 21 | } -------------------------------------------------------------------------------- /vue-chat-client/src/common/events.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import { socket } from './socket' 3 | 4 | socket.on('addFriend', ({ friendId }) => { 5 | let { user, activeList } = store.state 6 | if (user && user._id == friendId && activeList === 'friends') { 7 | store.dispatch('getList') 8 | } 9 | return 10 | }) 11 | 12 | socket.on('removeFriend', ({ from, friendId }) => { 13 | let { user, activeList, currentOne } = store.state 14 | if (user && user._id == friendId && activeList === 'friends') { 15 | store.dispatch('getList') 16 | } 17 | if (from === currentOne._id) { 18 | store.commit('removeCurrentOne') 19 | } 20 | return 21 | }) -------------------------------------------------------------------------------- /vue-chat-server/events/signUp.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | 3 | module.exports = socket => ({ data }) => { 4 | User 5 | .findOne({email: data.email}) 6 | .exec() 7 | .then(user => { 8 | if(user) { 9 | return Promise.reject({status: 403, msg: '邮箱已被注册'}) 10 | } 11 | return false 12 | }) 13 | .then(() => { 14 | return User.create(_.pick(data, ['name', 'password', 'email'])) 15 | }) 16 | .then(user => { 17 | socket.emit('signUp', user.toObject()) 18 | }) 19 | .catch(err => { 20 | socket.emit('signUp', err) 21 | }) 22 | } -------------------------------------------------------------------------------- /vue-chat-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-chat-server", 3 | "version": "1.0.0", 4 | "description": "一个vuejs+socket.io的微信仿照版 ", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "node app", 8 | "start": "pm2 start app.js --name vue-chat-server -e ../error.log -o ../access.log", 9 | "restart": "pm2 restart vue-chat-server -e ../error.log -o ../access.log", 10 | "stop": "pm2 delete vue-chat-server" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bluebird": "^3.5.0", 16 | "lodash": "^4.17.4", 17 | "mongoose": "^4.9.6", 18 | "socket.io": "^1.7.3" 19 | }, 20 | "devDependencies": { 21 | "pm2": "^2.4.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vue-chat-client/src/common/install.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | function fileValid(file, size = 100) { 4 | let types = ['image/jpeg', 'image/gif', 'image/png'] 5 | let res = { 6 | flag: true 7 | } 8 | if (types.indexOf(file.type) < 0) { 9 | res.flag = false 10 | res.msg = '文件类型只能是gif,png,jpg' 11 | } else if (file.size > size * 1024) { 12 | res.flag = false 13 | res.msg = `图片大小不得超过${size}KB` 14 | } 15 | 16 | return res 17 | } 18 | 19 | fileValid.types = { 20 | 'image/jpeg': 'jpg', 21 | 'image/gif': 'gif', 22 | 'image/png': 'png' 23 | } 24 | 25 | Object.defineProperty(Vue.prototype, 'fileValid', { 26 | value: fileValid 27 | }) -------------------------------------------------------------------------------- /vue-chat-server/events/getGroupMember.js: -------------------------------------------------------------------------------- 1 | const db = require("../models") 2 | 3 | module.exports = socket => ({ groupId }) => { 4 | 5 | if (!groupId) { 6 | socket.emit('getGroupMember', { status: 400, msg: 'groupId缺失' }) 7 | return 8 | } 9 | 10 | db.Group 11 | .findById(groupId) 12 | .select('member') 13 | .exec() 14 | .then(doc => { 15 | if (!doc) return [] 16 | return db.User 17 | .find({ _id: { $in: doc.member } }) 18 | .select('_id name avatar') 19 | .exec() 20 | }) 21 | .then(data => { 22 | socket.emit('getGroupMember', { groupId, data }) 23 | }) 24 | } -------------------------------------------------------------------------------- /vue-chat-server/events/addGroup.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | const Group = require("../models").Group 3 | 4 | module.exports = socket => ({ from, groupId }) => { 5 | 6 | Promise.all([ 7 | User.findByIdAndUpdate(from, {$addToSet: {groups: groupId}}), 8 | Group.update({_id: groupId}, {$addToSet: {member: from}}) 9 | ]) 10 | .then((cols) => { 11 | socket.broadcast.emit(groupId, { 12 | groupId, 13 | data: { 14 | type: 'system', 15 | name: '系统消息', 16 | content: `${cols[0].name} 加入了聊天群组` 17 | } 18 | }) 19 | socket.emit('addGroup', {}) 20 | }) 21 | .catch(err => { 22 | socket.emit('addGroup', err) 23 | }) 24 | 25 | } -------------------------------------------------------------------------------- /vue-chat-server/events/removeGroup.js: -------------------------------------------------------------------------------- 1 | const User = require("../models").User 2 | const Group = require("../models").Group 3 | 4 | module.exports = socket => ({ from, groupId }) => { 5 | 6 | Promise.all([ 7 | User.findByIdAndUpdate(from, {$pull: {groups: groupId}}).exec(), 8 | Group.update({_id: groupId}, {$pull: {member: from}}) 9 | ]) 10 | .then((cols) => { 11 | socket.broadcast.emit(groupId, { 12 | groupId, 13 | data: { 14 | type: 'system', 15 | name: '系统消息', 16 | content: `${cols[0].name} 已经退出聊天群组了` 17 | } 18 | }) 19 | socket.emit('removeGroup', {}) 20 | }) 21 | .catch(err => { 22 | socket.emit('removeGroup', err) 23 | }) 24 | 25 | } -------------------------------------------------------------------------------- /vue-chat-client/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | 4 | // lodash 5 | import _ from 'lodash' 6 | window._ = _ 7 | 8 | 9 | import Vue from 'vue' 10 | import ElementUI from 'element-ui' 11 | import './common/events' 12 | import './common/install' 13 | 14 | import './assets/styles/css_initialize.css' 15 | import 'element-ui/lib/theme-default/index.css' 16 | import './assets/font-icon/iconfont.css' 17 | 18 | import store from './store' 19 | import App from './App' 20 | 21 | Vue.use(ElementUI) 22 | 23 | 24 | Vue.config.productionTip = false 25 | 26 | /* eslint-disable no-new */ 27 | new Vue({ 28 | el: '#app', 29 | store, 30 | template: '', 31 | components: { App } 32 | }) 33 | -------------------------------------------------------------------------------- /vue-chat-server/events/uploadImg.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | const User = require("../models").User 4 | const crypto = require("crypto") 5 | 6 | function createNameByBuffer(buffer) { 7 | return crypto.createHash('md5').update(buffer).digest('hex') 8 | } 9 | 10 | module.exports = socket => ({ from, data }) => { 11 | if (!from) { 12 | socket.emit('uploadImg', { status: 400, msg: '参数from缺失' }) 13 | } 14 | 15 | let filename = `temp/${createNameByBuffer(data.file)}.${data.type}` 16 | 17 | let url = path.resolve(process.cwd(), `./static/${filename}`) 18 | fs.writeFile(url, data.file, err => { 19 | if (err) { 20 | return socket.emit('uploadImg', err) 21 | } 22 | 23 | return socket.emit('uploadImg', { img: `/img/${filename}` }) 24 | }) 25 | } -------------------------------------------------------------------------------- /vue-chat-server/app.js: -------------------------------------------------------------------------------- 1 | global._ = require("lodash") 2 | global.Promise = require("bluebird") 3 | 4 | const http = require('http') 5 | const fs = require('fs') 6 | 7 | let types = { 8 | 'jpg': 'image/jpeg', 9 | 'gif': 'image/gif', 10 | 'png': 'image/png' 11 | } 12 | 13 | let server = http.createServer((req, res) => { 14 | let url = req.url 15 | if (/^\/img\//.test(url)) { 16 | let ext = url.substring(url.lastIndexOf('.')) 17 | res.writeHead(200, {'content-type': types[ext]}) 18 | fs.createReadStream(`./static/${url.replace('/img/', '')}`).pipe(res) 19 | } 20 | }) 21 | 22 | let io = require('socket.io')(server); 23 | let eventsMap = require("./eventsMap")('./events') 24 | 25 | server.listen(3002, () => { 26 | console.log(`server listening on ${server.address().port}`) 27 | }); 28 | 29 | io.on('connection', function (socket) { 30 | eventsMap(socket, io) 31 | }); 32 | -------------------------------------------------------------------------------- /vue-chat-server/models/avatar.js: -------------------------------------------------------------------------------- 1 | let user = [ 2 | 'FrsP_fKd26dFBbbIQTtJ1IpEXbct', 3 | 'FqzW9bqSJLGjx4yCwBdR6YallVis', 4 | 'FqGy-gAxwYr9lKwQxXjB7QFhehgs', 5 | 'FpiKWZxVi2ZvPkg9G1YkBrvyNXUd', 6 | 'FlmQyroSCee5kwHIZ-k7rIC9UYsK', 7 | 'FkqOXqYnO_6ZKGinwcEYK94vSNpP', 8 | 'Fkfc7nDn1I2r1cdBFzvzVTKcOzeC', 9 | 'FhKC2btY7UkL6jXLc5ygiSEqR3Kx', 10 | 'FhAa3WusC6N8zciLg-6zJkgwfirr', 11 | 'FgLNtLQsduVqAxSzNzOkDjnyV0Dz' 12 | ] 13 | 14 | let group = [ 15 | 'Fs78VD3Owcpdp2j0XzaHXHX3Fu_3', 16 | 'FpzhUhaVyhs6nK-VvululSweD1yG', 17 | 'Fom5nX-UEo1VJdSK0aQkMwky2NtJ', 18 | 'FoHvYP7zIUIxtwJiT7fATMst3kv4', 19 | 'FnKKljiSMjzkKARlGnB1tJb8HqTF', 20 | '26123517m5bg.jpg', 21 | 'FjoRiCS7U1Sc5Rkv5JXWc51XUC4C', 22 | 'FkvzGzqARaO4TMn7FCwsgn8sRvhA', 23 | 'FlPm5nSeDgilXmxOc7lYaPMKAeTt', 24 | 'FlaMfV3woBqzISvdMzt9FIpEj0qY' 25 | ] 26 | 27 | 28 | module.exports = { 29 | user: _.map(user, item => 'http://ol5140dkq.bkt.clouddn.com/' + item), 30 | group: _.map(group, item => 'http://ol5140dkq.bkt.clouddn.com/' + item) 31 | } -------------------------------------------------------------------------------- /vue-chat-client/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | 28 | 48 | -------------------------------------------------------------------------------- /vue-chat-client/build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | console.log(chalk.cyan(' Build complete.\n')) 30 | console.log(chalk.yellow( 31 | ' Tip: built files are meant to be served over an HTTP server.\n' + 32 | ' Opening index.html over file:// won\'t work.\n' 33 | )) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-chat 2 | vue + socket.io + mongodb 实现的一个仿网页版的微信 3 | 4 | # 拥有功能 5 | - 用户注册 6 | - 用户登录 7 | - 用户退出 8 | - 获取用户 9 | - 修改用户 10 | - 创建群组 11 | - 获取全部好友 12 | - 获取全部群组 13 | - 添加好友 14 | - 添加聊天群组 15 | - 搜索用户 16 | - 搜索群组 17 | - 获取群组成员 18 | - 删除好友 19 | - 删除群组 20 | - 推送消息 21 | - 拉取消息 22 | 23 | # 目录内容 24 | ``` 25 | |-vue-chat-client vue搭建的前端页面,监听8080端口 26 | |-vue-chat-doument 开发文档 27 | ㄴvue-chat-server socket.io+monogdb搭建的服务端,监听3002端口 28 | ``` 29 | 30 | # 预览 31 | ## 1.登录注册页 32 | 33 | ![](http://p0mwchnvj.bkt.clouddn.com/%E7%99%BB%E5%BD%95%E9%A1%B5.png) 34 | 35 | ## 2.好友列表 36 | 37 | ![](http://p0mwchnvj.bkt.clouddn.com/%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8.png) 38 | 39 | ![](http://p0mwchnvj.bkt.clouddn.com/%E6%90%9C%E7%B4%A2%E5%A5%BD%E5%8F%8B.png) 40 | 41 | ## 3.群组列表 42 | 43 | ![](http://p0mwchnvj.bkt.clouddn.com/%E7%BE%A4%E7%BB%84%E5%88%97%E8%A1%A8.png) 44 | 45 | ![](http://p0mwchnvj.bkt.clouddn.com/%E6%90%9C%E7%B4%A2%E7%BE%A4%E7%BB%84.png) 46 | 47 | ## 4.好友聊天 48 | 49 | ![](http://p0mwchnvj.bkt.clouddn.com/%E5%A5%BD%E5%8F%8B%E8%81%8A%E5%A4%A9.png) 50 | 51 | ## 5.群内会话 52 | 53 | ![](http://p0mwchnvj.bkt.clouddn.com/%E7%BE%A4%E5%86%85%E4%BC%9A%E8%AF%9D.png) -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1493460678859'); /* IE9*/ 4 | src: url('iconfont.eot?t=1493460678859#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('iconfont.woff?t=1493460678859') format('woff'), /* chrome, firefox */ 6 | url('iconfont.ttf?t=1493460678859') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg?t=1493460678859#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family:"iconfont" !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-weibiaoti114:before { content: "\e688"; } 19 | 20 | .icon-icon-yxj-user:before { content: "\e631"; } 21 | 22 | .icon-qunzu:before { content: "\e6d7"; } 23 | 24 | .icon-kehuqunzu:before { content: "\e673"; } 25 | 26 | .icon-bianji:before { content: "\f0304"; } 27 | 28 | .icon-jia:before { content: "\f0307"; } 29 | 30 | .icon-guanbi:before { content: "\f0308"; } 31 | 32 | .icon-tuxiang:before { content: "\f030b"; } 33 | 34 | .icon-sousuo:before { content: "\f030d"; } 35 | 36 | -------------------------------------------------------------------------------- /vue-chat-server/events/modifyAvatar.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | const User = require("../models").User 4 | const crypto = require("crypto") 5 | 6 | function createNameByBuffer(buffer) { 7 | return crypto.createHash('md5').update(buffer).digest('hex') 8 | } 9 | 10 | // 判断temp文件夹是否存在,不存在则创建 11 | if (!fs.existsSync('./static/temp')) { 12 | fs.mkdirSync('./static/temp') 13 | } 14 | 15 | module.exports = socket => ({ from, data }) => { 16 | if (!from) { 17 | socket.emit('modifyAvatar', { status: 400, msg: '参数from缺失' }) 18 | } 19 | 20 | let filename = `temp/${createNameByBuffer(data.file)}.${data.type}` 21 | 22 | let url = path.resolve(process.cwd(), `./static/${filename}`) 23 | fs.writeFile(url, data.file, err => { 24 | if (err) console.error(err) 25 | }) 26 | 27 | User 28 | .findByIdAndUpdate(from, { 29 | avatar: `/img/${filename}` 30 | }) 31 | .exec() 32 | .then(user => { 33 | let body = _.assign(user.toObject(), { 34 | avatar: `/img/${filename}` 35 | }) 36 | 37 | socket.emit('modifyAvatar', body) 38 | }) 39 | .catch(err => { 40 | socket.emit('modifyAvatar', err) 41 | }) 42 | } -------------------------------------------------------------------------------- /vue-chat-client/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /vue-chat-client/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | ] 16 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm 22 | }) 23 | } 24 | 25 | module.exports = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /vue-chat-server/events/pushMsg.js: -------------------------------------------------------------------------------- 1 | const db = require("../models") 2 | 3 | module.exports = (socket, io) => ({ from, friendId, groupId, data }) => { 4 | 5 | if (!data.content || !from) { 6 | socket.emit('pushMsg', { status: 400, msg: '消息字段不全' }) 7 | return 8 | } 9 | 10 | if (groupId) { 11 | createMsg({ 12 | toGroup: groupId, 13 | from: from, 14 | content: data.content 15 | }).then(message => { 16 | io.sockets.emit(groupId, { 17 | from, 18 | data: message 19 | }) 20 | }) 21 | } else if (friendId) { 22 | createMsg({ 23 | toUser: friendId, 24 | from: from, 25 | content: data.content 26 | }).then(message => { 27 | io.sockets.emit(friendId, { 28 | from, 29 | data: message 30 | }) 31 | }) 32 | } else { 33 | socket.emit('pushMsg', { status: 400, msg: '消息字段不全' }) 34 | } 35 | } 36 | 37 | 38 | function createMsg(data) { 39 | return db.Message 40 | .create(data) 41 | .then(msg => { 42 | msg = msg.toObject(); 43 | return db.User 44 | .findById(msg.from) 45 | .select('_id name avatar') 46 | .exec() 47 | .then(user => { 48 | msg.from = user 49 | return msg 50 | }) 51 | }) 52 | } -------------------------------------------------------------------------------- /vue-chat-client/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: './src/main.js' 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' 18 | ? config.build.assetsPublicPath 19 | : config.dev.assetsPublicPath 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.vue', '.json'], 23 | alias: { 24 | 'vue$': 'vue/dist/vue.esm.js', 25 | '@': resolve('src') 26 | } 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.vue$/, 32 | loader: 'vue-loader', 33 | options: vueLoaderConfig 34 | }, 35 | { 36 | test: /\.js$/, 37 | loader: 'babel-loader', 38 | include: [resolve('src'), resolve('test')] 39 | }, 40 | { 41 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 42 | loader: 'url-loader', 43 | options: { 44 | limit: 10000, 45 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 46 | } 47 | }, 48 | { 49 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 50 | loader: 'url-loader', 51 | options: { 52 | limit: 10000, 53 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 54 | } 55 | } 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /vue-chat-client/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | setUser(state, { user }) { 3 | state.user = user 4 | }, 5 | removeUser(state) { 6 | _.assign(state, { 7 | user: '', 8 | list: '', 9 | activeList: 'friends', 10 | currentOne: '', 11 | isChange: true, 12 | messages: [], 13 | count: '', 14 | }) 15 | }, 16 | setActiveList(state, { type }) { 17 | state.isChange = true 18 | state.activeList = type 19 | }, 20 | setList(state, list) { 21 | state.list = list 22 | }, 23 | setResult(state, { result }) { 24 | state.result = result 25 | }, 26 | removeResult(state) { 27 | state.result = '' 28 | }, 29 | changeCurrentOne(state, { item }) { 30 | state.isChange = true 31 | if (state.count[item._id]) { 32 | state.count = _.assign(state.count, { 33 | [item._id]: 0 34 | }) 35 | } 36 | state.currentOne = item 37 | }, 38 | removeCurrentOne(state) { 39 | state.currentOne = '' 40 | state.messages = [] 41 | }, 42 | notChange(state) { 43 | state.isChange = false 44 | }, 45 | pullMsg(state, messages) { 46 | if (state.isChange) { 47 | state.messages = messages 48 | } else { 49 | state.messages = messages.concat(state.messages) 50 | } 51 | }, 52 | pushMsg(state, msg) { 53 | state.messages.push(msg) 54 | }, 55 | toggleFaces(state) { 56 | state.facesShow = !state.facesShow 57 | }, 58 | hiddenFaces(state) { 59 | state.facesShow = false 60 | } 61 | } -------------------------------------------------------------------------------- /vue-chat-client/config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: { 31 | '/socket.io': { 32 | target: 'http://localhost:3002', 33 | ws: true, 34 | changeOrigin: true, 35 | }, 36 | '/img': { 37 | target: 'http://localhost:3002', 38 | changeOrigin: true, 39 | } 40 | }, 41 | // CSS Sourcemaps off by default because relative paths are "buggy" 42 | // with this option, according to the CSS-Loader README 43 | // (https://github.com/webpack/css-loader#sourcemaps) 44 | // In our experience, they generally work as expected, 45 | // just be aware of this issue when enabling this option. 46 | cssSourceMap: false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /vue-chat-server/events/pullMsg.js: -------------------------------------------------------------------------------- 1 | const db = require("../models") 2 | 3 | module.exports = (socket, io) => ({ from, friendId, groupId, data }) => { 4 | let page = data.page || 1 5 | 6 | if (groupId) { 7 | findMsg({ toGroup: groupId }, page).then(coll => { 8 | socket.emit('pullMsg', { 9 | groupId, 10 | data: coll 11 | }) 12 | }).catch(err => { 13 | socket.emit('pullMsg', err) 14 | }) 15 | 16 | 17 | } else if (friendId && from) { 18 | let ids = [from, friendId] 19 | 20 | findMsg({ from: { $in: ids }, toUser: { $in: ids } }, page).then(coll => { 21 | socket.emit('pullMsg', { 22 | friendId, 23 | data: coll 24 | }) 25 | }).catch(err => { 26 | socket.emit('pullMsg', err) 27 | }) 28 | 29 | } else { 30 | socket.emit('pullMsg', { status: 400, msg: '消息字段缺失' }) 31 | } 32 | } 33 | 34 | 35 | function findMsg(opt, page) { 36 | return db.Message 37 | .find(opt) 38 | .sort({ 'createAt': -1 }) 39 | .skip((page - 1) * 10) 40 | .limit(10) 41 | .exec() 42 | .then(coll => { 43 | if (!coll) return [] 44 | 45 | let promise_arr = [] 46 | coll = coll.map(doc => doc.toObject()) 47 | 48 | coll.forEach(doc => { 49 | let promise = db.User 50 | .findById(doc.from) 51 | .select('_id name avatar') 52 | .exec() 53 | .then(user => { 54 | doc.from = user.toObject() 55 | }) 56 | 57 | promise_arr.push(promise) 58 | }) 59 | 60 | return Promise.all(promise_arr).then(() => coll.reverse()) 61 | }) 62 | } -------------------------------------------------------------------------------- /vue-chat-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-chat", 3 | "version": "1.0.0", 4 | "description": "A Vue.js chat tool like wechat", 5 | "author": "", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "pm2 start build/dev-server.js --name vue-chat-client -e ../error.log -o ../access.log", 10 | "stop": "pm2 delete vue-chat-client", 11 | "restart": "pm2 restart vue-chat-client -e ../error.log -o ../access.log", 12 | "build": "node build/build.js" 13 | }, 14 | "dependencies": { 15 | "element-ui": "^1.2.9", 16 | "vue": "^2.2.6", 17 | "vuex": "^2.3.1" 18 | }, 19 | "devDependencies": { 20 | "autoprefixer": "^6.7.2", 21 | "babel-core": "^6.22.1", 22 | "babel-loader": "^6.2.10", 23 | "babel-plugin-transform-runtime": "^6.22.0", 24 | "babel-preset-env": "^1.3.2", 25 | "babel-preset-stage-2": "^6.22.0", 26 | "babel-register": "^6.22.0", 27 | "chalk": "^1.1.3", 28 | "connect-history-api-fallback": "^1.3.0", 29 | "copy-webpack-plugin": "^4.0.1", 30 | "css-loader": "^0.28.0", 31 | "eventsource-polyfill": "^0.9.6", 32 | "express": "^4.14.1", 33 | "extract-text-webpack-plugin": "^2.0.0", 34 | "file-loader": "^0.11.1", 35 | "friendly-errors-webpack-plugin": "^1.1.3", 36 | "html-webpack-plugin": "^2.28.0", 37 | "http-proxy-middleware": "^0.17.3", 38 | "less": "^2.7.2", 39 | "less-loader": "^4.0.3", 40 | "opn": "^4.0.2", 41 | "optimize-css-assets-webpack-plugin": "^1.3.0", 42 | "ora": "^1.2.0", 43 | "pm2": "^2.4.6", 44 | "rimraf": "^2.6.0", 45 | "semver": "^5.3.0", 46 | "shelljs": "^0.7.6", 47 | "url-loader": "^0.5.8", 48 | "vue-loader": "^11.3.4", 49 | "vue-style-loader": "^2.0.5", 50 | "vue-template-compiler": "^2.2.6", 51 | "webpack": "^2.3.3", 52 | "webpack-bundle-analyzer": "^2.2.1", 53 | "webpack-dev-middleware": "^1.10.0", 54 | "webpack-hot-middleware": "^2.18.0", 55 | "webpack-merge": "^4.1.0" 56 | }, 57 | "engines": { 58 | "node": ">= 4.0.0", 59 | "npm": ">= 3.0.0" 60 | }, 61 | "browserslist": [ 62 | "> 1%", 63 | "last 2 versions", 64 | "not ie <= 8" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /vue-chat-client/src/components/avatar-reset.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 54 | 55 | -------------------------------------------------------------------------------- /vue-chat-client/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } 72 | -------------------------------------------------------------------------------- /vue-chat-client/build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = require('./webpack.dev.conf') 14 | 15 | // default port where dev server listens for incoming traffic 16 | var port = process.env.PORT || config.dev.port 17 | // automatically open browser, if not set will be false 18 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 19 | // Define HTTP proxies to your custom API backend 20 | // https://github.com/chimurai/http-proxy-middleware 21 | var proxyTable = config.dev.proxyTable 22 | 23 | var app = express() 24 | var compiler = webpack(webpackConfig) 25 | 26 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 27 | publicPath: webpackConfig.output.publicPath, 28 | quiet: true 29 | }) 30 | 31 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 32 | log: () => {} 33 | }) 34 | // force page reload when html-webpack-plugin template changes 35 | compiler.plugin('compilation', function (compilation) { 36 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 37 | hotMiddleware.publish({ action: 'reload' }) 38 | cb() 39 | }) 40 | }) 41 | 42 | // proxy api requests 43 | Object.keys(proxyTable).forEach(function (context) { 44 | var options = proxyTable[context] 45 | if (typeof options === 'string') { 46 | options = { target: options } 47 | } 48 | app.use(proxyMiddleware(options.filter || context, options)) 49 | }) 50 | 51 | // handle fallback for HTML5 history API 52 | app.use(require('connect-history-api-fallback')()) 53 | 54 | // serve webpack bundle output 55 | app.use(devMiddleware) 56 | 57 | // enable hot-reload and state-preserving 58 | // compilation error display 59 | app.use(hotMiddleware) 60 | 61 | // serve pure static assets 62 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 63 | app.use(staticPath, express.static('./static')) 64 | 65 | var uri = 'http://localhost:' + port 66 | 67 | var _resolve 68 | var readyPromise = new Promise(resolve => { 69 | _resolve = resolve 70 | }) 71 | 72 | console.log('> Starting dev server...') 73 | devMiddleware.waitUntilValid(() => { 74 | console.log('> Listening at ' + uri + '\n') 75 | // when env is testing, don't need open it 76 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 77 | opn(uri) 78 | } 79 | _resolve() 80 | }) 81 | 82 | var server = app.listen(port) 83 | 84 | module.exports = { 85 | ready: readyPromise, 86 | close: () => { 87 | server.close() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /vue-chat-client/src/common/socket.js: -------------------------------------------------------------------------------- 1 | // 连接webSocket 2 | const socket = io.connect("/") 3 | 4 | // 登陆后为所有群组添加监听器 5 | function addGroupsListener(user, state) { 6 | _.forEach(user.groups, item => { 7 | socket.on(item, ({ from, data }) => { 8 | if (data.type) return 9 | if (from !== user._id) { 10 | let notice = document.getElementById('notice') 11 | notice.play() 12 | } 13 | if (item === state.currentOne._id) { 14 | state.messages.push(data) 15 | } else { 16 | if (_.isString(state.count)) { 17 | state.count = { [item]: 1 } 18 | } else { 19 | let count = state.count[item] 20 | state.count = _.assign(state.count, { 21 | [item]: count ? ++count : 1 22 | }) 23 | } 24 | } 25 | }) 26 | }) 27 | } 28 | 29 | // 登陆后移除前一个用户的消息监听器,并为新用户添加消息监听器 30 | function addUserListener(user, state) { 31 | let old = state.user 32 | socket.removeAllListeners(old._id) 33 | socket.on(user._id, ({ from, data }) => { 34 | let notice = document.getElementById('notice') 35 | notice.play() 36 | if (from === state.currentOne._id) { 37 | state.messages.push(data) 38 | } else { 39 | if (_.isString(state.count)) { 40 | state.count = { [from]: 1 } 41 | } else { 42 | let count = state.count[from] 43 | state.count = _.assign(state.count, { 44 | [from]: count ? ++count : 1 45 | }) 46 | } 47 | } 48 | }) 49 | } 50 | 51 | // 移除用户自身和所有群组的监听器 52 | function removeGroupAndUserListeners(user) { 53 | socket.removeAllListeners(user._id) 54 | _.forEach(user.groups, item => { 55 | socket.removeAllListeners(item) 56 | }) 57 | } 58 | 59 | // 为单个群组添加监听 60 | function addSingleGroupListener(groupId, state) { 61 | socket.on(groupId, ({ from, data }) => { 62 | if (data.type) return 63 | if (from !== state.user._id) { 64 | let notice = document.getElementById('notice') 65 | notice.play() 66 | } 67 | if (groupId === state.currentOne._id) { 68 | state.messages.push(data) 69 | } else { 70 | if (_.isString(state.count)) { 71 | state.count = { [groupId]: 1 } 72 | } else { 73 | let count = state.count[groupId] 74 | state.count = _.assign(state.count, { 75 | [groupId]: count ? ++count : 1 76 | }) 77 | } 78 | } 79 | }) 80 | } 81 | 82 | // 删除单个群组的监听 83 | function removeSingleGroupListener(groupId) { 84 | socket.removeAllListeners(groupId) 85 | } 86 | 87 | export { 88 | socket, 89 | addGroupsListener, 90 | addUserListener, 91 | removeGroupAndUserListeners, 92 | addSingleGroupListener, 93 | removeSingleGroupListener 94 | } -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/demo_fontclass.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 10 | 11 |
12 |

IconFont 图标

13 | 70 | 71 |

font-class引用

72 |
73 | 74 |

font-class是unicode使用方式的一种变种,主要是解决unicode书写不直观,语意不明确的问题。

75 |

与unicode使用方式相比,具有如下特点:

76 | 82 |

使用步骤如下:

83 |

第一步:引入项目下面生成的fontclass代码:

84 | 85 | 86 |
<link rel="stylesheet" type="text/css" href="./iconfont.css">
87 |

第二步:挑选相应图标并获取类名,应用于页面:

88 |
<i class="iconfont icon-xxx"></i>
89 |
90 |

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

91 |
92 |
93 | 94 | 95 | -------------------------------------------------------------------------------- /vue-chat-client/src/assets/styles/css_initialize.css: -------------------------------------------------------------------------------- 1 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, dl, dt, dd, ul, ol, li, pre, form, fieldset, legend, button, input, textarea, th, td { margin:0; padding:0; } 2 | h1, h2, h3, h4, h5, h6{ font-size:100%; } 3 | address, cite, dfn, em, var { font-style:normal; } 4 | code, kbd, pre, samp { font-family:couriernew, courier, monospace; } 5 | small{ font-size:12px; } 6 | ul{ list-style:none; } 7 | a, *[href] { text-decoration:none; cursor: pointer; color: #333;} 8 | a:hover { text-decoration:underline; } 9 | sup { vertical-align:text-top; } 10 | sub{ vertical-align:text-bottom; } 11 | legend { color:#000; } 12 | fieldset, img { border:0; } 13 | button, input, select, textarea { font-size:100%; } 14 | table { border-collapse:collapse; border-spacing:0; } 15 | 16 | html, body { 17 | height: 100%; 18 | overflow: hidden; 19 | box-sizing: border-box; 20 | } 21 | 22 | body { 23 | background: url("../bg.jpg") no-repeat; 24 | background-size: cover; 25 | } 26 | 27 | .bg-green { 28 | background-color: #3caf36; 29 | } 30 | .bg-greenlight { 31 | background-color: #4bc0a5; 32 | } 33 | .bg-white { 34 | background-color: #ffffff; 35 | } 36 | .bg-black { 37 | background-color: #3f495b; 38 | } 39 | .bg-orange { 40 | background-color: #ffb300; 41 | } 42 | .bg-orangelight { 43 | background-color: #ffbe29; 44 | } 45 | .bg-red { 46 | background-color: #ea3434; 47 | } 48 | .bg-pale { 49 | background-color: #f6f9f9; 50 | } 51 | .bg-gray { 52 | background-color: #cacaca; 53 | } 54 | .text-green { 55 | color: #3caf36; 56 | } 57 | .text-init { 58 | color: #666666; 59 | } 60 | .text-red { 61 | color: #ea3434; 62 | } 63 | .text-blue { 64 | color: #0087ca; 65 | } 66 | .text-orange { 67 | color: #ffb300; 68 | } 69 | .text-yellow { 70 | color: #fdfd90; 71 | } 72 | .text-white { 73 | color: #fdfdfd; 74 | } 75 | .text-gray { 76 | color: #cacaca; 77 | } 78 | .text-center { 79 | text-align: center; 80 | } 81 | .text-left { 82 | text-align: left; 83 | } 84 | .text-right { 85 | text-align: right; 86 | } 87 | 88 | .font-bold { 89 | font-weight: bold; 90 | } 91 | .font-normal { 92 | font-weight: normal; 93 | } 94 | .font-bigger { 95 | font-size: 1.2em; 96 | } 97 | .font-big { 98 | font-size: 24px; 99 | } 100 | .cursor-pointer { 101 | cursor: pointer; 102 | padding: 15px; 103 | } 104 | .padding-24 { 105 | padding: 24px; 106 | } 107 | .padding-15 { 108 | padding: 15px; 109 | } 110 | .padding-10 { 111 | padding: 10px; 112 | } 113 | .padding-top-10 { 114 | padding-top: 10px; 115 | } 116 | .padding-top-bottom-8 { 117 | padding-top: 8px; 118 | padding-bottom: 8px; 119 | } 120 | .padding-5 { 121 | padding: 5px; 122 | } 123 | .padding-right-15 { 124 | padding-right: 15px; 125 | } 126 | .border-bottom { 127 | border-bottom: 1px solid #4bc0a5; 128 | } 129 | .link:hover { 130 | cursor: pointer; 131 | } 132 | .hover-black:hover { 133 | background-color: #3a3f45; 134 | } 135 | 136 | .scrollbar { 137 | overflow-y: auto 138 | } 139 | 140 | .scrollbar::-webkit-scrollbar { 141 | width: 8px; 142 | } 143 | .scrollbar::-webkit-scrollbar-track { 144 | background-color: #fdfdfd; 145 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 146 | -webkit-border-radius: 4px; 147 | border-radius: 4px; 148 | } /* 滚动条的滑轨背景颜色 */ 149 | 150 | .scrollbar::-webkit-scrollbar-thumb { 151 | -webkit-border-radius: 4px; 152 | border-radius: 4px; 153 | background-color: rgba(25, 25, 25, 0.7); 154 | } /* 滑块颜色 */ 155 | 156 | .scrollbar::-webkit-scrollbar-button { 157 | display: none; 158 | background-color: #7c2929; 159 | } /* 滑轨两头的监听按钮颜色 */ 160 | 161 | 162 | .pull-left { 163 | float: left; 164 | } 165 | .pull-right { 166 | float: right; 167 | } 168 | .clearfix:after { 169 | content: ''; 170 | height: 0; 171 | display: block; 172 | clear: both; 173 | } -------------------------------------------------------------------------------- /vue-chat-client/src/components/signinAndSignup.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 116 | 117 | -------------------------------------------------------------------------------- /vue-chat-client/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = config.build.env 13 | 14 | var webpackConfig = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ 17 | sourceMap: config.build.productionSourceMap, 18 | extract: true 19 | }) 20 | }, 21 | devtool: config.build.productionSourceMap ? '#source-map' : false, 22 | output: { 23 | path: config.build.assetsRoot, 24 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 25 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 26 | }, 27 | plugins: [ 28 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 29 | new webpack.DefinePlugin({ 30 | 'process.env': env 31 | }), 32 | new webpack.optimize.UglifyJsPlugin({ 33 | compress: { 34 | warnings: false 35 | }, 36 | sourceMap: true 37 | }), 38 | // extract css into its own file 39 | new ExtractTextPlugin({ 40 | filename: utils.assetsPath('css/[name].[contenthash].css') 41 | }), 42 | // Compress extracted CSS. We are using this plugin so that possible 43 | // duplicated CSS from different components can be deduped. 44 | new OptimizeCSSPlugin({ 45 | cssProcessorOptions: { 46 | safe: true 47 | } 48 | }), 49 | // generate dist index.html with correct asset hash for caching. 50 | // you can customize output by editing /index.html 51 | // see https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: config.build.index, 54 | template: 'index.html', 55 | inject: true, 56 | minify: { 57 | removeComments: true, 58 | collapseWhitespace: true, 59 | removeAttributeQuotes: true 60 | // more options: 61 | // https://github.com/kangax/html-minifier#options-quick-reference 62 | }, 63 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 64 | chunksSortMode: 'dependency' 65 | }), 66 | // split vendor js into its own file 67 | new webpack.optimize.CommonsChunkPlugin({ 68 | name: 'vendor', 69 | minChunks: function (module, count) { 70 | // any required modules inside node_modules are extracted to vendor 71 | return ( 72 | module.resource && 73 | /\.js$/.test(module.resource) && 74 | module.resource.indexOf( 75 | path.join(__dirname, '../node_modules') 76 | ) === 0 77 | ) 78 | } 79 | }), 80 | // extract webpack runtime and module manifest to its own file in order to 81 | // prevent vendor hash from being updated whenever app bundle is updated 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'manifest', 84 | chunks: ['vendor'] 85 | }), 86 | // copy custom static assets 87 | new CopyWebpackPlugin([ 88 | { 89 | from: path.resolve(__dirname, '../static'), 90 | to: config.build.assetsSubDirectory, 91 | ignore: ['.*'] 92 | } 93 | ]) 94 | ] 95 | }) 96 | 97 | if (config.build.productionGzip) { 98 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 99 | 100 | webpackConfig.plugins.push( 101 | new CompressionWebpackPlugin({ 102 | asset: '[path].gz[query]', 103 | algorithm: 'gzip', 104 | test: new RegExp( 105 | '\\.(' + 106 | config.build.productionGzipExtensions.join('|') + 107 | ')$' 108 | ), 109 | threshold: 10240, 110 | minRatio: 0.8 111 | }) 112 | ) 113 | } 114 | 115 | if (config.build.bundleAnalyzerReport) { 116 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 117 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 118 | } 119 | 120 | module.exports = webpackConfig 121 | -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/demo_unicode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 29 | 30 | 31 |
32 |

IconFont 图标

33 | 90 |

unicode引用

91 |
92 | 93 |

unicode是字体在网页端最原始的应用方式,特点是:

94 | 99 |
100 |

注意:新版iconfont支持多色图标,这些多色图标在unicode模式下将不能使用,如果有需求建议使用symbol的引用方式

101 |
102 |

unicode使用步骤如下:

103 |

第一步:拷贝项目下面生成的font-face

104 |
@font-face {
105 |   font-family: 'iconfont';
106 |   src: url('iconfont.eot');
107 |   src: url('iconfont.eot?#iefix') format('embedded-opentype'),
108 |   url('iconfont.woff') format('woff'),
109 |   url('iconfont.ttf') format('truetype'),
110 |   url('iconfont.svg#iconfont') format('svg');
111 | }
112 | 
113 |

第二步:定义使用iconfont的样式

114 |
.iconfont{
115 |   font-family:"iconfont" !important;
116 |   font-size:16px;font-style:normal;
117 |   -webkit-font-smoothing: antialiased;
118 |   -webkit-text-stroke-width: 0.2px;
119 |   -moz-osx-font-smoothing: grayscale;
120 | }
121 | 
122 |

第三步:挑选相应图标并获取字体编码,应用于页面

123 |
<i class="iconfont">&#x33;</i>
124 | 125 |
126 |

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

127 |
128 |
129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Sat Apr 29 18:11:18 2017 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 36 | 39 | 42 | 47 | 51 | 53 | 55 | 57 | 60 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /vue-chat-client/src/common/ws.js: -------------------------------------------------------------------------------- 1 | import { socket } from './socket' 2 | 3 | export default { 4 | getUser(from) { 5 | return new Promise((res, rej) => { 6 | socket.emit('getUser', { from }) 7 | socket.once('getUser', data => { 8 | if (data.msg || data.message) return rej(data) 9 | return res(data) 10 | }) 11 | }) 12 | }, 13 | signIn(payload) { 14 | return new Promise((res, rej) => { 15 | socket.emit('signIn', payload) 16 | socket.once('signIn', data => { 17 | if (data.msg || data.message) return rej(data) 18 | return res(data) 19 | }) 20 | }) 21 | }, 22 | signUp(payload) { 23 | return new Promise((res, rej) => { 24 | socket.emit('signUp', payload) 25 | socket.once('signUp', data => { 26 | if (data.msg || data.message) return rej(data) 27 | return res(data) 28 | }) 29 | }) 30 | }, 31 | getFriends(from) { 32 | return new Promise((res, rej) => { 33 | socket.emit('getFriends', { from }) 34 | socket.once('getFriends', data => { 35 | if (data.msg || data.message) return rej(data) 36 | return res(data) 37 | }) 38 | }) 39 | }, 40 | getGroups(from) { 41 | return new Promise((res, rej) => { 42 | socket.emit('getGroups', { from }) 43 | socket.once('getGroups', data => { 44 | if (data.msg || data.message) return rej(data) 45 | return res(data) 46 | }) 47 | }) 48 | }, 49 | searchUsers({ keyword, from }) { 50 | return new Promise((res, rej) => { 51 | socket.emit('searchUsers', { keyword, from }) 52 | socket.once('searchUsers', data => { 53 | if (data.msg || data.message) return rej(data) 54 | return res(data) 55 | }) 56 | }) 57 | }, 58 | searchGroups(keyword) { 59 | return new Promise((res, rej) => { 60 | socket.emit('searchGroups', { keyword }) 61 | socket.once('searchGroups', data => { 62 | if (data.msg || data.message) return rej(data) 63 | return res(data) 64 | }) 65 | }) 66 | }, 67 | createGroup(params) { 68 | return new Promise((res, rej) => { 69 | socket.emit('createGroup', params) 70 | socket.once('createGroup', data => { 71 | if (data.msg || data.message) return rej(data) 72 | return res(data) 73 | }) 74 | }) 75 | }, 76 | addGroup(params) { 77 | return new Promise((res, rej) => { 78 | socket.emit('addGroup', params) 79 | socket.once('addGroup', data => { 80 | if (data.msg || data.message) return rej(data) 81 | return res(data) 82 | }) 83 | }) 84 | }, 85 | addFriend(params) { 86 | return new Promise((res, rej) => { 87 | socket.emit('addFriend', params) 88 | socket.once('addFriend', data => { 89 | if (data.msg || data.message) return rej(data) 90 | return res(data) 91 | }) 92 | }) 93 | }, 94 | pullMsg(params) { 95 | return new Promise((res, rej) => { 96 | socket.emit('pullMsg', params) 97 | socket.once('pullMsg', data => { 98 | if (data.msg || data.message) return rej(data) 99 | return res(data) 100 | }) 101 | }) 102 | }, 103 | pushMsg(params) { 104 | socket.emit('pushMsg', params) 105 | }, 106 | removeFriend(params) { 107 | return new Promise((res, rej) => { 108 | socket.emit('removeFriend', params) 109 | socket.once('removeFriend', data => { 110 | if (data.msg || data.message) return rej(data) 111 | return res(data) 112 | }) 113 | }) 114 | }, 115 | removeGroup(params) { 116 | return new Promise((res, rej) => { 117 | socket.emit('removeGroup', params) 118 | socket.once('removeGroup', data => { 119 | if (data.msg || data.message) return rej(data) 120 | return res(data) 121 | }) 122 | }) 123 | }, 124 | getGroupMember(params) { 125 | return new Promise((res, rej) => { 126 | socket.emit('getGroupMember', params) 127 | socket.once('getGroupMember', data => { 128 | if (data.msg || data.message) return rej(data) 129 | return res(data) 130 | }) 131 | }) 132 | }, 133 | updateUser(params) { 134 | return new Promise((res, rej) => { 135 | socket.emit('updateUser', params) 136 | socket.once('updateUser', data => { 137 | if (data.msg || data.message) return rej(data) 138 | return res(data) 139 | }) 140 | }) 141 | }, 142 | modifyAvatar(params) { 143 | return new Promise((res, rej) => { 144 | socket.emit('modifyAvatar', params) 145 | socket.once('modifyAvatar', data => { 146 | if (data.msg || data.message) return rej(data) 147 | return res(data) 148 | }) 149 | }) 150 | }, 151 | uploadImg(params) { 152 | return new Promise((res, rej) => { 153 | socket.emit('uploadImg', params) 154 | socket.once('uploadImg', data => { 155 | if (data.msg || data.message) return rej(data) 156 | return res(data) 157 | }) 158 | }) 159 | } 160 | 161 | } -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/demo_symbol.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 10 | 24 | 25 | 26 |
27 |

IconFont 图标

28 | 103 | 104 | 105 |

symbol引用

106 |
107 | 108 |

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 109 | 这种用法其实是做了一个svg的集合,与另外两种相比具有如下特点:

110 | 116 |

使用步骤如下:

117 |

第一步:引入项目下面生成的symbol代码:

118 |
<script src="./iconfont.js"></script>
119 |

第二步:加入通用css代码(引入一次就行):

120 |
<style type="text/css">
121 | .icon {
122 |    width: 1em; height: 1em;
123 |    vertical-align: -0.15em;
124 |    fill: currentColor;
125 |    overflow: hidden;
126 | }
127 | </style>
128 |

第三步:挑选相应图标并获取类名,应用于页面:

129 |
<svg class="icon" aria-hidden="true">
130 |   <use xlink:href="#icon-xxx"></use>
131 | </svg>
132 |         
133 |
134 | 135 | 136 | -------------------------------------------------------------------------------- /vue-chat-client/src/store/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | addGroupsListener, 3 | addUserListener, 4 | removeGroupAndUserListeners, 5 | addSingleGroupListener, 6 | removeSingleGroupListener 7 | } from '@/common/socket' 8 | 9 | import ws from '@/common/ws' 10 | 11 | 12 | 13 | export default { 14 | getUser({ commit, state }) { 15 | return ws.getUser(state.user._id).then(user => { 16 | commit('setUser', { user }) 17 | }) 18 | }, 19 | signIn({ state, commit }, payload) { 20 | return ws.signIn(payload).then(user => { 21 | // 初始化用户监听器 22 | addUserListener(user, state) 23 | // 初始化群组监听器 24 | addGroupsListener(user, state) 25 | commit('setUser', { user }) 26 | }) 27 | }, 28 | signUp({ commit, state }, payload) { 29 | return ws.signUp(payload).then(user => { 30 | addUserListener(user, state) 31 | addGroupsListener(user, state) 32 | commit('setUser', { user }) 33 | }) 34 | }, 35 | signOut({ state, commit, dispatch }) { 36 | ws.getUser(state.user._id).then(removeGroupAndUserListeners) 37 | commit('removeUser') 38 | }, 39 | setActiveList({ commit }, type) { 40 | commit('setActiveList', { type }) 41 | commit('removeCurrentOne') 42 | }, 43 | getList({ commit, state }) { 44 | if (state.activeList === 'friends') { 45 | return ws.getFriends(state.user._id) 46 | .then(list => commit('setList', list)) 47 | } else { 48 | return ws.getGroups(state.user._id) 49 | .then(list => commit('setList', list)) 50 | } 51 | }, 52 | createGroup({ state, dispatch }, keyword) { 53 | return ws.createGroup({ 54 | from: state.user._id, 55 | data: { name: keyword } 56 | }).then(({ groupId }) => { 57 | addSingleGroupListener(groupId, state) 58 | return dispatch('getList') 59 | }) 60 | }, 61 | searchUsers({ state, commit }, keyword) { 62 | return ws.searchUsers({ 63 | keyword, 64 | from: state.user._id 65 | }).then(result => { 66 | commit('setResult', { result }) 67 | }) 68 | }, 69 | searchGroups({ commit }, keyword) { 70 | return ws.searchGroups(keyword).then(result => { 71 | commit('setResult', { result }) 72 | }) 73 | }, 74 | addGroup({ state, dispatch }, groupId) { 75 | addSingleGroupListener(groupId, state) 76 | return ws.addGroup({ 77 | from: state.user._id, 78 | groupId 79 | }).then(() => { 80 | return dispatch('getList') 81 | }) 82 | }, 83 | addFriend({ state, dispatch }, friendId) { 84 | return ws.addFriend({ 85 | from: state.user._id, 86 | friendId 87 | }).then(() => { 88 | return dispatch('getList') 89 | }) 90 | }, 91 | removeResult({ commit }) { 92 | commit('removeResult') 93 | }, 94 | changeCurrentOne({ commit }, item) { 95 | commit('changeCurrentOne', { item }) 96 | }, 97 | pullMsg({ commit, state }, page) { 98 | let params = { 99 | from: state.user._id, 100 | data: { page } 101 | } 102 | if (state.activeList === 'friends') { 103 | params.friendId = state.currentOne._id 104 | } else { 105 | params.groupId = state.currentOne._id 106 | } 107 | 108 | return ws.pullMsg(params).then(({ data }) => { 109 | commit('pullMsg', data) 110 | if (data.length < 1) return false 111 | }) 112 | }, 113 | pushMsg({ commit, state }, content) { 114 | let params = { 115 | from: state.user._id, 116 | data: { content } 117 | } 118 | 119 | if (state.activeList === 'friends') { 120 | params.friendId = state.currentOne._id 121 | } else { 122 | params.groupId = state.currentOne._id 123 | } 124 | 125 | ws.pushMsg(params) 126 | if (state.activeList === 'friends') { 127 | commit('pushMsg', { 128 | from: _.pick(state.user, ['_id', 'name', 'avatar']), 129 | content 130 | }) 131 | } 132 | }, 133 | removeFriend({ commit, state, dispatch }) { 134 | let id = state.currentOne._id 135 | let params = { 136 | from: state.user._id, 137 | friendId: id 138 | } 139 | 140 | if (state.count[id]) { 141 | state.count = _.assign(state.count, { 142 | [id]: 0 143 | }) 144 | } 145 | 146 | return ws.removeFriend(params).then(() => { 147 | commit('removeCurrentOne') 148 | dispatch('getList') 149 | }) 150 | }, 151 | removeGroup({ commit, state, dispatch }) { 152 | let id = state.currentOne._id 153 | let params = { 154 | from: state.user._id, 155 | groupId: id 156 | } 157 | 158 | // 同时移除群组的消息监听 159 | removeSingleGroupListener(id) 160 | 161 | if (state.count[id]) { 162 | state.count = _.assign(state.count, { 163 | [id]: 0 164 | }) 165 | } 166 | 167 | return ws.removeGroup(params).then(() => { 168 | commit('removeCurrentOne') 169 | dispatch('getList') 170 | }) 171 | }, 172 | getGroupMember({ state }) { 173 | let groupId = state.currentOne._id 174 | return ws.getGroupMember({ groupId }).then(({ data }) => data) 175 | }, 176 | updateUser({ state, commit }, data) { 177 | let params = { 178 | from: state.user._id, 179 | data 180 | } 181 | 182 | return ws.updateUser(params).then(user => { 183 | commit('setUser', { user }) 184 | return user 185 | }) 186 | }, 187 | modifyAvatar({ state, commit }, data) { 188 | let params = { 189 | from: state.user._id, 190 | data 191 | } 192 | 193 | return ws.modifyAvatar(params).then(user => commit('setUser', { user })) 194 | }, 195 | uploadImg({ state, commit }, data) { 196 | let params = { 197 | from: state.user._id, 198 | data 199 | } 200 | 201 | return ws.uploadImg(params) 202 | } 203 | } -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/demo.css: -------------------------------------------------------------------------------- 1 | *{margin: 0;padding: 0;list-style: none;} 2 | /* 3 | KISSY CSS Reset 4 | 理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。 5 | 2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。 6 | 3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。 7 | 特色:1. 适应中文;2. 基于最新主流浏览器。 8 | 维护:玉伯, 正淳 9 | */ 10 | 11 | /** 清除内外边距 **/ 12 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */ 13 | dl, dt, dd, ul, ol, li, /* list elements 列表元素 */ 14 | pre, /* text formatting elements 文本格式元素 */ 15 | form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */ 16 | th, td /* table elements 表格元素 */ { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | /** 设置默认字体 **/ 22 | body, 23 | button, input, select, textarea /* for ie */ { 24 | font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif; 25 | } 26 | h1, h2, h3, h4, h5, h6 { font-size: 100%; } 27 | address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */ 28 | code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */ 29 | small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */ 30 | 31 | /** 重置列表元素 **/ 32 | ul, ol { list-style: none; } 33 | 34 | /** 重置文本格式元素 **/ 35 | a { text-decoration: none; } 36 | a:hover { text-decoration: underline; } 37 | 38 | 39 | /** 重置表单元素 **/ 40 | legend { color: #000; } /* for ie6 */ 41 | fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */ 42 | button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */ 43 | /* 注:optgroup 无法扶正 */ 44 | 45 | /** 重置表格元素 **/ 46 | table { border-collapse: collapse; border-spacing: 0; } 47 | 48 | /* 清除浮动 */ 49 | .ks-clear:after, .clear:after { 50 | content: '\20'; 51 | display: block; 52 | height: 0; 53 | clear: both; 54 | } 55 | .ks-clear, .clear { 56 | *zoom: 1; 57 | } 58 | 59 | .main { 60 | padding: 30px 100px; 61 | width: 960px; 62 | margin: 0 auto; 63 | } 64 | .main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;} 65 | 66 | .helps{margin-top:40px;} 67 | .helps pre{ 68 | padding:20px; 69 | margin:10px 0; 70 | border:solid 1px #e7e1cd; 71 | background-color: #fffdef; 72 | overflow: auto; 73 | } 74 | 75 | .icon_lists{ 76 | width: 100% !important; 77 | 78 | } 79 | 80 | .icon_lists li{ 81 | float:left; 82 | width: 100px; 83 | height:180px; 84 | text-align: center; 85 | list-style: none !important; 86 | } 87 | .icon_lists .icon{ 88 | font-size: 42px; 89 | line-height: 100px; 90 | margin: 10px 0; 91 | color:#333; 92 | -webkit-transition: font-size 0.25s ease-out 0s; 93 | -moz-transition: font-size 0.25s ease-out 0s; 94 | transition: font-size 0.25s ease-out 0s; 95 | 96 | } 97 | .icon_lists .icon:hover{ 98 | font-size: 100px; 99 | } 100 | 101 | 102 | 103 | .markdown { 104 | color: #666; 105 | font-size: 14px; 106 | line-height: 1.8; 107 | } 108 | 109 | .highlight { 110 | line-height: 1.5; 111 | } 112 | 113 | .markdown img { 114 | vertical-align: middle; 115 | max-width: 100%; 116 | } 117 | 118 | .markdown h1 { 119 | color: #404040; 120 | font-weight: 500; 121 | line-height: 40px; 122 | margin-bottom: 24px; 123 | } 124 | 125 | .markdown h2, 126 | .markdown h3, 127 | .markdown h4, 128 | .markdown h5, 129 | .markdown h6 { 130 | color: #404040; 131 | margin: 1.6em 0 0.6em 0; 132 | font-weight: 500; 133 | clear: both; 134 | } 135 | 136 | .markdown h1 { 137 | font-size: 28px; 138 | } 139 | 140 | .markdown h2 { 141 | font-size: 22px; 142 | } 143 | 144 | .markdown h3 { 145 | font-size: 16px; 146 | } 147 | 148 | .markdown h4 { 149 | font-size: 14px; 150 | } 151 | 152 | .markdown h5 { 153 | font-size: 12px; 154 | } 155 | 156 | .markdown h6 { 157 | font-size: 12px; 158 | } 159 | 160 | .markdown hr { 161 | height: 1px; 162 | border: 0; 163 | background: #e9e9e9; 164 | margin: 16px 0; 165 | clear: both; 166 | } 167 | 168 | .markdown p, 169 | .markdown pre { 170 | margin: 1em 0; 171 | } 172 | 173 | .markdown > p, 174 | .markdown > blockquote, 175 | .markdown > .highlight, 176 | .markdown > ol, 177 | .markdown > ul { 178 | width: 80%; 179 | } 180 | 181 | .markdown ul > li { 182 | list-style: circle; 183 | } 184 | 185 | .markdown > ul li, 186 | .markdown blockquote ul > li { 187 | margin-left: 20px; 188 | padding-left: 4px; 189 | } 190 | 191 | .markdown > ul li p, 192 | .markdown > ol li p { 193 | margin: 0.6em 0; 194 | } 195 | 196 | .markdown ol > li { 197 | list-style: decimal; 198 | } 199 | 200 | .markdown > ol li, 201 | .markdown blockquote ol > li { 202 | margin-left: 20px; 203 | padding-left: 4px; 204 | } 205 | 206 | .markdown code { 207 | margin: 0 3px; 208 | padding: 0 5px; 209 | background: #eee; 210 | border-radius: 3px; 211 | } 212 | 213 | .markdown pre { 214 | border-radius: 6px; 215 | background: #f7f7f7; 216 | padding: 20px; 217 | } 218 | 219 | .markdown pre code { 220 | border: none; 221 | background: #f7f7f7; 222 | margin: 0; 223 | } 224 | 225 | .markdown strong, 226 | .markdown b { 227 | font-weight: 600; 228 | } 229 | 230 | .markdown > table { 231 | border-collapse: collapse; 232 | border-spacing: 0px; 233 | empty-cells: show; 234 | border: 1px solid #e9e9e9; 235 | width: 95%; 236 | margin-bottom: 24px; 237 | } 238 | 239 | .markdown > table th { 240 | white-space: nowrap; 241 | color: #333; 242 | font-weight: 600; 243 | 244 | } 245 | 246 | .markdown > table th, 247 | .markdown > table td { 248 | border: 1px solid #e9e9e9; 249 | padding: 8px 16px; 250 | text-align: left; 251 | } 252 | 253 | .markdown > table th { 254 | background: #F7F7F7; 255 | } 256 | 257 | .markdown blockquote { 258 | font-size: 90%; 259 | color: #999; 260 | border-left: 4px solid #e9e9e9; 261 | padding-left: 0.8em; 262 | margin: 1em 0; 263 | font-style: italic; 264 | } 265 | 266 | .markdown blockquote p { 267 | margin: 0; 268 | } 269 | 270 | .markdown .anchor { 271 | opacity: 0; 272 | transition: opacity 0.3s ease; 273 | margin-left: 8px; 274 | } 275 | 276 | .markdown .waiting { 277 | color: #ccc; 278 | } 279 | 280 | .markdown h1:hover .anchor, 281 | .markdown h2:hover .anchor, 282 | .markdown h3:hover .anchor, 283 | .markdown h4:hover .anchor, 284 | .markdown h5:hover .anchor, 285 | .markdown h6:hover .anchor { 286 | opacity: 1; 287 | display: inline-block; 288 | } 289 | 290 | .markdown > br, 291 | .markdown > p > br { 292 | clear: both; 293 | } 294 | 295 | 296 | .hljs { 297 | display: block; 298 | background: white; 299 | padding: 0.5em; 300 | color: #333333; 301 | overflow-x: auto; 302 | } 303 | 304 | .hljs-comment, 305 | .hljs-meta { 306 | color: #969896; 307 | } 308 | 309 | .hljs-string, 310 | .hljs-variable, 311 | .hljs-template-variable, 312 | .hljs-strong, 313 | .hljs-emphasis, 314 | .hljs-quote { 315 | color: #df5000; 316 | } 317 | 318 | .hljs-keyword, 319 | .hljs-selector-tag, 320 | .hljs-type { 321 | color: #a71d5d; 322 | } 323 | 324 | .hljs-literal, 325 | .hljs-symbol, 326 | .hljs-bullet, 327 | .hljs-attribute { 328 | color: #0086b3; 329 | } 330 | 331 | .hljs-section, 332 | .hljs-name { 333 | color: #63a35c; 334 | } 335 | 336 | .hljs-tag { 337 | color: #333333; 338 | } 339 | 340 | .hljs-title, 341 | .hljs-attr, 342 | .hljs-selector-id, 343 | .hljs-selector-class, 344 | .hljs-selector-attr, 345 | .hljs-selector-pseudo { 346 | color: #795da3; 347 | } 348 | 349 | .hljs-addition { 350 | color: #55a532; 351 | background-color: #eaffea; 352 | } 353 | 354 | .hljs-deletion { 355 | color: #bd2c00; 356 | background-color: #ffecec; 357 | } 358 | 359 | .hljs-link { 360 | text-decoration: underline; 361 | } 362 | 363 | pre{ 364 | background: #fff; 365 | } 366 | 367 | 368 | 369 | 370 | 371 | -------------------------------------------------------------------------------- /vue-chat-client/src/components/sider-left.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 212 | 213 | -------------------------------------------------------------------------------- /vue-chat-client/src/assets/font-icon/iconfont.js: -------------------------------------------------------------------------------- 1 | ;(function(window) { 2 | 3 | var svgSprite = '' + 4 | '' + 5 | '' + 6 | '' + 7 | '' + 8 | '' + 9 | '' + 10 | '' + 11 | '' + 12 | '' + 13 | '' + 14 | '' + 15 | '' + 16 | '' + 17 | '' + 18 | '' + 19 | '' + 20 | '' + 21 | '' + 22 | '' + 23 | '' + 24 | '' + 25 | '' + 26 | '' + 27 | '' + 28 | '' + 29 | '' + 30 | '' + 31 | '' + 32 | '' + 33 | '' + 34 | '' + 35 | '' + 36 | '' + 37 | '' + 38 | '' + 39 | '' + 40 | '' + 41 | '' + 42 | '' + 43 | '' + 44 | '' + 45 | '' + 46 | '' + 47 | '' + 48 | '' + 49 | '' + 50 | '' + 51 | '' + 52 | '' + 53 | '' + 54 | '' + 55 | '' + 56 | '' + 57 | '' + 58 | '' + 59 | '' + 60 | '' + 61 | '' + 62 | '' + 63 | '' + 64 | '' + 65 | '' + 66 | '' + 67 | '' 68 | var script = function() { 69 | var scripts = document.getElementsByTagName('script') 70 | return scripts[scripts.length - 1] 71 | }() 72 | var shouldInjectCss = script.getAttribute("data-injectcss") 73 | 74 | /** 75 | * document ready 76 | */ 77 | var ready = function(fn) { 78 | if (document.addEventListener) { 79 | if (~["complete", "loaded", "interactive"].indexOf(document.readyState)) { 80 | setTimeout(fn, 0) 81 | } else { 82 | var loadFn = function() { 83 | document.removeEventListener("DOMContentLoaded", loadFn, false) 84 | fn() 85 | } 86 | document.addEventListener("DOMContentLoaded", loadFn, false) 87 | } 88 | } else if (document.attachEvent) { 89 | IEContentLoaded(window, fn) 90 | } 91 | 92 | function IEContentLoaded(w, fn) { 93 | var d = w.document, 94 | done = false, 95 | // only fire once 96 | init = function() { 97 | if (!done) { 98 | done = true 99 | fn() 100 | } 101 | } 102 | // polling for no errors 103 | var polling = function() { 104 | try { 105 | // throws errors until after ondocumentready 106 | d.documentElement.doScroll('left') 107 | } catch (e) { 108 | setTimeout(polling, 50) 109 | return 110 | } 111 | // no errors, fire 112 | 113 | init() 114 | }; 115 | 116 | polling() 117 | // trying to always fire before onload 118 | d.onreadystatechange = function() { 119 | if (d.readyState == 'complete') { 120 | d.onreadystatechange = null 121 | init() 122 | } 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * Insert el before target 129 | * 130 | * @param {Element} el 131 | * @param {Element} target 132 | */ 133 | 134 | var before = function(el, target) { 135 | target.parentNode.insertBefore(el, target) 136 | } 137 | 138 | /** 139 | * Prepend el to target 140 | * 141 | * @param {Element} el 142 | * @param {Element} target 143 | */ 144 | 145 | var prepend = function(el, target) { 146 | if (target.firstChild) { 147 | before(el, target.firstChild) 148 | } else { 149 | target.appendChild(el) 150 | } 151 | } 152 | 153 | function appendSvg() { 154 | var div, svg 155 | 156 | div = document.createElement('div') 157 | div.innerHTML = svgSprite 158 | svgSprite = null 159 | svg = div.getElementsByTagName('svg')[0] 160 | if (svg) { 161 | svg.setAttribute('aria-hidden', 'true') 162 | svg.style.position = 'absolute' 163 | svg.style.width = 0 164 | svg.style.height = 0 165 | svg.style.overflow = 'hidden' 166 | prepend(svg, document.body) 167 | } 168 | } 169 | 170 | if (shouldInjectCss && !window.__iconfont__svg__cssinject__) { 171 | window.__iconfont__svg__cssinject__ = true 172 | try { 173 | document.write(""); 174 | } catch (e) { 175 | console && console.log(e) 176 | } 177 | } 178 | 179 | ready(appendSvg) 180 | 181 | 182 | })(window) -------------------------------------------------------------------------------- /vue-chat-client/src/components/sider-right.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 185 | 186 | --------------------------------------------------------------------------------