├── .gitignore
├── README.md
├── client
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── README.md
├── build
│ ├── build.js
│ ├── check-versions.js
│ ├── logo.png
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ ├── webpack.prod.conf.js
│ └── webpack.test.conf.js
├── config
│ ├── dev.env.js
│ ├── index.js
│ ├── prod.env.js
│ └── test.env.js
├── index.html
├── node_modules - 快捷方式.lnk
├── package-lock.json
├── package.json
├── src
│ ├── App.vue
│ ├── api
│ │ ├── index.js
│ │ └── user
│ │ │ └── index.js
│ ├── base
│ │ ├── button
│ │ │ └── Button.vue
│ │ ├── card
│ │ │ └── Card.vue
│ │ ├── input
│ │ │ └── Input.vue
│ │ ├── list
│ │ │ └── List.vue
│ │ ├── login
│ │ │ └── Login.vue
│ │ ├── message
│ │ │ └── Message.vue
│ │ ├── register
│ │ │ └── Register.vue
│ │ └── text
│ │ │ └── Text.vue
│ ├── common
│ │ ├── image
│ │ │ ├── background.jpg
│ │ │ └── icon.png
│ │ └── js
│ │ │ └── config.js
│ ├── components
│ │ ├── Contact.vue
│ │ ├── Index.vue
│ │ ├── Layout.vue
│ │ ├── Public.vue
│ │ └── Talk.vue
│ ├── main.js
│ ├── router
│ │ └── index.js
│ └── store
│ │ ├── actions.js
│ │ ├── index.js
│ │ ├── modules
│ │ ├── friend.js
│ │ ├── token.js
│ │ └── user.js
│ │ ├── mutation_types.js
│ │ └── mutations.js
├── static
│ ├── .gitkeep
│ └── config.js
└── test
│ └── unit
│ ├── .eslintrc
│ ├── index.js
│ ├── karma.conf.js
│ └── specs
│ └── HelloWorld.spec.js
├── document
└── user.md
├── public
├── index.html
└── static
│ ├── config.js
│ ├── css
│ ├── app.a7948a6d6a412660d155af7c08bb30b6.css
│ └── app.a7948a6d6a412660d155af7c08bb30b6.css.map
│ ├── fonts
│ └── element-icons.6f0a763.ttf
│ ├── img
│ ├── background.5a9adad.jpg
│ └── icon.3d77c66.png
│ └── js
│ ├── app.7ddf539c0082fd3a20ca.js
│ ├── app.7ddf539c0082fd3a20ca.js.map
│ ├── manifest.2ae2e69a05c33dfc65f8.js
│ ├── manifest.2ae2e69a05c33dfc65f8.js.map
│ ├── vendor.2022a142f4197d616f5f.js
│ └── vendor.2022a142f4197d616f5f.js.map
└── server
├── .autod.conf.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── README.md
├── README.zh-CN.md
├── app
├── controller
│ ├── friend.js
│ ├── request.js
│ ├── talk.js
│ └── user.js
├── io
│ ├── controller
│ │ ├── chat.js
│ │ └── login.js
│ └── middleware
│ │ └── auth.js
├── middleware
│ └── verify_token.js
├── model
│ ├── request.js
│ ├── talk.js
│ └── user.js
├── router.js
└── service
│ ├── request.js
│ ├── talk.js
│ └── user.js
├── appveyor.yml
├── config
├── config.default.js
└── plugin.js
├── package.json
└── test
└── app
├── controller
└── user.test.js
└── service
└── user.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | .idea
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `VUE-WE-CHAT`
2 | ## 仿微信网页聊天,使用vue框架,websocket实现聊天文件发送等功能,webRTC实现语音视频聊天等功能
3 |
4 | ## 在线地址
5 | [仿微信在线聊天](http://enable.dpdaidai.top/#/index)
6 | - 演示账户
7 | - 账号: 3,密码: 3
8 | - 账号: 4, 密码: 4
9 | - 目前只能进行双方同时登陆的聊天操作
10 |
11 | ## 前端 client
12 | - 安装依赖 `npm install`
13 | - 本地开发 `npm run dev`
14 | - 打包 `npm run build`
15 |
16 | ## 后端 server
17 | - 安装 `egg` `npm i egg-init -g`
18 | - 安装依赖 `npm install`
19 | - 本地开发 `npm run dev`
20 | - 调试
21 | - `npm run debug`
22 | - 浏览器在控制台打开`DevTools`地址,在`sources`中找到文件,打断点进行调试
23 | - `node@10`以上的版本运行时会报`ERROR 16205 nodejs.TypeError: Cannot set property 'parsingHeadersStart' of undefined (uncaughtException throw 2 times on pid:16205)`错误,解决办法,切换`node`版本为`8.15.0`
24 |
25 | ## mongodb
26 | - 开启`mongodb`服务 `sudo mongod`
27 |
28 | ## 功能
29 |
30 | ### Server
31 | - [x] **用户管理**
32 | - [x] 注册
33 | - [x] 登录
34 | - [x] 关闭浏览器退出登录
35 | - [x] `websocket`长连接
36 | - [x] 连接成功更新`socket id`信息
37 | - [ ] 好友管理
38 | - [ ] 添加好友
39 | - [x] 查找用户
40 | - [x] 发送好友请求
41 | - [x] 获取收到的好友请求
42 | - [ ] 添加好友操作
43 | - [x] 同意
44 | - [ ] 拒绝
45 | - [ ] 消息发送
46 | - [x] 同时在线
47 | - [ ] 离线信息保存
48 | ### Client
49 |
50 | - [x] **用户管理**
51 | - [x] 注册
52 | - [x] 登录
53 | - [x] 登录状态
54 | - [x] 用户在线状态
55 | - [x] 使用`websock`长连接判断用户在线状态
56 | - [ ] 好友管理
57 | - [ ] 添加好友
58 | - [x] 查找用户
59 | - [x] 发送好友请求
60 | - [x] 获取收到的好友请求
61 | - [ ] 添加好友操作
62 | - [x] 同意
63 | - [ ] 拒绝
64 | - [ ] 好友列表
65 | - [ ] 消息发送
66 | - [x] 同时在线
67 | - [ ] 登录时收到离线信息
68 | - [ ] 信息收到提示
69 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-vue-jsx", "transform-runtime"],
12 | "env": {
13 | "test": {
14 | "presets": ["env", "stage-2"],
15 | "plugins": ["transform-vue-jsx", "istanbul"]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/.eslintignore:
--------------------------------------------------------------------------------
1 | /build/
2 | /config/
3 | /dist/
4 | /*.js
5 | /test/unit/coverage/
6 |
--------------------------------------------------------------------------------
/client/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | parser: 'babel-eslint'
7 | },
8 | env: {
9 | browser: true,
10 | },
11 | extends: [
12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
14 | 'plugin:vue/essential',
15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md
16 | 'standard'
17 | ],
18 | // required to lint *.vue files
19 | plugins: [
20 | 'vue'
21 | ],
22 | // add your custom rules here
23 | rules: {
24 | // allow async-await
25 | 'generator-star-spacing': 'off',
26 | // allow debugger during development
27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | /test/unit/coverage/
8 |
9 | # Editor directories and files
10 | .idea
11 | .vscode
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | *.sln
16 |
--------------------------------------------------------------------------------
/client/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | "postcss-import": {},
6 | "postcss-url": {},
7 | // to edit target browsers: use "browserslist" field in package.json
8 | "autoprefixer": {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # vue-we-chat
2 |
3 | > A Vue.js project
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 | # run unit tests
21 | npm run unit
22 |
23 | # run all tests
24 | npm test
25 | ```
26 |
27 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
28 |
--------------------------------------------------------------------------------
/client/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // 判断当前node和npm的版本号,删除打包目录下的文件,再进行打包
3 | require('./check-versions')()
4 |
5 | process.env.NODE_ENV = 'production'
6 |
7 | const ora = require('ora')
8 | const rm = require('rimraf')
9 | const path = require('path')
10 | const chalk = require('chalk')
11 | const webpack = require('webpack')
12 | const config = require('../config')
13 | const webpackConfig = require('./webpack.prod.conf')
14 |
15 | const spinner = ora('building for production...')
16 | spinner.start()
17 |
18 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
19 | if (err) throw err
20 | webpack(webpackConfig, (err, stats) => {
21 | spinner.stop()
22 | if (err) throw err
23 | process.stdout.write(stats.toString({
24 | colors: true,
25 | modules: false,
26 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
27 | chunks: false,
28 | chunkModules: false
29 | }) + '\n\n')
30 |
31 | if (stats.hasErrors()) {
32 | console.log(chalk.red(' Build failed with errors.\n'))
33 | process.exit(1)
34 | }
35 |
36 | console.log(chalk.cyan(' Build complete.\n'))
37 | console.log(chalk.yellow(
38 | ' Tip: built files are meant to be served over an HTTP server.\n' +
39 | ' Opening index.html over file:// won\'t work.\n'
40 | ))
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/client/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // 检测当前环境中的node和npm版本和我们需要的是否一致
3 | const chalk = require('chalk')
4 | const semver = require('semver')
5 | const packageConfig = require('../package.json')
6 | const shell = require('shelljs')
7 |
8 | function exec (cmd) {
9 | return require('child_process').execSync(cmd).toString().trim()
10 | }
11 |
12 | const versionRequirements = [
13 | {
14 | name: 'node',
15 | currentVersion: semver.clean(process.version),
16 | versionRequirement: packageConfig.engines.node
17 | }
18 | ]
19 |
20 | if (shell.which('npm')) {
21 | versionRequirements.push({
22 | name: 'npm',
23 | currentVersion: exec('npm --version'),
24 | versionRequirement: packageConfig.engines.npm
25 | })
26 | }
27 |
28 | module.exports = function () {
29 | const warnings = []
30 |
31 | for (let i = 0; i < versionRequirements.length; i++) {
32 | const mod = versionRequirements[i]
33 |
34 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
35 | warnings.push(mod.name + ': ' +
36 | chalk.red(mod.currentVersion) + ' should be ' +
37 | chalk.green(mod.versionRequirement)
38 | )
39 | }
40 | }
41 |
42 | if (warnings.length) {
43 | console.log('')
44 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
45 | console.log()
46 |
47 | for (let i = 0; i < warnings.length; i++) {
48 | const warning = warnings[i]
49 | console.log(' ' + warning)
50 | }
51 |
52 | console.log()
53 | process.exit(1)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/client/build/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/client/build/logo.png
--------------------------------------------------------------------------------
/client/build/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const config = require('../config')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | const packageConfig = require('../package.json')
6 |
7 | // 返回不同环境下的static目录位置
8 | exports.assetsPath = function (_path) {
9 | const assetsSubDirectory = process.env.NODE_ENV === 'production'
10 | ? config.build.assetsSubDirectory
11 | : config.dev.assetsSubDirectory
12 |
13 | return path.posix.join(assetsSubDirectory, _path)
14 | }
15 |
16 | // 为不同的css预处理器提供统一的生成方式
17 | exports.cssLoaders = function (options) {
18 | options = options || {}
19 |
20 | const cssLoader = {
21 | loader: 'css-loader',
22 | options: {
23 | sourceMap: options.sourceMap
24 | }
25 | }
26 |
27 | const postcssLoader = {
28 | loader: 'postcss-loader',
29 | options: {
30 | sourceMap: options.sourceMap
31 | }
32 | }
33 |
34 | // generate loader string to be used with extract text plugin
35 | function generateLoaders (loader, loaderOptions) {
36 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
37 |
38 | if (loader) {
39 | loaders.push({
40 | loader: loader + '-loader',
41 | options: Object.assign({}, loaderOptions, {
42 | sourceMap: options.sourceMap
43 | })
44 | })
45 | }
46 |
47 | // Extract CSS when that option is specified
48 | // (which is the case during production build)
49 | if (options.extract) {
50 | return ExtractTextPlugin.extract({
51 | use: loaders,
52 | fallback: 'vue-style-loader'
53 | })
54 | } else {
55 | return ['vue-style-loader'].concat(loaders)
56 | }
57 | }
58 |
59 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
60 | return {
61 | css: generateLoaders(),
62 | postcss: generateLoaders(),
63 | less: generateLoaders('less'),
64 | sass: generateLoaders('sass', { indentedSyntax: true }),
65 | scss: generateLoaders('sass'),
66 | stylus: generateLoaders('stylus'),
67 | styl: generateLoaders('stylus')
68 | }
69 | }
70 |
71 | // 为单独的style文件创建加载器配置
72 | // Generate loaders for standalone style files (outside of .vue)
73 | exports.styleLoaders = function (options) {
74 | const output = []
75 | const loaders = exports.cssLoaders(options)
76 |
77 | for (const extension in loaders) {
78 | const loader = loaders[extension]
79 | output.push({
80 | test: new RegExp('\\.' + extension + '$'),
81 | use: loader
82 | })
83 | }
84 |
85 | return output
86 | }
87 |
88 | // 以类似浏览器的通知形式展示信息
89 | exports.createNotifierCallback = () => {
90 | const notifier = require('node-notifier')
91 |
92 | return (severity, errors) => {
93 | if (severity !== 'error') return
94 |
95 | const error = errors[0]
96 | const filename = error.file && error.file.split('!').pop()
97 |
98 | notifier.notify({
99 | title: packageConfig.name,
100 | message: severity + ': ' + error.name,
101 | subtitle: filename || '',
102 | icon: path.join(__dirname, 'logo.png')
103 | })
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/client/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const config = require('../config')
4 | const isProduction = process.env.NODE_ENV === 'production'
5 | // 根据环境不同引入不同的sourceMap文件
6 | const sourceMapEnabled = isProduction
7 | ? config.build.productionSourceMap
8 | : config.dev.cssSourceMap
9 |
10 | module.exports = {
11 | loaders: utils.cssLoaders({
12 | sourceMap: sourceMapEnabled,
13 | extract: isProduction
14 | }),
15 | cssSourceMap: sourceMapEnabled,
16 | cacheBusting: config.dev.cacheBusting,
17 | transformToRequire: {
18 | video: ['src', 'poster'],
19 | source: 'src',
20 | img: 'src',
21 | image: 'xlink:href'
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/client/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path') // 提供文件处理路径的小工具
3 | const utils = require('./utils') // 为整个框架提供方法
4 | const config = require('../config') // 环境配置文件
5 | const vueLoaderConfig = require('./vue-loader.conf') // 分析是否是生产环境,然后根据不同的环境来加载配置功能
6 |
7 | function resolve (dir) {
8 | return path.join(__dirname, '..', dir)
9 | }
10 |
11 | // eslint 的rules
12 | const createLintingRule = () => ({
13 | test: /\.(js|vue)$/,
14 | loader: 'eslint-loader',
15 | enforce: 'pre',
16 | include: [resolve('src'), resolve('test')],
17 | options: {
18 | formatter: require('eslint-friendly-formatter'),
19 | emitWarning: !config.dev.showEslintErrorsInOverlay
20 | }
21 | })
22 |
23 | // webpack 基础配置
24 | module.exports = {
25 | context: path.resolve(__dirname, '../'),
26 | entry: {
27 | app: './src/main.js'
28 | },
29 | output: {
30 | path: config.build.assetsRoot,
31 | filename: '[name].js',
32 | publicPath: process.env.NODE_ENV === 'production'
33 | ? config.build.assetsPublicPath
34 | : config.dev.assetsPublicPath
35 | },
36 | resolve: {
37 | extensions: ['.js', '.vue', '.json'],
38 | alias: {
39 | 'vue$': 'vue/dist/vue.esm.js',
40 | '@': resolve('src'),
41 | }
42 | },
43 | module: {
44 | rules: [
45 | ...(config.dev.useEslint ? [createLintingRule()] : []),
46 | {
47 | test: /\.vue$/,
48 | loader: 'vue-loader',
49 | options: vueLoaderConfig
50 | },
51 | {
52 | test: /\.js$/,
53 | loader: 'babel-loader',
54 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
55 | },
56 | {
57 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
58 | loader: 'url-loader',
59 | options: {
60 | limit: 10000,
61 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
62 | }
63 | },
64 | {
65 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
66 | loader: 'url-loader',
67 | options: {
68 | limit: 10000,
69 | name: utils.assetsPath('media/[name].[hash:7].[ext]')
70 | }
71 | },
72 | {
73 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
74 | loader: 'url-loader',
75 | options: {
76 | limit: 10000,
77 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
78 | }
79 | }
80 | ]
81 | },
82 | // 用于配置polyfill或mock某些Node.js全局变量和模块,
83 | // 使最初编写的nodejs代码可以在浏览器中运行
84 | node: {
85 | // prevent webpack from injecting useless setImmediate polyfill because Vue
86 | // source contains it (although only uses it if it's native).
87 | setImmediate: false,
88 | // prevent webpack from injecting mocks to Node native modules
89 | // that does not make sense for the client
90 | dgram: 'empty',
91 | fs: 'empty',
92 | net: 'empty',
93 | tls: 'empty',
94 | child_process: 'empty'
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/client/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const webpack = require('webpack')
4 | const config = require('../config')
5 | const merge = require('webpack-merge')
6 | const path = require('path')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
11 | const portfinder = require('portfinder')
12 |
13 | const HOST = process.env.HOST
14 | const PORT = process.env.PORT && Number(process.env.PORT)
15 |
16 | const devWebpackConfig = merge(baseWebpackConfig, {
17 | module: {
18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
19 | },
20 | // cheap-module-eval-source-map is faster for development
21 | devtool: config.dev.devtool,
22 |
23 | // these devServer options should be customized in /config/index.js
24 | devServer: {
25 | clientLogLevel: 'warning',
26 | historyApiFallback: {
27 | rewrites: [
28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
29 | ],
30 | },
31 | hot: true,
32 | contentBase: false, // since we use CopyWebpackPlugin.
33 | compress: true,
34 | host: HOST || config.dev.host,
35 | port: PORT || config.dev.port,
36 | open: config.dev.autoOpenBrowser,
37 | overlay: config.dev.errorOverlay
38 | ? { warnings: false, errors: true }
39 | : false,
40 | publicPath: config.dev.assetsPublicPath,
41 | proxy: config.dev.proxyTable,
42 | quiet: true, // necessary for FriendlyErrorsPlugin
43 | watchOptions: {
44 | poll: config.dev.poll,
45 | }
46 | },
47 | plugins: [
48 | new webpack.DefinePlugin({
49 | 'process.env': require('../config/dev.env')
50 | }),
51 | new webpack.HotModuleReplacementPlugin(),
52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
53 | new webpack.NoEmitOnErrorsPlugin(),
54 | // https://github.com/ampedandwired/html-webpack-plugin
55 | new HtmlWebpackPlugin({
56 | filename: 'index.html',
57 | template: 'index.html',
58 | inject: true
59 | }),
60 | // copy custom static assets
61 | new CopyWebpackPlugin([
62 | {
63 | from: path.resolve(__dirname, '../static'),
64 | to: config.dev.assetsSubDirectory,
65 | ignore: ['.*']
66 | }
67 | ])
68 | ]
69 | })
70 |
71 | // 为了确保启动程序时,如果端口被占用,会通过portfinder发布新端口
72 | module.exports = new Promise((resolve, reject) => {
73 | portfinder.basePort = process.env.PORT || config.dev.port
74 | portfinder.getPort((err, port) => {
75 | if (err) {
76 | reject(err)
77 | } else {
78 | // publish the new Port, necessary for e2e tests
79 | process.env.PORT = port
80 | // add port to devServer config
81 | devWebpackConfig.devServer.port = port
82 |
83 | // Add FriendlyErrorsPlugin
84 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
85 | compilationSuccessInfo: {
86 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
87 | },
88 | onErrors: config.dev.notifyOnErrors
89 | ? utils.createNotifierCallback()
90 | : undefined
91 | }))
92 |
93 | resolve(devWebpackConfig)
94 | }
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/client/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
13 |
14 | const env = process.env.NODE_ENV === 'testing'
15 | ? require('../config/test.env')
16 | : require('../config/prod.env')
17 |
18 | const webpackConfig = merge(baseWebpackConfig, {
19 | module: {
20 | rules: utils.styleLoaders({
21 | sourceMap: config.build.productionSourceMap,
22 | extract: true,
23 | usePostCSS: true
24 | })
25 | },
26 | devtool: config.build.productionSourceMap ? config.build.devtool : false,
27 | output: {
28 | path: config.build.assetsRoot,
29 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
31 | },
32 | plugins: [
33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
34 | new webpack.DefinePlugin({
35 | 'process.env': env
36 | }),
37 | new UglifyJsPlugin({
38 | uglifyOptions: {
39 | compress: {
40 | warnings: false
41 | }
42 | },
43 | sourceMap: config.build.productionSourceMap,
44 | parallel: true
45 | }),
46 | // extract css into its own file
47 | new ExtractTextPlugin({
48 | filename: utils.assetsPath('css/[name].[contenthash].css'),
49 | // Setting the following option to `false` will not extract CSS from codesplit chunks.
50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
53 | allChunks: true,
54 | }),
55 | // Compress extracted CSS. We are using this plugin so that possible
56 | // duplicated CSS from different components can be deduped.
57 | // css优化插件
58 | new OptimizeCSSPlugin({
59 | cssProcessorOptions: config.build.productionSourceMap
60 | ? { safe: true, map: { inline: false } }
61 | : { safe: true }
62 | }),
63 | // generate dist index.html with correct asset hash for caching.
64 | // you can customize output by editing /index.html
65 | // see https://github.com/ampedandwired/html-webpack-plugin
66 | new HtmlWebpackPlugin({
67 | filename: process.env.NODE_ENV === 'testing'
68 | ? 'index.html'
69 | : config.build.index,
70 | template: 'index.html',
71 | inject: true,
72 | minify: {
73 | removeComments: true,
74 | collapseWhitespace: true,
75 | removeAttributeQuotes: true
76 | // more options:
77 | // https://github.com/kangax/html-minifier#options-quick-reference
78 | },
79 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
80 | chunksSortMode: 'dependency'
81 | }),
82 | // keep module.id stable when vendor modules does not change
83 | new webpack.HashedModuleIdsPlugin(),
84 | // enable scope hoisting
85 | new webpack.optimize.ModuleConcatenationPlugin(),
86 | // split vendor js into its own file
87 | new webpack.optimize.CommonsChunkPlugin({
88 | name: 'vendor',
89 | minChunks (module) {
90 | // any required modules inside node_modules are extracted to vendor
91 | return (
92 | module.resource &&
93 | /\.js$/.test(module.resource) &&
94 | module.resource.indexOf(
95 | path.join(__dirname, '../node_modules')
96 | ) === 0
97 | )
98 | }
99 | }),
100 | // extract webpack runtime and module manifest to its own file in order to
101 | // prevent vendor hash from being updated whenever app bundle is updated
102 | new webpack.optimize.CommonsChunkPlugin({
103 | name: 'manifest',
104 | minChunks: Infinity
105 | }),
106 | // This instance extracts shared chunks from code splitted chunks and bundles them
107 | // in a separate chunk, similar to the vendor chunk
108 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
109 | new webpack.optimize.CommonsChunkPlugin({
110 | name: 'app',
111 | async: 'vendor-async',
112 | children: true,
113 | minChunks: 3
114 | }),
115 |
116 | // copy custom static assets
117 | new CopyWebpackPlugin([
118 | {
119 | from: path.resolve(__dirname, '../static'),
120 | to: config.build.assetsSubDirectory,
121 | ignore: ['.*']
122 | }
123 | ])
124 | ]
125 | })
126 |
127 | if (config.build.productionGzip) {
128 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
129 |
130 | webpackConfig.plugins.push(
131 | new CompressionWebpackPlugin({
132 | asset: '[path].gz[query]',
133 | algorithm: 'gzip',
134 | test: new RegExp(
135 | '\\.(' +
136 | config.build.productionGzipExtensions.join('|') +
137 | ')$'
138 | ),
139 | threshold: 10240,
140 | minRatio: 0.8
141 | })
142 | )
143 | }
144 |
145 | if (config.build.bundleAnalyzerReport) {
146 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
147 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
148 | }
149 |
150 | module.exports = webpackConfig
151 |
--------------------------------------------------------------------------------
/client/build/webpack.test.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // This is the webpack config used for unit tests.
3 |
4 | const utils = require('./utils')
5 | const webpack = require('webpack')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 |
9 | const webpackConfig = merge(baseWebpackConfig, {
10 | // use inline sourcemap for karma-sourcemap-loader
11 | module: {
12 | rules: utils.styleLoaders()
13 | },
14 | devtool: '#inline-source-map',
15 | resolveLoader: {
16 | alias: {
17 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option
18 | // see discussion at https://github.com/vuejs/vue-loader/issues/724
19 | 'scss-loader': 'sass-loader'
20 | }
21 | },
22 | plugins: [
23 | new webpack.DefinePlugin({
24 | 'process.env': require('../config/test.env')
25 | })
26 | ]
27 | })
28 |
29 | // no need for app entry during tests
30 | delete webpackConfig.entry
31 |
32 | module.exports = webpackConfig
33 |
--------------------------------------------------------------------------------
/client/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"',
7 | socketHost: '"http://127.0.0.1:7001"', //wxy本地测试
8 | })
9 |
--------------------------------------------------------------------------------
/client/config/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // Template version: 1.3.1
3 | // see http://vuejs-templates.github.io/webpack for documentation.
4 | const devRemoteUrl = 'http://127.0.0.1:7001/'
5 | const path = require('path')
6 |
7 | module.exports = {
8 | dev: {
9 |
10 | // Paths
11 | assetsSubDirectory: 'static',
12 | assetsPublicPath: '/',
13 | // proxyTable: {},
14 | proxyTable: {
15 | '/': {
16 | target: devRemoteUrl,
17 | pathRewrite: {'^/' : '/'}, // 重写路径
18 | changeOrigin: true
19 | }
20 | },
21 | // Various Dev Server settings
22 | host: '0.0.0.0', // can be overwritten by process.env.HOST
23 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
24 | autoOpenBrowser: false,
25 | errorOverlay: true,
26 | notifyOnErrors: true,
27 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
28 |
29 | // Use Eslint Loader?
30 | // If true, your code will be linted during bundling and
31 | // linting errors and warnings will be shown in the console.
32 | useEslint: true,
33 | // If true, eslint errors and warnings will also be shown in the error overlay
34 | // in the browser.
35 | showEslintErrorsInOverlay: false,
36 |
37 | /**
38 | * Source Maps
39 | */
40 |
41 | // https://webpack.js.org/configuration/devtool/#development
42 | devtool: 'cheap-module-eval-source-map',
43 |
44 | // If you have problems debugging vue-files in devtools,
45 | // set this to false - it *may* help
46 | // https://vue-loader.vuejs.org/en/options.html#cachebusting
47 | cacheBusting: true,
48 |
49 | cssSourceMap: true
50 | },
51 |
52 | build: {
53 | // Template for index.html
54 | index: path.resolve(__dirname, '../dist/index.html'),
55 |
56 | // Paths
57 | assetsRoot: path.resolve(__dirname, '../dist'),
58 | assetsSubDirectory: 'static',
59 | assetsPublicPath: '/',
60 |
61 | /**
62 | * Source Maps
63 | */
64 |
65 | productionSourceMap: true,
66 | // https://webpack.js.org/configuration/devtool/#production
67 | devtool: '#source-map',
68 |
69 | // Gzip off by default as many popular static hosts such as
70 | // Surge or Netlify already gzip all static assets for you.
71 | // Before setting to `true`, make sure to:
72 | // npm install --save-dev compression-webpack-plugin
73 | productionGzip: false,
74 | productionGzipExtensions: ['js', 'css'],
75 |
76 | // Run the build command with an extra argument to
77 | // View the bundle analyzer report after build finishes:
78 | // `npm run build --report`
79 | // Set to `true` or `false` to always turn it on or off
80 | bundleAnalyzerReport: process.env.npm_config_report
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/client/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"',
4 | socketHost: '"http://enable.dpdaidai.top:7001"', //wxy本地测试,
5 | baseUrl: '"http://enable.dpdaidai.top:7001"'
6 | }
7 |
--------------------------------------------------------------------------------
/client/config/test.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const devEnv = require('./dev.env')
4 |
5 | module.exports = merge(devEnv, {
6 | NODE_ENV: '"testing"'
7 | })
8 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue-we-chat
7 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/client/node_modules - 快捷方式.lnk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/client/node_modules - 快捷方式.lnk
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-we-chat",
3 | "version": "1.0.0",
4 | "description": "A Vue.js project",
5 | "author": "daidai <571143755@qq.com>",
6 | "private": true,
7 | "scripts": {
8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
9 | "start": "npm run dev",
10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
11 | "test": "npm run unit",
12 | "lint": "eslint --ext .js,.vue src test/unit",
13 | "build": "node build/build.js",
14 | "public": "rm -rf ../public/* && npm run build && mv ./dist/* ../public/"
15 | },
16 | "dependencies": {
17 | "axios": "^0.18.0",
18 | "better-scroll": "^1.12.5",
19 | "element-ui": "^2.4.7",
20 | "vue": "^2.5.2",
21 | "vue-router": "^3.0.1",
22 | "vuex": "^3.0.1",
23 | "es6-promise": "^4.2.5",
24 | "isomorphic-fetch": "^2.2.1",
25 | "vue-socket.io": "^2.1.1-b"
26 | },
27 | "devDependencies": {
28 | "autoprefixer": "^7.1.2",
29 | "babel-core": "^6.22.1",
30 | "babel-eslint": "^8.2.1",
31 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
32 | "babel-loader": "^7.1.1",
33 | "babel-plugin-istanbul": "^4.1.1",
34 | "babel-plugin-syntax-jsx": "^6.18.0",
35 | "babel-plugin-transform-runtime": "^6.22.0",
36 | "babel-plugin-transform-vue-jsx": "^3.5.0",
37 | "babel-preset-env": "^1.3.2",
38 | "babel-preset-stage-2": "^6.22.0",
39 | "chai": "^4.1.2",
40 | "chalk": "^2.0.1",
41 | "copy-webpack-plugin": "^4.0.1",
42 | "cross-env": "^5.0.1",
43 | "css-loader": "^0.28.0",
44 | "eslint": "^4.15.0",
45 | "eslint-config-standard": "^10.2.1",
46 | "eslint-friendly-formatter": "^3.0.0",
47 | "eslint-loader": "^1.7.1",
48 | "eslint-plugin-import": "^2.7.0",
49 | "eslint-plugin-node": "^5.2.0",
50 | "eslint-plugin-promise": "^3.4.0",
51 | "eslint-plugin-standard": "^3.0.1",
52 | "eslint-plugin-vue": "^4.0.0",
53 | "extract-text-webpack-plugin": "^3.0.0",
54 | "file-loader": "^1.1.4",
55 | "friendly-errors-webpack-plugin": "^1.6.1",
56 | "html-webpack-plugin": "^2.30.1",
57 | "inject-loader": "^3.0.0",
58 | "karma": "^3.0.0",
59 | "karma-coverage": "^1.1.1",
60 | "karma-mocha": "^1.3.0",
61 | "karma-phantomjs-launcher": "^1.0.2",
62 | "karma-phantomjs-shim": "^1.4.0",
63 | "karma-sinon-chai": "^1.3.1",
64 | "karma-sourcemap-loader": "^0.3.7",
65 | "karma-spec-reporter": "0.0.31",
66 | "karma-webpack": "^2.0.2",
67 | "mocha": "^5.2.0",
68 | "node-notifier": "^5.1.2",
69 | "node-sass": "^4.9.2",
70 | "optimize-css-assets-webpack-plugin": "^3.2.0",
71 | "ora": "^1.2.0",
72 | "phantomjs-prebuilt": "^2.1.14",
73 | "portfinder": "^1.0.13",
74 | "postcss-import": "^11.0.0",
75 | "postcss-loader": "^2.0.8",
76 | "postcss-url": "^7.2.1",
77 | "rimraf": "^2.6.0",
78 | "sass-loader": "^7.0.3",
79 | "semver": "^5.3.0",
80 | "shelljs": "^0.7.6",
81 | "sinon": "^4.0.0",
82 | "sinon-chai": "^2.8.0",
83 | "uglifyjs-webpack-plugin": "^1.1.1",
84 | "url-loader": "^1.1.0",
85 | "vue-loader": "^13.3.0",
86 | "vue-style-loader": "^3.0.1",
87 | "vue-template-compiler": "^2.5.2",
88 | "webpack": "^3.6.0",
89 | "webpack-bundle-analyzer": "^2.9.0",
90 | "webpack-dev-server": "^2.9.1",
91 | "webpack-merge": "^4.1.0"
92 | },
93 | "engines": {
94 | "node": ">= 6.0.0",
95 | "npm": ">= 3.0.0"
96 | },
97 | "browserslist": [
98 | "> 1%",
99 | "last 2 versions",
100 | "not ie <= 8"
101 | ]
102 | }
103 |
--------------------------------------------------------------------------------
/client/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
18 |
--------------------------------------------------------------------------------
/client/src/api/index.js:
--------------------------------------------------------------------------------
1 | import store from '../store/index'
2 |
3 | require('es6-promise').polyfill()
4 | require('isomorphic-fetch')
5 |
6 | function parseResponse (response) {
7 | return Promise.all([response.status, response.statusText, response.json()])
8 | }
9 | function checkStatus ([status, statusText, data]) {
10 | if (status >= 200 && status <= 300) {
11 | return data
12 | }
13 | if (status === 401) {
14 | store.commit('DELETE_TOKEN')
15 | }
16 | const error = new Error(statusText)
17 | error.status = status
18 | error.errorMessage = data
19 | return Promise.reject(error)
20 | }
21 |
22 | export default {
23 | get (url, param = {}, headers = {}) {
24 | const reqHeaders = new Headers(headers)
25 | reqHeaders.append('Accept', 'application/json')
26 | if (store.state.token.token) {
27 | reqHeaders.append('Authorization', `Bearer ${store.state.token.token}`)
28 | }
29 | const query = []
30 | Object.keys(param).forEach(item => {
31 | query.push(`${item}=${encodeURIComponent(param[item])}`)
32 | })
33 | const params = query.length ? `?${query.join('&')}` : ''
34 | url = url + params
35 |
36 | const init = {
37 | method: 'GET',
38 | headers: reqHeaders,
39 | credential: 'include',
40 | cache: 'default',
41 | mode: 'cors'
42 | }
43 | url = process.env.NODE_ENV === 'development' ? url : `${process.env.baseUrl}${url}`
44 | return fetch(url, init)
45 | .then(parseResponse)
46 | .then(checkStatus)
47 | },
48 | post (url, param = {}, headers = {}) {
49 | const reqHeaders = new Headers(headers)
50 | reqHeaders.append('Content-Type', 'application/json')
51 | reqHeaders.append('Accept', 'application/json')
52 | if (store.state.token.token) {
53 | reqHeaders.append('Authorization', `Bearer ${store.state.token.token}`)
54 | }
55 | const init = {
56 | method: 'POST',
57 | headers: reqHeaders,
58 | credential: 'include',
59 | mode: 'cors',
60 | body: JSON.stringify(param)
61 | }
62 | url = process.env.NODE_ENV === 'development' ? url : `${process.env.baseUrl}${url}`
63 | return fetch(url, init)
64 | .then(parseResponse)
65 | .then(checkStatus)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/client/src/api/user/index.js:
--------------------------------------------------------------------------------
1 | import api from '../index'
2 |
3 | export default {
4 | register (data) {
5 | return api.post(`/api/register`, data)
6 | },
7 | login (data) {
8 | return api.post(`/api/login`, data)
9 | },
10 | setSocketId () {
11 | return api.post('/api/set/socketId')
12 | },
13 | getUserInfo () {
14 | return api.get('/api/user/info')
15 | },
16 | find (name) {
17 | return api.get(`/api/user/find/${name}`)
18 | },
19 | addUser (id, data) {
20 | return api.post(`/api/request/add/${id}`, data)
21 | },
22 | addFriend (friendId) {
23 | return api.post(`/api/friend/add/${friendId}`)
24 | },
25 | getAllNotReceiveTalk (id) {
26 | return api.get(`/api/talk/all/${id}`)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/base/button/Button.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
39 |
40 |
80 |
--------------------------------------------------------------------------------
/client/src/base/card/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
{{ list.value.length > 0 ? list.type : '没有数据' }}
18 |
19 |
20 |
![]()
21 |
22 |
23 |
{{item.nickname}}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
89 |
90 |
272 |
--------------------------------------------------------------------------------
/client/src/base/input/Input.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
49 |
50 |
142 |
--------------------------------------------------------------------------------
/client/src/base/list/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
44 |
45 |
46 |
170 |
171 |
277 |
--------------------------------------------------------------------------------
/client/src/base/login/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
40 |
41 |
62 |
--------------------------------------------------------------------------------
/client/src/base/message/Message.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/client/src/base/register/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
45 |
46 |
73 |
--------------------------------------------------------------------------------
/client/src/base/text/Text.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/client/src/common/image/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/client/src/common/image/background.jpg
--------------------------------------------------------------------------------
/client/src/common/image/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/client/src/common/image/icon.png
--------------------------------------------------------------------------------
/client/src/common/js/config.js:
--------------------------------------------------------------------------------
1 | // export const avatar = 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533019302401&di=ed0b72fd25e47aa8c540bccdbd072e6c&imgtype=0&src=http%3A%2F%2Ff.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2Fcaef76094b36acaf239dc5be7fd98d1001e99c76.jpg'
2 | export const ERR_OK = 200
3 | export const avatar = 'http://bpic.588ku.com/element_origin_min_pic/17/06/23/f21e1f3b279c62d6f3469ca6c84e638f.jpg'
4 |
--------------------------------------------------------------------------------
/client/src/components/Contact.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/client/src/components/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
VUE-WE-CHAT
5 |
6 |
7 |
8 |
9 |
10 |
11 |
81 |
82 |
115 |
--------------------------------------------------------------------------------
/client/src/components/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <<<<<<< HEAD
6 |
12 |
13 | =======
14 |
15 |
16 | >>>>>>> ac2b20c17dd218bf5d923256dc437be6d7923fc7
17 |
18 |
19 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
37 | 添加
38 | {{clickUserData.nickname}}
39 | 为好友
40 |
41 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
207 |
208 |
278 |
--------------------------------------------------------------------------------
/client/src/components/Public.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/client/src/components/Talk.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
![]()
11 |
12 |
13 |
14 |
15 |
{{message.message}}
16 |
![]()
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
40 |
41 |
42 |
43 |
124 |
125 |
335 |
--------------------------------------------------------------------------------
/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 | import Vue from 'vue'
4 | import App from './App'
5 | import router from './router'
6 | import ElementUI from 'element-ui'
7 | import 'element-ui/lib/theme-chalk/index.css'
8 | import store from './store'
9 | import Vuex from 'vuex'
10 | import VueSocketio from 'vue-socket.io'
11 |
12 | Vue.use(ElementUI)
13 | Vue.use(Vuex)
14 | Vue.use(VueSocketio, process.env.socketHost)
15 | Vue.config.productionTip = false
16 | router.beforeEach((to, from, next) => {
17 | if (to.meta.requireAuth) {
18 | const isLogin = store.state.token.token // 根据token判断是否登录
19 | if (isLogin) {
20 | next()
21 | } else {
22 | next({path: '/index'})
23 | }
24 | } else {
25 | next()
26 | }
27 | })
28 |
29 | /* eslint-disable no-new */
30 | new Vue({
31 | el: '#app',
32 | router,
33 | store,
34 | components: { App },
35 | template: ''
36 | })
37 |
--------------------------------------------------------------------------------
/client/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | import Talk from '@/components/Talk'
5 | import Public from '@/components/Public'
6 | import Contact from '@/components/Contact'
7 | import Index from '@/components/Index'
8 | import Main from '@/components/Layout'
9 |
10 | Vue.use(Router)
11 |
12 | export default new Router({
13 | routes: [
14 | {
15 | path: '/',
16 | component: Main,
17 | redirect: '/talk',
18 | children: [
19 | {
20 | path: 'talk',
21 | name: 'talk',
22 | component: Talk,
23 | meta: {
24 | requireAuth: true
25 | }
26 | },
27 | {
28 | path: '/public',
29 | name: 'public',
30 | component: Public,
31 | meta: {
32 | requireAuth: true
33 | }
34 | },
35 | {
36 | path: '/contact',
37 | name: 'contact',
38 | component: Contact,
39 | meta: {
40 | requireAuth: true
41 | }
42 | }
43 | ]
44 | },
45 | {
46 | path: '/index',
47 | name: 'index',
48 | component: Index
49 | }
50 | ]
51 | })
52 |
--------------------------------------------------------------------------------
/client/src/store/actions.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/client/src/store/actions.js
--------------------------------------------------------------------------------
/client/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import user from './modules/user'
4 | import token from './modules/token'
5 | import friend from './modules/friend'
6 | Vue.use(Vuex)
7 |
8 | const debug = process.env.NODE_ENV !== 'production'
9 |
10 | export default new Vuex.Store({
11 | modules: {
12 | user,
13 | token,
14 | friend
15 | },
16 | strict: debug
17 | })
18 |
--------------------------------------------------------------------------------
/client/src/store/modules/friend.js:
--------------------------------------------------------------------------------
1 | import * as types from '../mutation_types'
2 |
3 | // state
4 | const state = {
5 | nickname: '',
6 | id: '',
7 | avatar: '',
8 | loginStatus: false
9 | }
10 |
11 | const getters = {
12 | friendInfo: (state) => {
13 | return {
14 | nickname: state.nickname,
15 | id: state.id,
16 | avatar: state.avatar,
17 | loginStatus: state.loginStatus
18 | }
19 | }
20 | }
21 |
22 | // mutations
23 | const mutations = {
24 | [types.SAVE_FRIEND_INFO] (state, data) {
25 | state.nickname = data.nickname
26 | state.socketId = data.socketId
27 | state.id = data._id
28 | state.avatar = data.avatar
29 | state.loginStatus = state.loginStatus
30 | },
31 | [types.SET_FRIEND_LOGIN_STATUS] (state, data) {
32 | state.loginStatus = data
33 | }
34 | }
35 |
36 | export default {
37 | state,
38 | mutations,
39 | getters
40 | }
41 |
--------------------------------------------------------------------------------
/client/src/store/modules/token.js:
--------------------------------------------------------------------------------
1 | import * as types from '../mutation_types'
2 |
3 | // state
4 | const state = {
5 | token: localStorage.getItem('token')
6 | }
7 |
8 | // mutations
9 | const mutations = {
10 | [types.CREATE_TOKEN] (state, token) {
11 | state.token = token
12 | localStorage.setItem('token', token)
13 | },
14 | [types.DELETE_TOKEN] (state) {
15 | localStorage.removeItem('token')
16 | state.token = null
17 | }
18 | }
19 |
20 | export default {
21 | state,
22 | mutations
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/store/modules/user.js:
--------------------------------------------------------------------------------
1 | import * as types from '../mutation_types'
2 | const state = {
3 | userEmail: '',
4 | nickname: '',
5 | avatar: '',
6 | id: '',
7 | friends: {}
8 | }
9 |
10 | const getters = {
11 | userInfo: (state) => {
12 | return {
13 | nickname: state.nickname,
14 | userEmail: state.userEmail,
15 | avatar: state.avatar,
16 | id: state.id
17 | }
18 | },
19 | friendSockets: (state) => {
20 | return state.friends
21 | }
22 | }
23 |
24 | const actions = {
25 | }
26 |
27 | const mutations = {
28 | [types.SET_USER] (state, userinfo) {
29 | state.userEmail = userinfo.userEmail
30 | state.nickname = userinfo.nickname
31 | state.avatar = userinfo.avatar
32 | state.id = userinfo._id
33 | userinfo.friends.forEach(ele => {
34 | state.friends[ele._id] = ele.socketId
35 | })
36 | },
37 | [types.SET_USER_EMAIL] (state, data) {
38 | state.userEmail = data
39 | },
40 | [types.SET_USER_FRIEND] (state, data) {
41 | state.friends[data.userId] = data.socketId
42 | },
43 | [types.SET_USER_FRIENDS_LIST] (state, data) {
44 | data.forEach(ele => {
45 | state.friends[ele._id] = ele.socketId
46 | })
47 | }
48 | }
49 |
50 | export default {
51 | state,
52 | getters,
53 | mutations,
54 | actions
55 | }
56 |
--------------------------------------------------------------------------------
/client/src/store/mutation_types.js:
--------------------------------------------------------------------------------
1 | // token
2 | export const CREATE_TOKEN = 'CREATE_TOKEN'
3 | export const DELETE_TOKEN = 'DELETE_TOKEN'
4 | // user
5 | export const SET_USER = 'SET_USER'
6 | export const DELETE_USER = 'DELETE_USER'
7 | export const SET_USER_EMAIL = 'SET_USER_EMAIL'
8 | export const SET_USER_FRIEND = 'SET_USER_FRIEND'
9 | export const SET_USER_FRIENDS_LIST = 'SET_USER_FRIENDS_LIST'
10 | // chat
11 | export const SAVE_FRIEND_INFO = 'SAVE_FRIEND_INFO'
12 | export const SET_FRIEND_LOGIN_STATUS = 'SET_FRIEND_LOGIN_STATUS'
13 |
--------------------------------------------------------------------------------
/client/src/store/mutations.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/client/src/store/mutations.js
--------------------------------------------------------------------------------
/client/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/client/static/.gitkeep
--------------------------------------------------------------------------------
/client/static/config.js:
--------------------------------------------------------------------------------
1 | window.config = {
2 | baseUrl: '"http://enable.dpdaidai.top:7001"',
3 | socketHost: '"http://enable.dpdaidai.top:7001"'
4 | }
5 |
--------------------------------------------------------------------------------
/client/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "expect": true,
7 | "sinon": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/test/unit/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | Vue.config.productionTip = false
4 |
5 | // require all test files (files that ends with .spec.js)
6 | const testsContext = require.context('./specs', true, /\.spec$/)
7 | testsContext.keys().forEach(testsContext)
8 |
9 | // require all src files except main.js for coverage.
10 | // you can also change this to match only the subset of files that
11 | // you want coverage for.
12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
13 | srcContext.keys().forEach(srcContext)
14 |
--------------------------------------------------------------------------------
/client/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
1 | // This is a karma config file. For more details see
2 | // http://karma-runner.github.io/0.13/config/configuration-file.html
3 | // we are also using it with karma-webpack
4 | // https://github.com/webpack/karma-webpack
5 |
6 | var webpackConfig = require('../../build/webpack.test.conf')
7 |
8 | module.exports = function karmaConfig (config) {
9 | config.set({
10 | // to run in additional browsers:
11 | // 1. install corresponding karma launcher
12 | // http://karma-runner.github.io/0.13/config/browsers.html
13 | // 2. add it to the `browsers` array below.
14 | browsers: ['PhantomJS'],
15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
16 | reporters: ['spec', 'coverage'],
17 | files: ['./index.js'],
18 | preprocessors: {
19 | './index.js': ['webpack', 'sourcemap']
20 | },
21 | webpack: webpackConfig,
22 | webpackMiddleware: {
23 | noInfo: true
24 | },
25 | coverageReporter: {
26 | dir: './coverage',
27 | reporters: [
28 | { type: 'lcov', subdir: '.' },
29 | { type: 'text-summary' }
30 | ]
31 | }
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/client/test/unit/specs/HelloWorld.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import HelloWorld from '@/components/HelloWorld'
3 |
4 | describe('HelloWorld.vue', () => {
5 | it('should render correct contents', () => {
6 | const Constructor = Vue.extend(HelloWorld)
7 | const vm = new Constructor().$mount()
8 | expect(vm.$el.querySelector('.hello h1').textContent)
9 | .to.equal('Welcome to Your Vue.js App')
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/document/user.md:
--------------------------------------------------------------------------------
1 | # 用户操作接口文档
2 |
3 | ### 注册用户
4 |
5 | - `POST`,`/api/register`
6 |
7 | - 发送数据
8 |
9 | ```json
10 | {
11 | nickname: '...',
12 | password: '...',
13 | userEamil: '...'
14 | }
15 | ```
16 |
17 | - 返回数据
18 |
19 | ```json
20 | data: {user: {,…}, msg: "注册成功", code: 200}
21 | code: 200
22 | msg: "注册成功"
23 | user: {,…}
24 | avatar: "..."
25 | createTime: "2018-12-01T07:15:55.924Z"
26 | friends: []
27 | loginStatus: false
28 | nickname: "123456"
29 | password: "123456"
30 | requestFriends: []
31 | updateTime: "2018-12-01T07:15:55.924Z"
32 | userEmail: "123456"
33 | __v: 0
34 | _id: "5c0238bb8b64b151dac363a3"
35 | success: true
36 | ```
37 |
38 | ### 用户登录
39 |
40 | - `POST`,`/api/login`
41 |
42 | - 发送数据
43 |
44 | ```json
45 | {
46 | password: '...',
47 | userEamil: '...'
48 | }
49 | ```
50 |
51 | - 返回数据
52 |
53 | ```json
54 | code: 200
55 | msg: "登录成功"
56 | token: "..."
57 | user: {,…}
58 | avatar: "..."
59 | createTime: "2018-10-25T07:14:10.327Z"
60 | friends: []
61 | loginStatus: false
62 | nickname: "999999"
63 | password: "999999"
64 | requestFriends: []
65 | updateTime: "2018-10-25T07:14:10.327Z"
66 | userEmail: "999999"
67 | __v: 0
68 | _id: "5bd16d647b26f01491c8482c"
69 | success: true
70 | ```
71 |
72 | ### 获取用户信息
73 |
74 | - `GET`,`/api/getuserinfo`,头部带`token`,根据`token`中的数据获取`uid`,查找相应的数据
75 |
76 | - 返回数据
77 |
78 | ```json
79 |
80 | code: 200
81 | data: {,…}
82 | avatar: "..."
83 | createTime: "2018-10-25T07:14:10.327Z"
84 | friends: []
85 | loginStatus: false
86 | nickname: "999999"
87 | password: "999999"
88 | requestFriends: []
89 | updateTime: "2018-10-25T07:14:10.327Z"
90 | userEmail: "999999"
91 | __v: 0
92 | _id: "5bd16d647b26f01491c8482c"
93 | success: true
94 | ```
95 |
96 | ### 查找用户
97 |
98 | - `GET`,`/api/user/find/:findName`
99 |
100 | - 返回数据
101 |
102 | ```json
103 | code: 200
104 | data: [
105 | {avatar: '...', createTime: ..., nickname: ...},
106 | ....
107 | ]
108 | success: true
109 | ```
110 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 | vue-we-chat
--------------------------------------------------------------------------------
/public/static/config.js:
--------------------------------------------------------------------------------
1 | window.config = {
2 | baseUrl: '"http://enable.dpdaidai.top:7001"',
3 | socketHost: '"http://enable.dpdaidai.top:7001"'
4 | }
5 |
--------------------------------------------------------------------------------
/public/static/fonts/element-icons.6f0a763.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/public/static/fonts/element-icons.6f0a763.ttf
--------------------------------------------------------------------------------
/public/static/img/background.5a9adad.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/public/static/img/background.5a9adad.jpg
--------------------------------------------------------------------------------
/public/static/img/icon.3d77c66.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/public/static/img/icon.3d77c66.png
--------------------------------------------------------------------------------
/public/static/js/app.7ddf539c0082fd3a20ca.js:
--------------------------------------------------------------------------------
1 | webpackJsonp([1],{"0DzJ":function(t,e){},"1dPb":function(t,e){},"9fVX":function(t,e){},NHnr:function(t,e,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var i=s("xd7I"),n={render:function(){var t=this.$createElement,e=this._self._c||t;return e("div",{attrs:{id:"app"}},[e("router-view")],1)},staticRenderFns:[]};var a=s("C7Lr")({name:"App"},n,!1,function(t){s("cQZg")},null,null).exports,r=s("usm0"),c=s("4YfN"),o=s.n(c),l=s("R4Sj"),u={name:"Talk",components:{},computed:o()({},Object(l.b)(["friendInfo","userInfo","friendSockets"])),data:function(){return{preEditable:!1,chatContent:[]}},created:function(){},mounted:function(){this.sendMessageDiv=document.getElementById("sendMessage")},sockets:{chat:function(t){this.pushChatContentHandler(t,!1)},friendLogin:function(t){this.setUserFriend(t)}},methods:o()({sendChatMessage:function(){var t=this.sendMessageDiv.innerHTML;this.sendMessageDiv.innerHTML="",this.pushChatContentHandler(t,!0),this.$socket.emit("chat",{msg:t,socketId:this.friendSockets[this.friendInfo.userId]}),console.log("this.friendInfo.userId",this.friendInfo.userId,this.friendSockets)},pushChatContentHandler:function(t,e){this.chatContent.push({time:new Date,avatar:e?this.userInfo.avatar:this.friendInfo.avatar,message:t,isSend:e})}},Object(l.c)({setUserFriend:"SET_USER_FRIEND"})),watch:{friendInfo:{handler:function(t){this.chatContent=[]},deep:!0}}},d={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",[s("div",{ref:"wrapper",staticClass:"chat-box"},[s("div",{staticClass:"srcoll-wrapper"},t._l(t.chatContent,function(e){return s("div",{key:e.id},[s("div",{staticClass:"message",class:{you:!e.isSend,me:e.isSend}},[s("div",{staticClass:"message_system"},[s("div",{staticClass:"content"},[t._v(t._s(e.time))])]),t._v(" "),s("img",{staticClass:"avatar",attrs:{src:e.avatar}}),t._v(" "),s("div",{staticClass:"content"},[s("div",{staticClass:"bubble",class:{right:e.isSend,left:!e.isSend,bubble_primary:e.isSend,bubble_default:!e.isSend}},[s("div",{staticClass:"bubble_cont"},[s("div",{staticClass:"plain"},[s("pre",{staticClass:"message_plain"},[t._v(t._s(e.message))]),t._v(" "),s("img",{staticClass:"ico_loading",attrs:{src:""}})])])])])])])}),0)]),t._v(" "),s("div",{staticClass:"footer"},[t._m(0),t._v(" "),s("div",{staticClass:"content"},[s("pre",{staticClass:"flex",attrs:{contenteditable:t.preEditable,id:"sendMessage"},on:{click:function(e){t.preEditable=!0}}})]),t._v(" "),s("div",{staticClass:"action"},[s("span",{staticClass:"desc"},[t._v("按下Cmd+Enter换行")]),t._v(" "),s("a",{staticClass:"btn btn_send",attrs:{href:"javascript:;"},on:{click:t.sendChatMessage}},[t._v("发送")])])])])},staticRenderFns:[function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"toolbar"},[e("i",{staticClass:"emoji"}),this._v(" "),e("i",{staticClass:"screencut"}),this._v(" "),e("i",{staticClass:"file"})])}]};var f=s("C7Lr")(u,d,!1,function(t){s("1dPb")},"data-v-58c54149",null).exports,p={render:function(){var t=this.$createElement;return(this._self._c||t)("div")},staticRenderFns:[]};var h=s("C7Lr")({name:"Public",data:function(){return{}}},p,!1,function(t){s("wEMj")},"data-v-5fae7a2a",null).exports,v={render:function(){var t=this.$createElement;return(this._self._c||t)("div")},staticRenderFns:[]};var m=s("C7Lr")({name:"Contact",data:function(){return{}}},v,!1,function(t){s("tDZk")},"data-v-b8d4790a",null).exports,g={name:"Input",props:{tip:{type:String,default:""},type:{type:String,default:"text"}},data:function(){return{inputValue:"",isFocus:!1,tipString:"",placeholderStr:""}},created:function(){this.placeholderStr=this.tip,this.tipString=this.tip},methods:{focusHandler:function(){this.isFocus=!0,this.placeholderStr=""},blurHandler:function(){this.isFocus=!1,this.placeholderStr=this.tipString}}},_={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",{staticClass:"input-wrapper"},[s("div",{staticClass:"content-wrapper"},["checkbox"===t.type?s("input",{directives:[{name:"model",rawName:"v-model",value:t.inputValue,expression:"inputValue"}],staticClass:"input",attrs:{placeholder:t.placeholderStr,type:"checkbox"},domProps:{checked:Array.isArray(t.inputValue)?t._i(t.inputValue,null)>-1:t.inputValue},on:{focus:t.focusHandler,blur:t.blurHandler,change:function(e){var s=t.inputValue,i=e.target,n=!!i.checked;if(Array.isArray(s)){var a=t._i(s,null);i.checked?a<0&&(t.inputValue=s.concat([null])):a>-1&&(t.inputValue=s.slice(0,a).concat(s.slice(a+1)))}else t.inputValue=n}}}):"radio"===t.type?s("input",{directives:[{name:"model",rawName:"v-model",value:t.inputValue,expression:"inputValue"}],staticClass:"input",attrs:{placeholder:t.placeholderStr,type:"radio"},domProps:{checked:t._q(t.inputValue,null)},on:{focus:t.focusHandler,blur:t.blurHandler,change:function(e){t.inputValue=null}}}):s("input",{directives:[{name:"model",rawName:"v-model",value:t.inputValue,expression:"inputValue"}],staticClass:"input",attrs:{placeholder:t.placeholderStr,type:t.type},domProps:{value:t.inputValue},on:{focus:t.focusHandler,blur:t.blurHandler,input:function(e){e.target.composing||(t.inputValue=e.target.value)}}}),t._v(" "),t.isFocus?s("div",{staticClass:"tips"},[t._v(t._s(t.tipString))]):t._e()]),t._v(" "),t.isFocus?s("div",{staticClass:"bottom-line-blue"}):s("div",{staticClass:"bottom-line-grey"})])},staticRenderFns:[]};var C=s("C7Lr")(g,_,!1,function(t){s("f7Kz")},"data-v-76682d77",null).exports,k={name:"Button",props:{text:{type:String,default:""},type:{type:String,default:"primary"},long:{type:String,default:"short"}},data:function(){return{}},methods:{btnClickHandler:function(){this.$emit("btnClick")}}},b={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",{staticClass:"button-wrapper"},[s("button",{staticClass:"button",class:{long:"long"===t.long,"button-primary":"primary"===t.type,"button-submit":"submit"===t.type,"button-delete":"delete"===t.type},on:{click:t.btnClickHandler}},[s("span",{staticClass:"button-text"},[t._v(t._s(t.text))])])])},staticRenderFns:[]};var y=s("C7Lr")(k,b,!1,function(t){s("QjwS")},"data-v-397c79e4",null).exports,E={name:"Login",components:{MyInput:C,MyButton:y},data:function(){return{}},methods:{goRegister:function(){this.$emit("goRegister")},loginBtnClick:function(){this.$emit("loginBtnClick",{userEmail:this.$refs.email.inputValue,password:this.$refs.password.inputValue})}}},S={render:function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"form"},[e("div",{staticClass:"form-item"},[e("my-input",{ref:"email",attrs:{tip:"输入您的邮箱"}})],1),this._v(" "),e("div",{staticClass:"form-item"},[e("my-input",{ref:"password",attrs:{tip:"输入您的密码",type:"password"}})],1),this._v(" "),e("div",{staticClass:"form-item btn"},[e("my-button",{attrs:{text:"登录",type:"submit",long:"long"},on:{btnClick:this.loginBtnClick}})],1),this._v(" "),e("div",{staticClass:"form-item"},[e("div",{staticClass:"tip",on:{click:this.goRegister}},[this._v("没有账号?注册")])])])},staticRenderFns:[]};var I=s("C7Lr")(E,S,!1,function(t){s("QUqG")},"data-v-b6ff9396",null).exports,w={name:"Register",components:{MyInput:C,MyButton:y},data:function(){return{}},methods:{goLogin:function(){this.$emit("goLogin")},registerBtnClick:function(){var t={nickname:this.$refs.nickname.inputValue,userEmail:this.$refs.email.inputValue,password:this.$refs.password.inputValue};this.$emit("registerBtnClick",t)}}},R={render:function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"form register"},[e("div",{staticClass:"form-item"},[e("my-input",{ref:"nickname",attrs:{tip:"输入您的昵称"}})],1),this._v(" "),e("div",{staticClass:"form-item"},[e("my-input",{ref:"email",attrs:{tip:"输入您的邮箱"}})],1),this._v(" "),e("div",{staticClass:"form-item"},[e("my-input",{ref:"password",attrs:{tip:"输入您的密码",type:"password"}})],1),this._v(" "),e("div",{staticClass:"form-item btn"},[e("my-button",{attrs:{text:"注册",type:"submit",long:"long"},on:{btnClick:this.registerBtnClick}})],1),this._v(" "),e("div",{staticClass:"form-item"},[e("div",{staticClass:"tip register-tip",on:{click:this.goLogin}},[this._v("返回登录")])])])},staticRenderFns:[]};var L,T,x=s("C7Lr")(w,R,!1,function(t){s("ljba")},"data-v-fc1693da",null).exports,$=s("3cXf"),F=s.n($),H=s("ZLEe"),U=s.n(H),D=s("KH7x"),A=s.n(D),V=s("rVsN"),B=s.n(V),N=s("a3Yh"),j=s.n(N),M={state:{userEmail:"",nickname:"",avatar:"",id:"",friends:{}},getters:{userInfo:function(t){return{nickname:t.nickname,userEmail:t.userEmail,avatar:t.avatar,id:t.id}},friendSockets:function(t){return t.friends}},mutations:(L={},j()(L,"SET_USER",function(t,e){t.userEmail=e.userEmail,t.nickname=e.nickname,t.avatar=e.avatar,t.id=e._id,e.friends.forEach(function(e){t.friends[e._id]=e.socketId})}),j()(L,"SET_USER_EMAIL",function(t,e){t.userEmail=e}),j()(L,"SET_USER_FRIEND",function(t,e){t.friends[e.userId]=e.socketId}),j()(L,"SET_USER_FRIENDS_LIST",function(t,e){e.forEach(function(e){t.friends[e._id]=e.socketId})}),L),actions:{}},O={state:{token:localStorage.getItem("token")},mutations:(T={},j()(T,"CREATE_TOKEN",function(t,e){t.token=e,localStorage.setItem("token",e)}),j()(T,"DELETE_TOKEN",function(t){localStorage.removeItem("token"),t.token=null}),T)},P={state:{nickname:"",userId:"",avatar:"",loginStatus:!1},mutations:j()({},"SAVE_FRIEND_INFO",function(t,e){t.nickname=e.nickname,t.socketId=e.socketId,t.userId=e._id,t.avatar=e.avatar,t.loginStatus=t.loginStatus}),getters:{friendInfo:function(t){return{nickname:t.nickname,userId:t.userId,avatar:t.avatar,loginStatus:t.loginStatus}}}};i.default.use(l.a);var z=new l.a.Store({modules:{user:M,token:O,friend:P},strict:!1});function q(t){return B.a.all([t.status,t.statusText,t.json()])}function K(t){var e=A()(t,3),s=e[0],i=e[1],n=e[2];if(s>=200&&s<=300)return n;401===s&&z.commit("DELETE_TOKEN");var a=new Error(i);return a.status=s,a.errorMessage=n,B.a.reject(a)}s("WK9f").polyfill(),s("BUcb");var Q=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=new Headers(s);i.append("Accept","application/json"),z.state.token.token&&i.append("Authorization","Bearer "+z.state.token.token);var n=[];return U()(e).forEach(function(t){n.push(t+"="+encodeURIComponent(e[t]))}),t="http://enable.dpdaidai.top:7001"+(t+=n.length?"?"+n.join("&"):""),fetch(t,{method:"GET",headers:i,credential:"include",cache:"default",mode:"cors"}).then(q).then(K)},Y=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=new Headers(s);i.append("Content-Type","application/json"),i.append("Accept","application/json"),z.state.token.token&&i.append("Authorization","Bearer "+z.state.token.token);var n={method:"POST",headers:i,credential:"include",mode:"cors",body:F()(e)};return t="http://enable.dpdaidai.top:7001"+t,fetch(t,n).then(q).then(K)},G=function(t){return Y("/api/register",t)},W=function(t){return Y("/api/login",t)},Z=function(){return Q("/api/user/info")},J=function(t){return Q("/api/user/find/"+t)},X=function(t,e){return Y("/api/request/add/"+t,e)},tt=function(t){return Y("/api/friend/add/"+t)},et={name:"Index",components:{Login:I,Register:x},data:function(){return{tipStr:{nickname:"输入您的昵称",userEmail:"输入您的邮箱",password:"输入您的密码"},formType:"login"}},methods:o()({registerHandler:function(t){var e=this;G(t).then(function(t){if(t.success){var s=t.msg.indexOf("成功")>-1?"success":"info";e.messageTipHandler(t.msg,s)}}).catch(function(t){e.messageTipHandler(t.errorMessage&&t.errorMessage.message,"error")})},loginHandler:function(t){var e=this;W(t).then(function(s){s.success&&("登录成功"===s.msg?(e.createToken(s.data.token),e.setUserEmail(t.userEmail),e.$router.push("/talk")):e.messageTipHandler(s.msg,"error"))}).catch(function(t){e.messageTipHandler(t.errorMessage.message,"error")})},messageTipHandler:function(t,e){this.$message({message:t,type:e})}},Object(l.c)({createToken:"CREATE_TOKEN",setUserEmail:"SET_USER_EMAIL"}))},st={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",{staticClass:"wrapper"},[s("div",{staticClass:"card"},[s("h2",{staticClass:"title"},[t._v("VUE-WE-CHAT")]),t._v(" "),"login"===t.formType?s("login",{on:{goRegister:function(e){t.formType="register"},loginBtnClick:t.loginHandler}}):s("register",{on:{goLogin:function(e){t.formType="login"},registerBtnClick:t.registerHandler}})],1)])},staticRenderFns:[]};var it=s("C7Lr")(et,st,!1,function(t){s("i/o/")},"data-v-1833a5a8",null).exports,nt={name:"card",props:{searchResult:{type:Array,default:function(){return[]}},user:{type:Object,default:function(){return{nickname:"",userEmail:"",avatar:""}}},show:Boolean},data:function(){return{imgUrl:""}},created:function(){this.imgUrl="http://bpic.588ku.com/element_origin_min_pic/17/06/23/f21e1f3b279c62d6f3469ca6c84e638f.jpg"},methods:{goPage:function(t){this.$emit("go",t)},searchInput:function(){this.$emit("input",event.target.value)},clickUserHandler:function(t,e){this.$emit("click",t,e)}}},at={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",{staticClass:"card"},[s("div",{staticClass:"header"},[s("div",{staticClass:"avatar"},[s("img",{staticClass:"img",attrs:{src:t.user.avatar}})]),t._v(" "),s("div",{staticClass:"info"},[s("h3",{staticClass:"nickname"},[t._v(t._s(t.user.nickname))])])]),t._v(" "),s("div",{staticClass:"search"},[s("i",{staticClass:"search-icon"}),t._v(" "),s("input",{on:{input:t.searchInput}}),t._v(" "),t.show&&t.searchResult&&t.searchResult.length>0?s("div",{staticClass:"search-list"},[s("div",{staticClass:"list-wrapper"},t._l(t.searchResult,function(e,i){return s("div",{key:i,staticClass:"list-content"},[s("p",{staticClass:"list-title"},[t._v(t._s(e.value.length>0?e.type:"没有数据"))]),t._v(" "),t._l(e.value,function(e,i){return s("div",{key:i,staticClass:"item",on:{click:function(s){return t.clickUserHandler(e,"add")}}},[s("div",{staticClass:"avatar"},[s("img",{attrs:{src:e.avatar}})]),t._v(" "),s("div",{staticClass:"info"},[s("h4",{staticClass:"nickname"},[t._v(t._s(e.nickname))])])])})],2)}),0)]):t._e()]),t._v(" "),s("div",{staticClass:"tab"},[s("div",{staticClass:"tab-item",on:{click:function(e){return t.goPage("chat")}}},[s("i",{staticClass:"chat"})]),t._v(" "),s("div",{staticClass:"tab-item",on:{click:function(e){return t.goPage("public")}}},[s("i",{staticClass:"public"})]),t._v(" "),s("div",{staticClass:"tab-item",on:{click:function(e){return t.goPage("contact")}}},[s("i",{staticClass:"contact"})])])])},staticRenderFns:[]};var rt=s("C7Lr")(nt,at,!1,function(t){s("9fVX")},"data-v-41a11b5d",null).exports,ct=s("GRyI"),ot={name:"list",props:{lists:{type:Array,default:function(){return[]}},type:{type:String,default:"chat"},data:{type:Array,default:function(){return[]}},probeType:{type:Number,default:1},click:{type:Boolean,default:!0},listenScroll:{type:Boolean,default:!1},listenBeforeScroll:{type:Boolean,default:!1},listenScrollEnd:{type:Boolean,default:!1},direction:{type:String,default:"vertical"},scrollbar:{type:null,default:!1},pullDownRefresh:{type:null,default:!1},pullUpLoad:{type:null,default:!1},startY:{type:Number,default:0},refreshDelay:{type:Number,default:20},freeScroll:{type:Boolean,default:!1},mouseWheel:{type:Boolean,default:!1},bounce:{default:!0},zoom:{default:!1}},data:function(){return{scroll:""}},mounted:function(){var t=this;setTimeout(function(){t.initScroll()},20)},methods:{initScroll:function(){if(this.$refs.wrapper){var t={probeType:this.probeType,click:this.click,scrollY:this.freeScroll||"vertical"===this.direction,scrollX:this.freeScroll||"horizontal"===this.direction,scrollbar:this.scrollbar,pullDownRefresh:this.pullDownRefresh,pullUpLoad:this.pullUpLoad,startY:this.startY,freeScroll:this.freeScroll,mouseWheel:this.mouseWheel,bounce:this.bounce,zoom:this.zoom};this.scroll=new ct.a(this.$refs.wrapper,t)}},clickFriendHandler:function(t){this.$emit("click",t)}}},lt={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",{ref:"wrapper",staticClass:"list"},[s("div",{staticClass:"scroll-wrapper"},[s("p",{staticClass:"ico-loading"}),t._v(" "),"chat"===t.type?s("div",{staticClass:"chat"},[t.lists.length>0?s("div",t._l(t.lists,function(e,i){return s("div",{key:i,staticClass:"chat-item"},[s("div",{on:{click:function(s){return t.clickFriendHandler(e)}}},[s("div",{staticClass:"avatar"},[s("img",{staticClass:"img",attrs:{src:e.avatar}})]),t._v(" "),s("div",{staticClass:"info"},[s("h3",{staticClass:"nickname"},[s("span",{staticClass:"nickname-txt"},[t._v(t._s(e.nickname))])])])])])}),0):s("div",{staticClass:"no-data"},[t._v("\n 暂无数据\n ")])]):t._e(),t._v(" "),"contact"===t.type?s("div",{staticClass:"contact"},t._l(t.lists,function(e,i){return s("div",{key:i,staticClass:"contact-list"},[s("div",{staticClass:"letter-item"},[s("h4",{staticClass:"contact-title"},[t._v("\n "+t._s(e.letter)+"\n ")])]),t._v(" "),t._l(e.contact,function(e,n){return s("div",{key:i+"-"+n,staticClass:"contact-item"},[s("div",{staticClass:"avatar"},[s("img",{staticClass:"img",attrs:{src:e.avatar}})]),t._v(" "),s("div",{staticClass:"info"},[s("h4",{staticClass:"nickname"},[t._v(t._s(e.nickname))])])])})],2)}),0):t._e()])])},staticRenderFns:[]};var ut={name:"Layout",components:{Card:rt,List:s("C7Lr")(ot,lt,!1,function(t){s("NaCQ")},"data-v-f4cac7bc",null).exports},computed:o()({},Object(l.b)(["userInfo"])),data:function(){return{user:{nickname:"",userEmail:"",avatar:""},dialog:{title:"",visible:!1,type:"",content:""},listType:"chat",contactList:[],listData:[],preEditable:!1,searchResult:[],selectUser:"",clickUserData:"",title:"",showSearchList:!1}},created:function(){this.getUserInfo()},sockets:{connect:function(t){this.socketId=this.$socket.id,console.log("connect server: socketId",this.socketId)},login:function(t){console.log("client receive messgae : login: ",t)},addfriend:function(t){var e=this;console.log("client revice add friend request",t),this.$confirm(t.nickname+" 请求加你为好友,验证信息:"+t.message,"添加好友请求",{confirmButtonText:"确定",cancelButtonText:"取消"}).then(function(){tt(t.userId).then(function(t){e.$message({message:"好友添加成功",type:"success"})})})},friends:function(t){this.listData=JSON.parse(F()(t)),this.setUserFriendsList(t),this.clickFriendList(this.listData[0])}},mounted:function(){},methods:o()({goPage:function(t){this.$emit("go",t)},getUserInfo:function(){var t=this;Z().then(function(e){e.success&&(t.setUserInfo(e.data.user),t.listData=e.data.user.friends,t.$socket.emit("login",e.data.user),t.clickFriendList(t.listData[0]))})},findUserList:function(t){var e=this;t?J(t).then(function(t){t.success&&(e.searchResult=t.data,e.showSearchList=!0)}):this.searchResult=[]},searchListClick:function(t,e){this.clickUserData=t,this.dialog.type=e,this.dialog.title="add"===e?"请填写验证信息":"",this.dialog.visible=!0,this.dialog.content=""},dialogCloseHandler:function(){this.clickUserData="",this.dialog.content=""},clickFriendList:function(t){t&&(this.saveFriendInfo(t),this.title=t.nickname)},sendAddFriendHandler:function(){var t=this;this.dialog.visible=!1,this.showSearchList=!1,X(this.clickUserData._id,{message:this.dialog.content}).then(function(e){e.success&&t.$message({type:"success",message:e.msg})})}},Object(l.c)({setUserInfo:"SET_USER",saveFriendInfo:"SAVE_FRIEND_INFO",setUserFriendsList:"SET_USER_FRIENDS_LIST"}))},dt={render:function(){var t=this,e=t.$createElement,s=t._self._c||e;return s("div",{staticClass:"layout"},[s("div",{staticClass:"content-wrapper"},[s("div",{staticClass:"panel"},[s("card",{attrs:{user:t.userInfo,show:t.showSearchList,searchResult:t.searchResult},on:{go:t.goPage,input:t.findUserList,click:t.searchListClick}}),t._v(" "),s("list",{attrs:{lists:t.listData,type:t.listType},on:{click:t.clickFriendList}})],1),t._v(" "),s("div",{staticClass:"box"},[s("div",{staticClass:"box-header"},[s("div",{staticClass:"title-wrap"},[s("div",{staticClass:"title"},[t._v(t._s(t.title))])])]),t._v(" "),s("div",{staticClass:"box-body"},[s("router-view")],1),t._v(" "),s("div",{staticClass:"box-footer"},[t._t("footer")],2)])]),t._v(" "),s("el-dialog",{attrs:{title:t.dialog.title,visible:t.dialog.visible,width:"30%"},on:{"update:visible":function(e){return t.$set(t.dialog,"visible",e)},close:t.dialogCloseHandler}},[s("p",[t._v("添加\n "),s("span",{staticStyle:{"font-weight":"bolder","font-size":"18px"}},[t._v(t._s(t.clickUserData.nickname))]),t._v("\n 为好友\n ")]),t._v(" "),s("el-input",{attrs:{type:"textarea",rows:2,placeholder:"请输入内容"},model:{value:t.dialog.content,callback:function(e){t.$set(t.dialog,"content",e)},expression:"dialog.content"}}),t._v(" "),s("span",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[s("el-button",{on:{click:function(e){t.dialog.visible=!1}}},[t._v("取 消")]),t._v(" "),s("el-button",{attrs:{type:"primary"},on:{click:t.sendAddFriendHandler}},[t._v("确 定")])],1)],1)],1)},staticRenderFns:[]};var ft=s("C7Lr")(ut,dt,!1,function(t){s("0DzJ")},"data-v-7a06d674",null).exports;i.default.use(r.a);var pt=new r.a({routes:[{path:"/",component:ft,redirect:"/talk",children:[{path:"talk",name:"talk",component:f,meta:{requireAuth:!0}},{path:"/public",name:"public",component:h,meta:{requireAuth:!0}},{path:"/contact",name:"contact",component:m,meta:{requireAuth:!0}}]},{path:"/index",name:"index",component:it}]}),ht=s("Gir3"),vt=s.n(ht),mt=(s("hsAa"),s("EhIl")),gt=s.n(mt);i.default.use(vt.a),i.default.use(l.a),i.default.use(gt.a,"http://enable.dpdaidai.top:7001"),i.default.config.productionTip=!1,pt.beforeEach(function(t,e,s){t.meta.requireAuth?z.state.token.token?s():s({path:"/index"}):s()}),new i.default({el:"#app",router:pt,store:z,components:{App:a},template:""})},NaCQ:function(t,e){},QUqG:function(t,e){},QjwS:function(t,e){},cQZg:function(t,e){},f7Kz:function(t,e){},hsAa:function(t,e){},"i/o/":function(t,e){},ljba:function(t,e){},tDZk:function(t,e){},wEMj:function(t,e){}},["NHnr"]);
2 | //# sourceMappingURL=app.7ddf539c0082fd3a20ca.js.map
--------------------------------------------------------------------------------
/public/static/js/manifest.2ae2e69a05c33dfc65f8.js:
--------------------------------------------------------------------------------
1 | !function(r){var n=window.webpackJsonp;window.webpackJsonp=function(e,u,c){for(var f,i,p,a=0,l=[];a
8 |
9 | see [egg docs][egg] for more detail.
10 |
11 | ### Development
12 |
13 | ```bash
14 | $ npm i
15 | $ npm run dev
16 | $ open http://localhost:7001/
17 | ```
18 |
19 | ### Deploy
20 |
21 | ```bash
22 | $ npm start
23 | $ npm stop
24 | ```
25 |
26 | ### npm scripts
27 |
28 | - Use `npm run lint` to check code style.
29 | - Use `npm test` to run unit test.
30 | - Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail.
31 |
32 |
33 | [egg]: https://eggjs.org
34 |
35 | ## mongodb
36 |
37 | ### Development
38 | ```bash
39 | $ sudo mongod
40 | ```
--------------------------------------------------------------------------------
/server/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # example
2 |
3 |
4 |
5 | ## 快速入门
6 |
7 |
8 |
9 | 如需进一步了解,参见 [egg 文档][egg]。
10 |
11 | ### 本地开发
12 |
13 | ```bash
14 | $ npm i
15 | $ npm run dev
16 | $ open http://localhost:7001/
17 | ```
18 |
19 | ### 部署
20 |
21 | ```bash
22 | $ npm start
23 | $ npm stop
24 | ```
25 |
26 | ### 单元测试
27 |
28 | - [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。
29 | - 断言库非常推荐使用 [power-assert]。
30 | - 具体参见 [egg 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。
31 |
32 | ### 内置指令
33 |
34 | - 使用 `npm run lint` 来做代码风格检查。
35 | - 使用 `npm test` 来执行单元测试。
36 | - 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。
37 |
38 |
39 | [egg]: https://eggjs.org
40 |
--------------------------------------------------------------------------------
/server/app/controller/friend.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Controller = require('egg').Controller;
4 |
5 | class FriendController extends Controller {
6 | async addFriend() {
7 | const { ctx, service, app } = this;
8 | const nsp = app.io.of('/');
9 | const friendId = ctx.params.friendId
10 | const userId = ctx.token.uid;
11 | // 当前用户
12 | const currentUser = await service.user.findOneByUserId(userId);
13 | // 好友用户
14 | const addUser = await service.user.findOneByUserId(friendId)
15 | // 添加
16 | let retData = {};
17 | let currentUserFriendIds = currentUser.friends;
18 | let addUserFriendIds = addUser.friends;
19 | addUserFriendIds.push(userId)
20 | currentUserFriendIds.push(friendId);
21 | addUserFriendIds = Array.from(new Set(addUserFriendIds))
22 | currentUserFriendIds = Array.from(new Set(currentUserFriendIds))
23 | await service.user.updateOneUserInfo(addUser.userEmail, { friends: addUserFriendIds });
24 | await service.user.updateOneUserInfo(currentUser.userEmail, { friends: currentUserFriendIds });
25 | // 根据ids数组查找数据表中包含在其中数据
26 | const currentUserFriends = await service.user.findAllUsersById(currentUserFriendIds)
27 | const addUserFriends = await service.user.findAllUsersById(addUserFriendIds)
28 | nsp.to(currentUser.socketId).emit('friends', currentUserFriends)
29 | nsp.to(addUser.socketId).emit('friends', addUserFriends)
30 | retData = {
31 | code: 200,
32 | msg: '好友已添加',
33 | }
34 | resHandle(ctx, retData);
35 | }
36 | }
37 | function resHandle(ctx, res) {
38 | if (res.code === 200) {
39 | ctx.status = 200;
40 | ctx.body = {
41 | success: true,
42 | data: res.data,
43 | msg: res.msg,
44 | };
45 | } else {
46 | ctx.throw(res.code, res.msg);
47 | }
48 | }
49 | module.exports = FriendController;
50 |
--------------------------------------------------------------------------------
/server/app/controller/request.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Controller = require('egg').Controller;
4 | const jwt = require('jsonwebtoken');
5 | // const ms = require('ms');
6 |
7 | class RequestController extends Controller {
8 | async addUser() {
9 | const { ctx, service, app } = this;
10 | const nsp = app.io.of('/');
11 | const id = ctx.params.id
12 | const userId = ctx.token.uid;
13 | const { message } = ctx.request.body;
14 | // 当前用户
15 | const findUser = await service.user.findOneByUserId(userId);
16 | // 添加用户
17 | console.log('findUser', findUser)
18 | const addUser = await service.user.findOneByUserId(id);
19 | console.log('addUser', addUser)
20 | let retData = {}
21 | if (addUser.loginStatus) {
22 | nsp.to(addUser.socketId).emit('addfriend', {
23 | message,
24 | nickname: findUser.nickname,
25 | userId: findUser._id
26 | })
27 | // TODO 用户处于登录状态,则直接将请求信息通过websocket发送
28 | } else {
29 | await service.request.createAddRequest({ addUser: addUser._id, requestUser: findUser._id, message });
30 | }
31 | // await ctx.socket.emit('request', ' add friends' + addUser);
32 | retData = {
33 | code: 200,
34 | msg: '添加好友请求已发送,等待用户同意',
35 | }
36 | resHandle(ctx, retData);
37 | }
38 | }
39 | function resHandle(ctx, res) {
40 | if (res.code === 200) {
41 | ctx.status = 200;
42 | ctx.body = {
43 | success: true,
44 | data: res.data,
45 | msg: res.msg,
46 | };
47 | } else {
48 | ctx.throw(res.code, res.msg);
49 | }
50 | }
51 | module.exports = RequestController;
52 |
--------------------------------------------------------------------------------
/server/app/controller/talk.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Controller = require('egg').Controller;
4 |
5 | class TalkController extends Controller {
6 | async getAllNotReceiveTalk() {
7 | const { ctx, service } = this;
8 | const receiveId = ctx.params.receiveId;
9 | const talks = await service.talk.getAllNotReceiveTalkById(receiveId);
10 | // TODO: 更新信息状态
11 | // await service.talk.updateTalkHandler()
12 | let data = {}
13 | talks.forEach(ele => {
14 | if (data[ele.sendId]) {
15 | data[ele.sendId].push(ele);
16 | } else {
17 | data[ele.sendId] = [];
18 | }
19 | })
20 | let retData = {};
21 | if (talks) {
22 | retData = {
23 | code: 200,
24 | data,
25 | };
26 | } else {
27 | retData = {
28 | code: 401,
29 | msg: '没有未接收信息',
30 | };
31 | }
32 | resHandle(ctx, retData);
33 | }
34 | }
35 | function resHandle(ctx, res) {
36 | if (res.code === 200) {
37 | ctx.status = 200;
38 | ctx.body = {
39 | success: true,
40 | data: res.data,
41 | msg: res.msg,
42 | };
43 | } else {
44 | ctx.throw(res.code, res.msg);
45 | }
46 | }
47 | module.exports = TalkController;
48 |
--------------------------------------------------------------------------------
/server/app/controller/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Controller = require('egg').Controller;
4 | const jwt = require('jsonwebtoken');
5 | // const ms = require('ms');
6 |
7 | class UserController extends Controller {
8 |
9 | async create() {
10 | const { ctx, service } = this;
11 | const { userEmail } = ctx.request.body;
12 | const findUser = await service.user.findOneByUserEmail(userEmail);
13 | let retData = {};
14 | if (findUser) {
15 | retData = {
16 | code: 200,
17 | msg: '用户已存在,请登陆',
18 | };
19 | } else {
20 | await service.user.createUser(ctx.request.body);
21 | retData = {
22 | code: 200,
23 | msg: '用户创建成功,请登陆',
24 | };
25 | }
26 | resHandle(ctx, retData);
27 | }
28 | async login() {
29 | const { ctx, service, app } = this;
30 | const { userEmail, password } = ctx.request.body;
31 | const userLogin = await service.user.findOneByUserEmail(userEmail);
32 | let retData = {};
33 | if (userLogin) {
34 | if (password === userLogin.password) {
35 | const token = jwt.sign({
36 | uid: userLogin._id,
37 | exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60 * 7, // 过期时间为7天
38 | }, app.config.cert);
39 | await service.user.updateOneUserInfo(userLogin.userEmail, { loginStatus: true })
40 | retData = {
41 | msg: '登录成功',
42 | code: 200,
43 | data: {
44 | user: userLogin,
45 | token,
46 | },
47 | };
48 | userLogin.loginStatus = true;
49 | } else {
50 | retData = {
51 | code: 401,
52 | msg: '密码输入错误,请重新输入!',
53 | };
54 | }
55 | } else {
56 | retData = {
57 | code: 401,
58 | msg: '该用户不存在, 请注册!',
59 | };
60 | }
61 | resHandle(ctx, retData);
62 | }
63 | async getUserInfo() {
64 | const { ctx, service } = this;
65 | const userId = ctx.token.uid;
66 | const findUser = await service.user.findOneByUserId(userId);
67 | const friends = await service.user.findAllUsersById(findUser.friends)
68 | let retData = {
69 | code: 401,
70 | msg: '该用户不存在!',
71 | };
72 | delete findUser.friends
73 | findUser.friends = friends
74 | if (findUser) {
75 | retData = {
76 | code: 200,
77 | data: {
78 | user: findUser,
79 | },
80 | };
81 | }
82 | resHandle(ctx, retData);
83 | }
84 | async findUsers() {
85 | const { ctx, service } = this;
86 | const name = ctx.params.name;
87 | const findUsers = await service.user.findUsersByNickname(name);
88 | let retData = {
89 | code: 401,
90 | msg: '该用户不存在!',
91 | };
92 | if (findUsers) {
93 | retData = {
94 | code: 200,
95 | data: [
96 | {
97 | type: '用户',
98 | value: findUsers,
99 | },
100 | ],
101 | };
102 | }
103 | resHandle(ctx, retData);
104 | }
105 | // async requestFriend() {
106 | // const { ctx, service, app } = this;
107 | // const nsp = app.io.of('/');
108 | // const res = await service.user.requestFriend(ctx.request.body);
109 | // nsp.emit('request', res);
110 | // resHandle(ctx, res);
111 | // }
112 | }
113 | function resHandle(ctx, res) {
114 | if (res.code === 200) {
115 | ctx.status = 200;
116 | ctx.body = {
117 | success: true,
118 | data: res.data,
119 | msg: res.msg,
120 | };
121 | } else {
122 | ctx.throw(res.code, res.msg);
123 | }
124 | }
125 | module.exports = UserController;
126 |
--------------------------------------------------------------------------------
/server/app/io/controller/chat.js:
--------------------------------------------------------------------------------
1 | const Controller = require('egg').Controller;
2 |
3 | class ChatController extends Controller {
4 | async index() {
5 | const { ctx, service } = this;
6 | const data = ctx.args[0];
7 | if (data.loginStatus) {
8 | await ctx.socket.to(data.socketId).emit('chat', data.msg);
9 | } else {
10 | await service.talk.createTalkHandler(data);
11 | }
12 | }
13 | async disconnecting() {
14 | const { ctx } = this
15 | console.log('disconnecting', ctx.args[0]);
16 | }
17 | async disconnect() {
18 | const { ctx, service } = this;
19 | await service.user.logout();
20 | console.log('disconnet', ctx.args[0]);
21 | }
22 | }
23 | module.exports = ChatController;
24 |
--------------------------------------------------------------------------------
/server/app/io/controller/login.js:
--------------------------------------------------------------------------------
1 | const Controller = require('egg').Controller;
2 |
3 | class LoginController extends Controller {
4 | async index() {
5 | const { ctx, service } = this;
6 | const user = ctx.args[0];
7 | await service.user.updateOneUserInfo(user.userEmail, { socketId: ctx.socket.id, loginStatus: true });
8 | await ctx.socket.broadcast.emit('friendLogin', {
9 | userId: user._id,
10 | socketId: ctx.socket.id,
11 | });
12 | }
13 | }
14 | module.exports = LoginController;
15 |
--------------------------------------------------------------------------------
/server/app/io/middleware/auth.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | return async (ctx, next) => {
3 | ctx.socket.emit('connect', 'connected!');
4 | await next();
5 | // execute when disconnect.
6 | console.log('disconnection!');
7 | };
8 | };
--------------------------------------------------------------------------------
/server/app/middleware/verify_token.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const jwt = require('jsonwebtoken');
4 |
5 | module.exports = () => {
6 | return function* (next) {
7 | const cert = this.app.config.cert;
8 | const authorization = this.get('Authorization');
9 | if (authorization === '') {
10 | this.throw(401, 'no token');
11 | }
12 | const token = authorization.split(' ')[1];
13 | let tokenContent;
14 | try {
15 | tokenContent = jwt.verify(token, cert);
16 | } catch (err) {
17 | if (err.name === 'TokenExpiredError') {
18 | this.throw(401, 'token expired');
19 | }
20 | this.throw(401, 'invalid token');
21 | }
22 | this.token = tokenContent;
23 | yield next;
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/server/app/model/request.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | 'use strict';
4 | module.exports = app => {
5 | const mongoose = app.mongoose;
6 | const Schema = mongoose.Schema;
7 |
8 | const RequestSchema = new Schema({
9 | addUser: { type: String },
10 | requestUser: { type: String },
11 | requestTime: {
12 | type: Date,
13 | default: Date.now(),
14 | },
15 | status: {
16 | type: Number,
17 | default: 0
18 | },
19 | message: {
20 | type: String
21 | },
22 | read: {
23 | type: Boolean,
24 | default: false
25 | }
26 | });
27 |
28 | return mongoose.model('Request', RequestSchema);
29 | };
30 |
--------------------------------------------------------------------------------
/server/app/model/talk.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = app => {
3 | const mongoose = app.mongoose;
4 | const Schema = mongoose.Schema;
5 |
6 | const TalkSchema = new Schema({
7 | sendId: {
8 | type: String,
9 | },
10 | receiveId: {
11 | type: String,
12 | },
13 | message: {
14 | type: String,
15 | default: '',
16 | },
17 | receiveStatus: {
18 | type: Number,
19 | default: 0,
20 | },
21 | readStatus: {
22 | type: Number,
23 | default: 0,
24 | },
25 | createTime: {
26 | type: Date,
27 | default: Date.now(),
28 | },
29 | updateTime: {
30 | type: Date,
31 | default: Date.now(),
32 | },
33 | });
34 |
35 | return mongoose.model('Talk', TalkSchema);
36 | };
37 |
--------------------------------------------------------------------------------
/server/app/model/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = app => {
3 | const mongoose = app.mongoose;
4 | const Schema = mongoose.Schema;
5 |
6 | const UserSchema = new Schema({
7 | nickname: { type: String },
8 | // 用户邮箱唯一
9 | userEmail: { type: String },
10 | password: { type: String },
11 | socketId: {
12 | type: String,
13 | default: '',
14 | },
15 | avatar: {
16 | type: String,
17 | default: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3986557066,2648426149&fm=26&gp=0.jpg'
18 | },
19 | friends: {
20 | type: Array
21 | },
22 | loginStatus: {
23 | type: Boolean,
24 | default: false
25 | },
26 | createTime: {
27 | type: Date,
28 | default: Date.now(),
29 | },
30 | updateTime: {
31 | type: Date,
32 | default: Date.now(),
33 | },
34 | });
35 |
36 | return mongoose.model('User', UserSchema);
37 | };
38 |
--------------------------------------------------------------------------------
/server/app/router.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * @param {Egg.Application} app - egg application
5 | */
6 | module.exports = app => {
7 | const { router, controller, io } = app;
8 | const verifyToken = app.middlewares.verifyToken();
9 | router.post('register', '/api/register', controller.user.create);
10 | router.post('login', '/api/login', controller.user.login);
11 | router.get('getUserInfo', '/api/user/info', verifyToken, controller.user.getUserInfo);
12 | router.get('findUsers', '/api/user/find/:name', verifyToken, controller.user.findUsers);
13 | router.post('addUserRequest', '/api/request/add/:id', verifyToken, controller.request.addUser);
14 | router.post('addFriend', '/api/friend/add/:friendId', verifyToken, controller.friend.addFriend);
15 | router.get('getAllNotReceiveTalk', '/api/talk/all/:receiveId', verifyToken, controller.talk.getAllNotReceiveTalk)
16 | // router.post('requestUsers', '/api/user/request', controller.user.requestFriend);
17 | app.io.of('/').route('login', app.io.controller.login.index);
18 | app.io.of('/').route('chat', app.io.controller.chat.index);
19 | app.io.route('disconnecting', app.io.controller.chat.disconnecting);
20 | app.io.route('disconnect', app.io.controller.chat.disconnect);
21 |
22 | // io.route('server', io.controller.user.index);
23 | // io.route('request', io.controller.user.request);
24 | };
25 |
--------------------------------------------------------------------------------
/server/app/service/request.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Service = require('egg').Service;
3 |
4 | class RequestService extends Service {
5 | async createAddRequest (requestData) {
6 | const { ctx } = this;
7 | const Request = ctx.model.Request;
8 | const requestObj = new Request(requestData);
9 | await requestObj.save();
10 | }
11 | async getAllFriendRequest ({ userid }) {
12 | const { ctx } = this
13 | const result = await ctx.model.request.findAll({
14 | addUser: userid,
15 | status: 0
16 | })
17 | }
18 | }
19 | module.exports = RequestService;
20 |
--------------------------------------------------------------------------------
/server/app/service/talk.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Service = require('egg').Service;
3 | const NOT_FIND = 0
4 | class TalkService extends Service {
5 | /**
6 | * 创建离线信息
7 | * @param talkInfo
8 | * @returns {Promise}
9 | */
10 | async createTalkHandler(talkInfo) {
11 | const { ctx } = this;
12 | const Talk = ctx.model.Talk;
13 | const talkObj = new Talk(talkInfo);
14 | await talkObj.save();
15 | return '信息保存成功';
16 | }
17 | async getAllNotReceiveTalkById(receiveId, sendDataFormat = {}) {
18 | const { ctx } = this;
19 | const talks = await ctx.model.Talk.find({ receiveId }, sendDataFormat);
20 | console.log('talks', talks)
21 | if (talks) {
22 | return talks;
23 | }
24 | return NOT_FIND;
25 | }
26 | async updateTalkHandler(receiveId, sendId, updatedData) {
27 | const { ctx } = this
28 | return await ctx.model.Talk.update({ receiveId, sendId }, updatedData);
29 | }
30 | }
31 | module.exports = TalkService;
32 |
--------------------------------------------------------------------------------
/server/app/service/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const Service = require('egg').Service;
3 | const NOT_FIND = 0;
4 | class UserService extends Service {
5 | /**
6 | * 通过userEmail 查找用户
7 | * @param {string} userEmail 用户邮箱
8 | * @param {object} sendDataFormat 返回数据格式
9 | * @return {Promise<*>} 返回查找结果,没有结果则返回0
10 | */
11 | async findOneByUserEmail(userEmail, sendDataFormat = {}) {
12 | const { ctx } = this;
13 | const findUser = await ctx.model.User.findOne({ userEmail }, sendDataFormat);
14 | if (findUser) {
15 | return findUser;
16 | }
17 | return NOT_FIND;
18 | }
19 | /**
20 | * 通过userId 查找用户
21 | * @param {string} userId 用户id
22 | * @param {object} sendDataFormat 返回数据格式
23 | * @return {Promise<*>} 返回查找结果,没有结果则返回0
24 | */
25 | async findOneByUserId(userId, sendDataFormat = {}) {
26 | const { ctx } = this;
27 | const findUser = await ctx.model.User.findOne({ _id: userId }, sendDataFormat);
28 | if (findUser) {
29 | return findUser;
30 | }
31 | return NOT_FIND;
32 | }
33 | /**
34 | <<<<<<< HEAD
35 | * 通过userEmail 查找用户
36 | =======
37 | * 通过nickname 查找用户
38 | >>>>>>> ac2b20c17dd218bf5d923256dc437be6d7923fc7
39 | * @param {string} nickname 用户昵称
40 | * @param {object} sendDataFormat 返回数据格式
41 | * @return {Promise<*>} 返回查找结果,没有结果则返回0
42 | */
43 | async findUsersByNickname(nickname, sendDataFormat = {}) {
44 | const { ctx } = this;
45 | const findUser = await ctx.model.User.find({ nickname: { $regex: new RegExp(nickname) } }, sendDataFormat);
46 | if (findUser) {
47 | return findUser;
48 | }
49 | return NOT_FIND;
50 | }
51 | async updateOneUserInfo(userEmail, updatedData) {
52 | const { ctx } = this
53 | return await ctx.model.User.updateOne({ userEmail }, updatedData);
54 | }
55 | async findAllUsersById(ids, sendDataFormat = {}) {
56 | const { ctx } = this;
57 | const users = await ctx.model.User.find({ _id: { $in: ids } }, sendDataFormat);
58 | if (users) {
59 | return users;
60 | }
61 | return NOT_FIND;
62 | }
63 | /**
64 | * 创建用户
65 | * @param {object} userInfo 创建用户细腻些
66 | * @return {Promise<*>} 创建用户结果
67 | */
68 | async createUser(userInfo) {
69 | const { ctx } = this;
70 | const User = ctx.model.User;
71 | const userObj = new User(userInfo);
72 | await userObj.save();
73 | return '注册成功';
74 | }
75 |
76 | /**
77 | * 用户退出
78 | * @returns {Promise}
79 | */
80 | async logout() {
81 | const { ctx } = this;
82 | const socketId = ctx.socket.id;
83 | console.log('logout', socketId)
84 | await ctx.model.User.updateOne({ socketId }, { loginStatus: false });
85 | return '退出登录';
86 | }
87 | }
88 | module.exports = UserService;
89 |
--------------------------------------------------------------------------------
/server/appveyor.yml:
--------------------------------------------------------------------------------
1 | environment:
2 | matrix:
3 | - nodejs_version: '8'
4 |
5 | install:
6 | - ps: Install-Product node $env:nodejs_version
7 | - npm i npminstall && node_modules\.bin\npminstall
8 |
9 | test_script:
10 | - node --version
11 | - npm --version
12 | - npm run test
13 |
14 | build: off
15 |
--------------------------------------------------------------------------------
/server/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = appInfo => {
4 | const config = exports = {};
5 |
6 | // use for cookie sign key, should change to your own and keep security
7 | config.keys = appInfo.name + '_1533450168502_326';
8 |
9 | config.cert = 'vue_we_chat_by_dengpan_1533450168502_326';
10 |
11 | // add your config here
12 | config.middleware = [];
13 |
14 | config.security = {
15 | csrf: {
16 | enable: false,
17 | },
18 | domainWhiteList: [
19 | 'http://localhost:7001',
20 | 'http://127.0.0.1:7001',
21 | ],
22 | };
23 |
24 | config.cors = {
25 | credentials: true,
26 | origin: '*',
27 | allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
28 | };
29 |
30 | config.mongoose = {
31 | url: 'mongodb://127.0.0.1/vue-we-chat',
32 | options: {
33 | server: {
34 | auto_reconnect: true,
35 | poolSize: 10,
36 | },
37 | },
38 | };
39 | config.alinode = {
40 | // 从 `Node.js 性能平台` 获取对应的接入参数
41 | appid: 81909,
42 | secret: '24975000ac8261ac900f83b1aff27a6099b1c4ae',
43 | };
44 | return config;
45 | };
46 |
--------------------------------------------------------------------------------
/server/config/plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // had enabled by egg
4 | // exports.static = true;
5 | exports.mongoose = {
6 | enable: true,
7 | package: 'egg-mongoose',
8 | };
9 | exports.io = {
10 | enable: true,
11 | package: 'egg-socket.io',
12 | // init: { }, // passed to engine.io
13 | namespace: {
14 | '/': {
15 | connectionMiddleware: ['auth'],
16 | packetMiddleware: [],
17 | },
18 | },
19 | };
20 | exports.cors = {
21 | enable: true,
22 | package: 'egg-cors',
23 | };
24 | exports.alinode = {
25 | enable: true,
26 | package: 'egg-alinode',
27 | };
28 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "private": true,
6 | "dependencies": {
7 | "egg": "^2.2.1",
8 | "egg-alinode": "^2.0.1",
9 | "egg-cors": "^2.2.0",
10 | "egg-scripts": "^2.5.0"
11 | },
12 | "devDependencies": {
13 | "autod": "^3.0.1",
14 | "autod-egg": "^1.0.0",
15 | "egg-bin": "^4.3.5",
16 | "egg-ci": "^1.8.0",
17 | "egg-mock": "^3.14.0",
18 | "eslint": "^4.11.0",
19 | "eslint-config-egg": "^6.0.0",
20 | "webstorm-disable-index": "^1.2.0",
21 | "egg-mongoose": "^3.1.0",
22 | "jsonwebtoken": "^8.3.0",
23 | "egg-socket.io": "^4.1.5"
24 | },
25 | "engines": {
26 | "node": ">=8.9.0"
27 | },
28 | "scripts": {
29 | "start": "egg-scripts start --port=7001 --daemon --title=egg-server-example --sticky",
30 | "stop": "egg-scripts stop --title=egg-server-example",
31 | "dev": "egg-bin dev --sticky",
32 | "debug": "egg-bin debug --sticky",
33 | "test": "npm run lint -- --fix && npm run test-local",
34 | "test-local": "egg-bin test --full-trace",
35 | "cov": "egg-bin cov",
36 | "lint": "eslint .",
37 | "ci": "npm run lint && npm run cov",
38 | "autod": "autod"
39 | },
40 | "ci": {
41 | "version": "8"
42 | },
43 | "repository": {
44 | "type": "git",
45 | "url": ""
46 | },
47 | "author": "",
48 | "license": "MIT"
49 | }
50 |
--------------------------------------------------------------------------------
/server/test/app/controller/user.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { app, assert } = require('egg-mock/bootstrap');
4 |
5 | describe('test/app/controller/user.test.js', () => {
6 |
7 | it('should assert', function* () {
8 | const pkg = require('../../../package.json');
9 | assert(app.config.keys.startsWith(pkg.name));
10 |
11 | // const ctx = app.mockContext({});
12 | // yield ctx.service.xx();
13 | });
14 |
15 | it('should registered POST /api/register', () => {
16 | return app.httpRequest()
17 | .post('/api/register')
18 | .set('Content-Type', 'application/json')
19 | .send({
20 | nickname: '123',
21 | userEmail: '11111',
22 | password: '123',
23 | })
24 | .expect(200)
25 | .expect({
26 | code: '200',
27 | success: true,
28 | data: {
29 | msg: '该用户已注册,请登录',
30 | },
31 | });
32 | });
33 |
34 | it('should register POST /api/register', () => {
35 | return app.httpRequest()
36 | .post('/api/register')
37 | .set('Content-Type', 'application/json')
38 | .send({
39 | nickname: '123',
40 | userEmail: '1111111',
41 | password: '123',
42 | })
43 | .expect(200)
44 | .expect({
45 | code: '200',
46 | success: true,
47 | data: {
48 | nickname: '123',
49 | userEmail: '1111111',
50 | id: '5b76c82ec47f3439ed5677a5',
51 | msg: '注册成功'
52 | },
53 | });
54 | });
55 |
56 | it('should null POST /api/register', () => {
57 | return app.httpRequest()
58 | .post('/api/register')
59 | .set('Content-Type', 'application/json')
60 | .send({
61 | nickname: '123',
62 | userEmail: '',
63 | password: '123',
64 | })
65 | .expect(200)
66 | .expect({
67 | code: '200',
68 | success: true,
69 | data: {
70 | msg: '用户邮箱和密码不能为空',
71 | },
72 | });
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/server/test/app/service/user.test.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MyDAIDAI/vue-we-chat/eecd4dddac5105baf3fe31a44b7299abe01315f1/server/test/app/service/user.test.js
--------------------------------------------------------------------------------