├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── build ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── gulpfile.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── server ├── app.js ├── config │ ├── index.js │ ├── mongoose.js │ ├── statusCode.js │ └── winston.conf.js ├── controllers │ ├── user.controller.js │ └── webIssues.controller.js ├── index.js ├── middleware │ ├── authControl.js │ └── checkOrigin.js ├── models │ ├── commons │ │ └── counters.js │ ├── user.model.js │ └── webIssues.model.js ├── routers │ └── index.js ├── services │ ├── user.service.js │ └── webIssues.service.js └── utils │ ├── loghelper.js │ ├── sender.js │ └── util.js ├── src ├── entry │ ├── index.dev.js │ └── index.js ├── images │ └── logo.svg ├── libs │ └── index.js ├── routes │ ├── Account │ │ ├── account.scss │ │ └── changePwd.js │ ├── Auth │ │ ├── auth.scss │ │ ├── index.js │ │ ├── login.js │ │ ├── register.js │ │ └── resetPwd.js │ ├── IndexPage │ │ └── index.js │ ├── LayoutPage │ │ └── index.js │ ├── User │ │ ├── detail.js │ │ └── list.js │ ├── index.js │ ├── index.x.js │ └── routes.js ├── scss │ ├── animation.scss │ └── global.scss ├── utils │ ├── fetcher.js │ ├── storage.js │ ├── util.js │ └── xfetch.js └── views │ └── index.html ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", {"modules": false}], 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "react-hot-loader/babel", 9 | ["import", { "libraryName": "antd", "style": "css", "css": "true" }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "generator-star-spacing": [0], 6 | "consistent-return": [0], 7 | "react/forbid-prop-types": [0], 8 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }], 9 | "global-require": [1], 10 | "import/prefer-default-export": [0], 11 | "react/jsx-no-bind": [0], 12 | "react/prop-types": [0], 13 | "react/prefer-stateless-function": [0], 14 | "no-else-return": [0], 15 | "no-restricted-syntax": [0], 16 | "import/no-extraneous-dependencies": [0], 17 | "no-use-before-define": [0], 18 | "jsx-a11y/no-static-element-interactions": [0], 19 | "no-nested-ternary": [0], 20 | "arrow-body-style": [0], 21 | "import/extensions": [0], 22 | "no-bitwise": [0], 23 | "no-cond-assign": [0], 24 | "import/no-unresolved": [0], 25 | "require-yield": [1] 26 | }, 27 | "parserOptions": { 28 | "ecmaFeatures": { 29 | "experimentalObjectRestSpread": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | /node_modules 4 | /coverage 5 | /dist 6 | /logs 7 | 8 | .DS_Store 9 | .env 10 | .idea 11 | .vscode 12 | *.log 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Koa-Mongodb技术栈 2 | 3 | ## 安装依赖 4 | 5 | 如果本机没有安装过yarn,请先执行`npm install -g yarn`; 6 | 7 | ``` 8 | yarn install 9 | ``` 10 | 11 | **友情提示** 12 | 1. 建议设置包镜像源为国内的: 13 | 14 | ``` 15 | $ npm set registry https://registry.npm.taobao.org 16 | $ npm set disturl https://npm.taobao.org/dist 17 | $ npm set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass 18 | $ yarn config set registry https://registry.npm.taobao.org; 19 | ``` 20 | 21 | 2. 如果一直卡主无法成功安装完成,多半是因为`node-sass`资源被墙的问题,如: 22 | 23 | ``` 24 | ... 25 | [-/5] ⠠ waiting... 26 | [-/5] ⠠ waiting... 27 | [-/5] ⠠ waiting... 28 | [-/5] ⠠ waiting... 29 | [5/5] ⠠ node-sass: g++ '-DNODE_GYP_MODULE_NAME=libsass' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-D_ 30 | ``` 31 | 32 | 建议先取消安装,然后`cd node_modules/`,然后`rm -rf node-sass`,接着回到上级目录`cd ../`,执行: 33 | 34 | ``` 35 | yarn add node-sass sass-loader css-loader style-loader postcss-loader 36 | ``` 37 | 38 | ## 配置Nginx 39 | 40 | ``` 41 | server { 42 | listen 80; 43 | server_name cims.thinktxt.com; 44 | 45 | location / { 46 | proxy_pass http://127.0.0.1:8000; 47 | } 48 | } 49 | 50 | server { 51 | listen 80; 52 | server_name static.cims.thinktxt.com; 53 | 54 | location / { 55 | proxy_pass http://127.0.0.1:8088; 56 | } 57 | 58 | } 59 | ``` 60 | 61 | ## 绑定Host 62 | 63 | ``` 64 | 127.0.0.1 cims.thinktxt.com 65 | 127.0.0.1 static.cims.thinktxt.com 66 | ``` 67 | 68 | ## 本地开发构建 69 | 70 | 本地开发环境用`gulp server`命令。`gulp server`会启动一个基于内存的WEB服务器,端口为8088,同时监听(watch)本地文件的变化,然后自动编译。 71 | 72 | ``` 73 | gulp server 74 | ``` 75 | 76 | ## 启动Node服务 77 | 78 | ``` 79 | node server/index.js 80 | ``` 81 | 82 | 启动服务后在浏览器输入`http://cims.thinktxt.com`即可预览。 83 | 84 | 85 | ## 发布生产 86 | 87 | 正式环境的构建主要是增加了混淆、压缩的插件。 88 | 89 | ``` 90 | gulp release 91 | ``` 92 | 93 | 发布后可在`dist`目录查看所有静态资源 94 | 95 | 96 | ## 拓展服务配置 97 | 98 | ### 本地数据库(MongoDB) 99 | 100 | #### 安装 101 | 102 | * 打开终端,执行`brew install mongodb` 103 | 104 | * 在根目录 / 下创建 `data/db` 目录,用于放置`mongodb`数据,并且给该目录设置用户(txBoy)权限 105 | 106 | ```bash 107 | sudo mkdir -p /data/db 108 | sudo chown txBoy /data/db 109 | ``` 110 | 111 | #### 启动 112 | 113 | ``` 114 | mongod 115 | ``` 116 | 117 | 或者指定`数据库路径`和`端口`启动: 118 | 119 | ``` 120 | mongod --dbpath=/data/db --port=27017 121 | ``` 122 | 123 | ### MongoDB客户端 124 | 125 | 推荐MongoDB可视化操作客户端[Robomongo](https://robomongo.org/download) 126 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const { resolve } = require('path'); 3 | 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin; 7 | 8 | process.noDeprecation = true; 9 | 10 | module.exports = { 11 | //文件入口配置 12 | /*entry: { 13 | index: [ 14 | 'react-hot-loader/patch', 15 | 'webpack-dev-server/client?http://127.0.0.1:9000', 16 | 'webpack/hot/only-dev-server', 17 | './src/entry/index.js' 18 | ] 19 | },*/ 20 | //文件输出配置 21 | output: { 22 | path: resolve(__dirname, '../dist'), //打包输出目录 23 | }, 24 | // 声明CDN加载的库,不会通过webpack打包 25 | externals: { 26 | "react": 'React', 27 | "react-dom": "ReactDOM", 28 | "react-router": "ReactRouter", 29 | // 'history': "History", 30 | // 'redux': 'Redux', 31 | // 'react-redux': 'ReactRedux', 32 | // 'lodash': '_' 33 | }, 34 | //加载器配置 35 | module: { 36 | rules: [{ 37 | test: /\.js?$/, 38 | exclude: /(node_modules|bower_components)/, 39 | loader: "babel-loader", 40 | options: { 41 | cacheDirectory: true 42 | } 43 | }, { 44 | test: /\.(png|jpe?g|gif)$/i, 45 | loader: 'url-loader', 46 | options: { 47 | limit: 8192, 48 | name:'images/[name].[hash:8].[ext]' 49 | } 50 | }, { 51 | test: /\.(woff|woff2|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/i, 52 | use: [ 53 | { 54 | loader: 'file-loader', 55 | options: { 56 | name:'images/[name]-[hash:6].[ext]' 57 | } 58 | }] 59 | }] 60 | }, 61 | //插件项 62 | plugins: [ 63 | /*new webpack.ProvidePlugin({ 64 | 'Promise': 'bluebird', 65 | // 'React': 'react' 66 | }),*/ 67 | // new webpack.optimize.ModuleConcatenationPlugin(), // webpack 3 新增的作用域提升插件 68 | new CommonsChunkPlugin({ 69 | names:['manifest', 'vendor'].reverse(), 70 | minChunks: 2, 71 | }), 72 | new HtmlWebpackPlugin({ 73 | alwaysWriteToDisk: true, 74 | filename: 'index.html', 75 | template: './src/views/index.html', //html模板路径 76 | chunks: ['manifest', 'vendor', 'index'] // manifest: 可以理解为模块清单,载货单 77 | }) 78 | ], 79 | resolve: { 80 | extensions: ['.js', '.jsx', '.scss', '.css'], 81 | alias: { 82 | libs: resolve('./src/libs'), 83 | utils: resolve('./src/utils'), 84 | scss: resolve('./src/scss'), 85 | css: resolve('./src/css'), 86 | img: resolve('./src/images'), 87 | api: resolve('./src/api'), 88 | cpn: resolve('./src/components'), 89 | routes: resolve('./src/routes'), 90 | data: resolve('./src/data') 91 | }, 92 | modules: ['node_modules'] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const path = require("path"); 4 | 5 | const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); 6 | const InlineChunkWebpackPlugin = require('html-webpack-inline-chunk-plugin'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | 9 | const AggressiveMergingPlugin = webpack.optimize.AggressiveMergingPlugin; 10 | 11 | const webpackBaseConfig = require('./webpack.base.conf.js'); 12 | 13 | var buildInfo = { 14 | assets: true, // 增加资源信息 15 | assetsSort: "field", // 对资源按指定的项进行排序 16 | cached: true, // 增加缓存了的(但没构建)模块的信息 17 | children: true, // 增加子级的信息 18 | chunks: false, // 增加包信息(设置为 `false` 能允许较少的冗长输出) 19 | chunkModules: false, // 将内置模块信息增加到包信息 20 | chunkOrigins: false, // 增加包 和 包合并 的来源信息 21 | chunksSort: "field", // 对包按指定的项进行排序 22 | context: "../src/", // 用于缩短请求的上下文目录 23 | colors: true, // 等同于`webpack --colors` 24 | errors: true, // 增加错误信息 25 | errorDetails: true, // 增加错误的详细信息(就像解析日志一样) 26 | hash: true, // 增加编译的哈希值 27 | modules: false, // 增加内置的模块信息 28 | modulesSort: "field", // 对模块按指定的项进行排序 29 | publicPath: true, // 增加 publicPath 的信息 30 | reasons: true, // 增加模块被引入的原因 31 | source: true, // 增加模块的源码 32 | timings: true, // 增加时间信 33 | version: false, // 增加 webpack 版本信息 34 | warnings: true // 增加提示 35 | } 36 | 37 | module.exports = merge(webpackBaseConfig, { 38 | entry: { 39 | index: [ 40 | 'react-hot-loader/patch', 41 | 'webpack-dev-server/client?http://127.0.0.1:9000', 42 | 'webpack/hot/only-dev-server', 43 | './src/entry/index.dev.js' 44 | ], 45 | vendor: ['whatwg-fetch', 'es6-promise', 'fetch-detector'] 46 | }, 47 | output: { 48 | publicPath: '//static.cims.thinktxt.com/', //webpack-dev-server访问的路径 49 | filename: 'js/[name].[hash:8].js', 50 | chunkFilename: 'js/[id].chunk.[hash:8].js' 51 | }, 52 | devtool: '#eval-source-map', 53 | devServer: { 54 | hot: true, 55 | contentBase: path.join(__dirname, "dist"), 56 | compress: true, 57 | port: 8088, 58 | stats: buildInfo 59 | }, 60 | module: { 61 | rules: [{ 62 | test: /\.(css|scss|sass)$/, 63 | use: [ 64 | { loader: "style-loader" }, 65 | { loader: "css-loader" }, 66 | { loader: "postcss-loader" }, 67 | { loader: "sass-loader" } 68 | ] 69 | /*use: ExtractTextPlugin.extract({ 70 | fallback: 'style-loader', 71 | use: [ 72 | { 73 | loader: 'css-loader' 74 | }, { 75 | loader: 'postcss-loader' 76 | }, { 77 | loader: 'sass-loader' 78 | }] 79 | })*/ 80 | }] 81 | }, 82 | //插件项 83 | plugins: [ 84 | new webpack.HotModuleReplacementPlugin(), // enable HMR globally 85 | new webpack.NamedModulesPlugin(), // prints more readable module names in the browser console on HMR updates 86 | 87 | new webpack.HashedModuleIdsPlugin(), //稳定chunkhash 88 | new AggressiveMergingPlugin(), //Merge chunks 89 | new HtmlWebpackHarddiskPlugin(), 90 | new InlineChunkWebpackPlugin({ 91 | inlineChunks: ['manifest'] 92 | }), 93 | /*new ExtractTextPlugin({ 94 | filename: 'css/[name].[hash:8].css' 95 | }),*/ 96 | new webpack.LoaderOptionsPlugin({ 97 | // test: /\.xxx$/, // may apply this only for some modules 98 | options: { 99 | hot: true, 100 | proxy: { 101 | "/api": "http://172.3.1.33:8086" 102 | } 103 | } 104 | }) 105 | ] 106 | }) 107 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const merge = require('webpack-merge'); 3 | const path = require("path"); 4 | 5 | const autoprefixer = require('autoprefixer'); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | 8 | // const CleanWebpackPlugin = require('clean-webpack-plugin'); 9 | 10 | const InlineChunkWebpackPlugin = require('html-webpack-inline-chunk-plugin'); 11 | const AggressiveMergingPlugin = webpack.optimize.AggressiveMergingPlugin; 12 | 13 | const webpackBaseConfig = require('./webpack.base.conf.js'); 14 | 15 | module.exports = merge(webpackBaseConfig, { 16 | //文件入口配置 17 | entry: { 18 | index: './src/entry/index.js', 19 | vendor: ['whatwg-fetch', 'es6-promise', 'fetch-detector'] 20 | }, 21 | //文件输出配置 22 | output: { 23 | publicPath: '/', 24 | filename: 'js/[name].[chunkhash:8].js', 25 | chunkFilename: 'js/[id].chunk.[chunkhash:8].js' 26 | }, 27 | module: { 28 | rules: [{ 29 | test: /\.(css|scss|sass)$/, 30 | use: [ 31 | { loader: "style-loader" }, 32 | { 33 | loader: "css-loader", 34 | options: { 35 | minimize: true 36 | } 37 | }, 38 | { loader: "postcss-loader" }, 39 | { loader: "sass-loader" } 40 | ] 41 | }] 42 | }, 43 | devtool: false, 44 | //插件项 45 | plugins: [ 46 | new webpack.HashedModuleIdsPlugin(), //稳定chunkhash 47 | new AggressiveMergingPlugin(), //Merge chunks 48 | new InlineChunkWebpackPlugin({ 49 | inlineChunks: ['manifest'] 50 | }), 51 | /*new ExtractTextPlugin({ 52 | filename: 'css/[name].[contenthash:8].css' 53 | }),*/ 54 | new webpack.LoaderOptionsPlugin({ 55 | options: { 56 | postcss: function() { 57 | return [autoprefixer]; 58 | } 59 | } 60 | }), 61 | /*new CleanWebpackPlugin(['dist'], { 62 | root: path.resolve(__dirname, '../'), 63 | verbose: true, 64 | dry: false, 65 | // exclude: ['shared.js'] 66 | }),*/ 67 | new webpack.optimize.UglifyJsPlugin({ 68 | compress: { 69 | warnings: false, 70 | drop_debugger: true, 71 | drop_console: true 72 | } 73 | }), 74 | new webpack.DefinePlugin({ 75 | "process.env": { 76 | NODE_ENV: JSON.stringify("production") 77 | } 78 | }) 79 | ] 80 | }) 81 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const gutil = require('gulp-util'); 3 | 4 | const webpack = require('webpack'); 5 | const webpackDevServer = require('webpack-dev-server'); 6 | 7 | var devConfig = require('./build/webpack.dev.conf.js'); 8 | var prodConfig = require('./build/webpack.prod.conf.js'); 9 | 10 | var port = devConfig.devServer.port; 11 | 12 | var buildInfo = { 13 | assets: true, // 增加资源信息 14 | assetsSort: "field", // 对资源按指定的项进行排序 15 | cached: false, // 增加缓存了的(但没构建)模块的信息 16 | children: false, // 增加子级的信息 17 | chunks: false, // 增加包信息(设置为 `false` 能允许较少的冗长输出) 18 | chunkModules: false, // 将内置模块信息增加到包信息 19 | chunkOrigins: false, // 增加包 和 包合并 的来源信息 20 | chunksSort: "field", // 对包按指定的项进行排序 21 | context: "../src/", // 用于缩短请求的上下文目录 22 | colors: true, // 等同于`webpack --colors` 23 | errors: true, // 增加错误信息 24 | errorDetails: true, // 增加错误的详细信息(就像解析日志一样) 25 | hash: true, // 增加编译的哈希值 26 | modules: false, // 增加内置的模块信息 27 | modulesSort: "field", // 对模块按指定的项进行排序 28 | publicPath: true, // 增加 publicPath 的信息 29 | reasons: true, // 增加模块被引入的原因 30 | source: true, // 增加模块的源码 31 | timings: true, // 增加时间信 32 | version: false, // 增加 webpack 版本信息 33 | warnings: true // 增加提示 34 | } 35 | 36 | // 开发环境服务 37 | gulp.task('server', function(callback) { 38 | var compiler = webpack(devConfig); 39 | var serverConfig = { 40 | hot: true, 41 | contentBase: 'dist', 42 | stats: buildInfo 43 | } 44 | new webpackDevServer(compiler, serverConfig).listen(port, "127.0.0.1", function(err) { 45 | if (err) throw new gutil.PluginError("webpack-dev-server", err); 46 | // Server listening 47 | gutil.log("[webpack-dev-server]", 48 | "http://127.0.0.1:" + port); 49 | }); 50 | }); 51 | 52 | // 打包发布 53 | gulp.task('release', function(callback) { 54 | return webpack(prodConfig, function(err, stats) { 55 | if (err) throw new gutil.PluginError("webpack", err); 56 | gutil.log("[webpack]", stats.toString(buildInfo)); 57 | callback(); 58 | }); 59 | }); 60 | 61 | gulp.task('default', ['server'], function() { 62 | console.log( 'default task...' ); 63 | }); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ant-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "antd": "^2.8.2", 7 | "autoprefixer": "^6.7.7", 8 | "axios": "^0.16.1", 9 | "babel-core": "^6.24.0", 10 | "babel-loader": "^6.4.1", 11 | "babel-plugin-import": "^1.1.1", 12 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", 13 | "babel-plugin-transform-runtime": "^6.23.0", 14 | "babel-polyfill": "^6.23.0", 15 | "babel-preset-es2015": "^6.24.0", 16 | "babel-preset-react": "^6.23.0", 17 | "babel-preset-stage-0": "^6.22.0", 18 | "babel-preset-stage-3": "^6.22.0", 19 | "babel-register": "^6.24.0", 20 | "babel-runtime": "^6.23.0", 21 | "bcryptjs": "^2.4.3", 22 | "bluebird": "^3.5.0", 23 | "css-loader": "^0.28.0", 24 | "es6-promise": "^4.1.0", 25 | "extract-text-webpack-plugin": "^2.1.0", 26 | "fetch-detector": "^1.0.1", 27 | "gulp": "^3.9.1", 28 | "gulp-util": "^3.0.8", 29 | "html-webpack-harddisk-plugin": "^0.1.0", 30 | "html-webpack-inline-chunk-plugin": "^1.1.1", 31 | "html-webpack-plugin": "^2.28.0", 32 | "image-webpack-loader": "^3.2.0", 33 | "jsonwebtoken": "^7.3.0", 34 | "koa": "^2.2.0", 35 | "koa-body": "^1.6.0", 36 | "koa-router": "^7.1.1", 37 | "koa-session": "4.0.1", 38 | "koa-views": "^6.0.1", 39 | "lazy-route": "^1.0.7", 40 | "log4js": "^1.1.1", 41 | "mongoose": "^4.9.5", 42 | "node-sass": "^4.5.2", 43 | "postcss-load-config": "^1.2.0", 44 | "postcss-loader": "^1.3.3", 45 | "react": "^15.4.2", 46 | "react-dom": "^15.4.2", 47 | "react-hot-loader": "next", 48 | "react-router": "^4.0.0", 49 | "react-router-config": "^1.0.0-beta.1", 50 | "react-router-dom": "^4.0.0", 51 | "sass-loader": "^6.0.3", 52 | "style-loader": "^0.16.1", 53 | "svg-sprite-loader": "^0.3.0", 54 | "svg-url-loader": "^2.0.2", 55 | "webpack": "^2.2.1", 56 | "webpack-dev-server": "^2.4.2", 57 | "webpack-merge": "^4.1.0", 58 | "whatwg-fetch": "^2.0.3" 59 | }, 60 | "devDependencies": { 61 | "react-scripts": "0.9.5" 62 | }, 63 | "scripts": { 64 | "start": "react-scripts start", 65 | "build": "react-scripts build", 66 | "test": "react-scripts test --env=jsdom", 67 | "eject": "react-scripts eject" 68 | }, 69 | "theme": { 70 | "primary-color": "#1DA57A" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Joyotang/React-Koa-Mongodb/aff8ddac9c2065e26619fc5d700af3a7ae61cc52/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React App 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: Koa App.js 3 | * @author: txBoy 4 | * @created 2017-03-31 5 | */ 6 | "use strict"; 7 | import Koa from 'koa'; 8 | import Router from 'koa-router'; 9 | import koaBody from 'koa-body'; 10 | import views from 'koa-views'; 11 | import path from 'path'; 12 | import queryString from 'querystring'; 13 | import {config, logConfig} from './config'; 14 | import sender from './utils/sender'; 15 | import appRoutes from './routers'; 16 | import mongo from './config/mongoose'; 17 | import checkOrigin from './middleware/checkOrigin'; 18 | import { refreshToken } from './middleware/authControl'; 19 | 20 | // logger 21 | import loghelper from './utils/loghelper'; 22 | const logger = loghelper('server'); 23 | 24 | const app = new Koa(); 25 | const router = new Router(); 26 | const port = process.env.PORT || 8000; 27 | 28 | app.use(views(path.resolve(__dirname, '../dist'), { 29 | extension: 'html' 30 | })) 31 | 32 | app.use(koaBody()); 33 | 34 | var db = mongo(); 35 | db.connection.on("error", function (error) { 36 | console.log("------数据库连接失败:" + error); 37 | }); 38 | db.connection.on("open", function () { 39 | console.log("------数据库连接成功!------"); 40 | }); 41 | 42 | app.use(async (ctx, next) => { 43 | var path = ctx.path; 44 | if (path.startsWith('/api')) { 45 | console.log('-----api-----') 46 | await refreshToken(ctx, next) 47 | } else { 48 | console.log('-----index-----') 49 | await ctx.render('index', { 50 | data: 'txBoy' 51 | }); 52 | } 53 | }) 54 | 55 | appRoutes(app, router); // app routes config. 56 | 57 | app.listen(port, () => { 58 | console.log('Server started on port ' + port); 59 | }) 60 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | const config = { 4 | host: '127.0.0.1', 5 | dbHost: '127.0.0.1', 6 | dbPort: '27017', 7 | dbName: 'cims', 8 | mongodb: 'mongodb://127.0.0.1:27017/cims', 9 | cookieSecret: 'cimsCookie', 10 | jwt: { 11 | secret: 'vimplus' 12 | } 13 | } 14 | 15 | // Config for log4js. 16 | const logConfig = { 17 | appenders: [{ 18 | "type": "dateFile", 19 | "filename": resolve(__dirname, "../../logs/server.log"), 20 | "pattern": "-yyyy-MM-dd.log", 21 | "category": "server" 22 | }, { 23 | "type": "dateFile", 24 | "filename": resolve(__dirname, "../../logs/mongodb.log"), 25 | "pattern": "-yyyy-MM-dd.log", 26 | "category": "mongodb" 27 | }] 28 | } 29 | 30 | export {config, logConfig} 31 | -------------------------------------------------------------------------------- /server/config/mongoose.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { config } from './index'; 3 | 4 | export default function () { 5 | mongoose.Promise = global.Promise; 6 | let db = mongoose.connect(config.mongodb); 7 | return db; 8 | } 9 | -------------------------------------------------------------------------------- /server/config/statusCode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 0* 通用错误 4 | const common = { 5 | success: { 6 | code: 10000, 7 | msg: '请求成功!', 8 | status: 'success' 9 | }, 10 | error: { 11 | code: 99999, 12 | msg: 'System Error!', 13 | status: 'error' 14 | }, 15 | addFail: { 16 | code: 99999, 17 | msg: '新增失败!', 18 | status: 'error' 19 | }, 20 | updateFail: { 21 | code: 99999, 22 | msg: '更新失败!', 23 | status: 'error' 24 | }, 25 | deleteFail: { 26 | code: 99999, 27 | msg: '删除失败!', 28 | status: 'error' 29 | }, 30 | none: { 31 | code: 90005, 32 | msg: '参数不能为空!', 33 | status: 'error' 34 | }, 35 | noType: { 36 | code: 90005, 37 | msg: '类型不能为空!', 38 | status: 'error' 39 | }, 40 | noneId: { 41 | code: 90006, 42 | msg: 'id不能为空!', 43 | status: 'error' 44 | }, 45 | missParam: { 46 | code: 90007, 47 | msg: '请检查所需的参数!', 48 | status: 'error' 49 | }, 50 | notLogin: { 51 | code: 88888, 52 | msg: '未登录或登录超时,请重新登录!', 53 | status: 'error' 54 | }, 55 | invalidToken: { 56 | code: 88888, 57 | msg: 'Invalid Token!', 58 | status: 'error' 59 | }, 60 | errorToken: { 61 | code: 88888, 62 | msg: 'Token verify failed!', 63 | status: 'error' 64 | }, 65 | timeOut: { 66 | code: 50004, 67 | msg: '处理超时!', 68 | status: 'error' 69 | }, 70 | notPermission: { 71 | code: 50005, 72 | msg: '您暂时无权进行此操作,请联系管理员授权!', 73 | status: 'error' 74 | } 75 | } 76 | 77 | // 1* 为用户报错信息 78 | const account = { 79 | notInfo: { 80 | code: 10001, 81 | msg: '用户名或密码不能为空!', 82 | status: 'error' 83 | }, 84 | dupName: { 85 | code: 10002, 86 | msg: '用户名已存在!', 87 | status: 'error' 88 | }, 89 | notUser: { 90 | code: 10003, 91 | msg: '用户不存在!', 92 | status: 'error' 93 | }, 94 | pwdError: { 95 | code: 10004, 96 | msg: '用户名或密码错误!', 97 | status: 'error' 98 | }, 99 | noAccess: { 100 | code: 10005, 101 | msg: '暂无访问权限!', 102 | status: 'error' 103 | }, 104 | oldPwdErr: { 105 | code: 10006, 106 | msg: '原密码输入错误!' 107 | }, 108 | notMatch: { 109 | code: 10007, 110 | msg: '用户名和密码不匹配,请核对!', 111 | }, 112 | forbidden: { 113 | code: 10008, 114 | msg: '该用户已被限制登录,详情咨询管理员!', 115 | } 116 | } 117 | 118 | export { 119 | common, 120 | account 121 | } 122 | -------------------------------------------------------------------------------- /server/config/winston.conf.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import 'winston-daily-rotate-file'; 3 | 4 | winston.loggers.add('server', { 5 | console: { 6 | level: 'info', 7 | colorize: true, 8 | label: 'Server-log' 9 | }, 10 | file: { 11 | filename: 'logs/server.log' 12 | } 13 | }); 14 | 15 | var transport = new winston.transports.DailyRotateFile({ 16 | filename: 'logs/log', 17 | datePattern: 'yyyy-MM-dd.', 18 | prepend: true, 19 | level: process.env.ENV === 'development' ? 'debug' : 'info' 20 | }); 21 | 22 | const logConfig = { 23 | transports: [ 24 | transport 25 | ] 26 | } 27 | 28 | export { 29 | logConfig 30 | }; 31 | -------------------------------------------------------------------------------- /server/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: user controller - Provide user related controller. 3 | * @author: txBoy 4 | * @created 2017-04-17 5 | */ 6 | 7 | import jwt from 'jsonwebtoken'; 8 | import { md5 } from '../utils/util'; 9 | import { config } from '../config'; 10 | import { common } from '../config/statusCode'; 11 | import { generateToken } from '../middleware/authControl'; 12 | import userService from '../services/user.service'; 13 | 14 | var user = { 15 | register: async function (ctx, next) { 16 | var userInfo = ctx.request.body; 17 | var data = await userService.register(userInfo); 18 | if (data) { 19 | ctx.body = Object.assign({data: data}, common.success); 20 | } else { 21 | ctx.body = Object.assign({data: null}, common.error); 22 | } 23 | }, 24 | login: async function (ctx, next) { 25 | var userInfo = ctx.request.body; 26 | console.log('Req:', userInfo) 27 | var res = await userService.login(userInfo); 28 | console.log('loginData:', res); 29 | 30 | if (res && res.data) { 31 | var tokenInfo = {username: res.data.username}; 32 | var token = generateToken(tokenInfo); 33 | ctx.cookies.set('token', token); 34 | ctx.body = res; 35 | } else { 36 | ctx.body = res; 37 | } 38 | }, 39 | logout: async function (ctx, next) { 40 | ctx.cookies.set('token', ''); 41 | ctx.body = Object.assign({data: true}, common.success); 42 | }, 43 | getList: async function (ctx, next) { 44 | var params = ctx.query; 45 | params.filter = data && params.filter ? JSON.parse(params.filter) : {}; 46 | var data = await userService.getUsers(params); 47 | if (data) { 48 | ctx.body = Object.assign({data: data}, common.success); 49 | } else { 50 | ctx.body = Object.assign({data: null}, common.error); 51 | } 52 | } 53 | } 54 | 55 | export default user; 56 | -------------------------------------------------------------------------------- /server/controllers/webIssues.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: webIssues controller - Provide webIssues related controller. 3 | * @author: txBoy 4 | * @created 2017-07-04 5 | */ 6 | 7 | import { common } from '../config/statusCode'; 8 | import webIssuesService from '../services/webIssues.service'; 9 | import checkOrigin from '../middleware/checkOrigin'; 10 | 11 | var webIssues = { 12 | push: async function (ctx, next) { 13 | var data = ctx.request.body; 14 | // var data = ctx.query; 15 | console.log('------data--------', data) 16 | var doc = await webIssuesService.add(data); 17 | var origin = ctx.headers.origin; 18 | console.log('------origin:',origin); 19 | 20 | if (checkOrigin(origin)) { 21 | ctx.set('Access-Control-Allow-Origin', origin); 22 | ctx.set('Access-Control-Allow-Credentials', true); 23 | ctx.set('Access-Control-Allow-Headers', 'x-requested-with,content-type'); 24 | } 25 | 26 | if (doc) { 27 | var res = Object.assign({data: null}, common.success); 28 | res.data = doc; 29 | ctx.response.body = res; 30 | } else { 31 | var res = Object.assign({data: null}, common.error); 32 | ctx.response.body = res; 33 | } 34 | } 35 | } 36 | 37 | export default webIssues; 38 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | 2 | var register = require('babel-register'); 3 | 4 | register({ 5 | presets: ['es2015', 'stage-3'] 6 | }); 7 | require('babel-polyfill'); 8 | require('./app.js'); 9 | global.navigator = { userAgent: 'all' } 10 | -------------------------------------------------------------------------------- /server/middleware/authControl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: Authentication - Verify user token. 3 | * @author: txBoy 4 | * @created 2017-04-17 5 | */ 6 | 7 | import jwt from 'jsonwebtoken'; 8 | import { config } from '../config'; 9 | import { common } from '../config/statusCode'; 10 | 11 | // logger 12 | import loghelper from '../utils/loghelper'; 13 | const logger = loghelper('server'); 14 | 15 | const expiredTime = 60 * 30; // 30min 16 | const sKeys = config.jwt.secret; 17 | 18 | function generateToken(data) { 19 | var token = jwt.sign(data, sKeys, {expiresIn: expiredTime}); 20 | return token; 21 | } 22 | 23 | function handleTokenError(ctx, msg) { 24 | ctx.cookies.set('token', ''); 25 | switch (msg) { 26 | case 'jwt expired': 27 | ctx.body = Object.assign({data: null}, common.notLogin); 28 | break; 29 | case 'invalid token': 30 | ctx.body = Object.assign({data: null}, common.invalidToken); 31 | break; 32 | case 'jwt malformed': 33 | ctx.body = Object.assign({data: null}, common.errorToken); 34 | break; 35 | default: 36 | ctx.body = Object.assign({data: null}, common.errorToken); 37 | } 38 | } 39 | 40 | async function authVerify(ctx, next) { 41 | const token = ctx.cookies.get('token'); 42 | if (!token) { 43 | ctx.body = Object.assign({data: null}, common.notLogin); 44 | return; 45 | } 46 | try { 47 | var tokenContent = await jwt.verify(token, sKeys); 48 | console.log('----------verify tokenContent:', tokenContent); 49 | console.log("Authentication success!"); 50 | await next(); 51 | 52 | } catch(err) { 53 | logger.error('---------------Verify Error:', err); 54 | if (err && err.message) { 55 | handleTokenError(ctx, err.message); 56 | } else { 57 | ctx.body = Object.assign({data: null}, common.error); 58 | } 59 | } 60 | } 61 | 62 | 63 | async function refreshToken(ctx, next) { 64 | var token = ctx.cookies.get('token'); 65 | if (token) { 66 | try { 67 | var tokenContent = await jwt.verify(token, sKeys); 68 | delete tokenContent.iat; 69 | delete tokenContent.exp; 70 | 71 | var newToken = jwt.sign(tokenContent, sKeys, {expiresIn: expiredTime}); 72 | ctx.cookies.set('token', newToken); // 更新token, 延长过期时间 73 | 74 | await next(); 75 | } catch (err) { 76 | logger.error('---------------RefreshToken Error:', err); 77 | if (err && err.message) { 78 | handleTokenError(ctx, err.message); 79 | } else { 80 | ctx.body = Object.assign({data: null}, common.error); 81 | } 82 | } 83 | } else { 84 | await next(); 85 | } 86 | } 87 | 88 | export default authVerify; 89 | 90 | export { 91 | generateToken, 92 | authVerify, 93 | refreshToken 94 | }; 95 | -------------------------------------------------------------------------------- /server/middleware/checkOrigin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: Check allow origin. 3 | * @author: txBoy 4 | * @created 2017-07-05 5 | */ 6 | 7 | function checkOrigin(origin) { 8 | var whiteList = [ 9 | 'http://lms-dev.yylending.com', 10 | 'http://127.0.0.1:4000' 11 | ] 12 | if (whiteList.indexOf(origin) != -1) { 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | export default checkOrigin; 19 | -------------------------------------------------------------------------------- /server/models/commons/counters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: Auto increment id. - Provide Auto increment id for mongodb. 3 | * @author: txBoy 4 | * @created 2017-07-05 5 | */ 6 | 7 | import mongoose from 'mongoose'; 8 | const Schema = mongoose.Schema; 9 | 10 | var CounterSchema = Schema({ 11 | tableName: {type: String, required: true}, 12 | seq: { type: Number, default: 1 } 13 | }); 14 | var Counter = mongoose.model('cims_counters', CounterSchema); 15 | 16 | async function getIncrementId(tableName) { 17 | try { 18 | var counter = await Counter.findOneAndUpdate({tableName: tableName}, {$inc: { seq: 1} }); 19 | if (counter) { 20 | return counter && counter.seq; 21 | } 22 | } catch (err) { 23 | throw err; 24 | return null; 25 | } 26 | } 27 | 28 | export default getIncrementId; 29 | -------------------------------------------------------------------------------- /server/models/user.model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: user model. - Provide user related data Schema. 3 | * @author: txBoy 4 | * @created 2017-04-17 5 | */ 6 | 7 | import mongoose from 'mongoose'; 8 | const Schema = mongoose.Schema; 9 | 10 | // 定义数据库中的集合名称(相当于 MySQL 数据库中的表名),如果有则保存到该集合,如果没有则创建名为 cims_users 的集合后保存数据。 11 | const COLLECTTION = 'cims_users'; 12 | 13 | var CounterSchema = Schema({ 14 | _id: {type: String, required: true}, 15 | seq: { type: Number, default: 1 } 16 | }); 17 | var Counter = mongoose.model('counters', CounterSchema); 18 | 19 | // 定义user的数据模型。 20 | var UserSchema = new Schema({ 21 | uid: { type: Number }, 22 | username: { 23 | type: String, 24 | unique: true, 25 | trim: true, 26 | required: true, 27 | index: true 28 | }, 29 | name: String, 30 | nickname: String, 31 | email: { 32 | type: String, 33 | validate: { 34 | validator: function(email) { 35 | return /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(email); 36 | }, 37 | message: '{VALUE} is not a valid email!' 38 | } 39 | }, 40 | password: { 41 | type: String, 42 | required: true 43 | }, 44 | state: { 45 | type: String, 46 | enum: ["single", "inlove"] 47 | }, 48 | gender: { 49 | type: Number, 50 | enum: [1, 2, 3], 51 | default: 3 52 | }, 53 | phoneNo: String, 54 | accessToken: String, 55 | avatar: String, 56 | location: String, 57 | url: String, 58 | signature: String, 59 | createTime: { 60 | type: Number, 61 | default: Date.now() 62 | }, 63 | updateTime: Number 64 | }); 65 | 66 | UserSchema.set('autoIndex', true); 67 | 68 | // 根据Schema创建一个Model 69 | var UserModel = mongoose.model(COLLECTTION, UserSchema); 70 | 71 | export default UserModel; 72 | -------------------------------------------------------------------------------- /server/models/webIssues.model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: Web-issues model. - Provide Web-issues related data Schema. 3 | * @author: txBoy 4 | * @created 2017-07-04 5 | */ 6 | 7 | import mongoose from 'mongoose'; 8 | const Schema = mongoose.Schema; 9 | 10 | const COLLECTTION = 'cims_web_issues'; 11 | 12 | // 定义数据模型。 13 | var issuesSchema = new Schema({ 14 | logId: Number, 15 | title: String, 16 | category: String, 17 | browserType: String, 18 | osType: String, 19 | environments: Object, 20 | userInfo: Object, 21 | stack: String, 22 | targetUrl: String, 23 | status: Number, 24 | crashCount: Number, 25 | createdTime: Number, 26 | updateTime: Number, 27 | lastCrashedTime: Number 28 | }); 29 | 30 | // 根据Schema创建一个Model 31 | var issuesModel = mongoose.model(COLLECTTION, issuesSchema); 32 | 33 | export default issuesModel; 34 | -------------------------------------------------------------------------------- /server/routers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview koa-router config. 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | 7 | import sender from '../utils/sender'; 8 | import authVerify from '../middleware/authControl'; 9 | import user from '../controllers/user.controller'; 10 | import webIssues from '../controllers/webIssues.controller'; 11 | 12 | function appRoutes(app, router) { 13 | app.use(router.routes()).use(router.allowedMethods()); 14 | 15 | router.post('/api/user/register', user.register); 16 | router.post('/api/user/login', user.login); 17 | router.get('/api/user/getUsers', authVerify, user.getList); 18 | router.get('/api/user/logout', user.logout); 19 | 20 | router.get('/api/webIssues/push', webIssues.push); 21 | router.post('/api/webIssues/push', webIssues.push); 22 | 23 | } 24 | 25 | export default appRoutes; 26 | -------------------------------------------------------------------------------- /server/services/user.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: user service - Provide user related database operations services. 3 | * @author: txBoy 4 | * @created 2017-04-17 5 | */ 6 | 7 | import mongoose from 'mongoose'; 8 | 9 | import { md5 } from '../utils/util'; 10 | import { config } from '../config'; 11 | import { common, account } from '../config/statusCode'; 12 | import UserModel from '../models/user.model'; 13 | import getIncrementId from '../models/commons/counters'; 14 | 15 | // logger 16 | import loghelper from '../utils/loghelper'; 17 | const logger = loghelper('mongodb'); 18 | 19 | const COLLECTTION = 'cims_users'; 20 | const User = mongoose.model(COLLECTTION); 21 | 22 | var userService = { 23 | register: async function (info) { 24 | try { 25 | var uid = await getIncrementId(COLLECTTION); 26 | if (uid) { 27 | info.uid = uid; 28 | info.updateTime = Date.now(); 29 | var doc = await User.create(info); 30 | if (doc) { 31 | return { 32 | username: doc.username, 33 | email: doc.email 34 | } 35 | } 36 | } else { 37 | return null; 38 | } 39 | } catch (err) { 40 | logger.error('---------------The register action DB-Error:', err); 41 | console.log('---------------The register action DB-Error:', err); 42 | return null; 43 | } 44 | }, 45 | login: async function (info) { 46 | try { 47 | var doc = await User.findOne({username: info.username}); 48 | if (doc) { 49 | var res = null; 50 | if (info.password === doc.password) { 51 | var data = { 52 | uid: doc.uid, 53 | username: doc.username, 54 | email: doc.email 55 | } 56 | res = Object.assign({data: data}, common.success); 57 | } else { 58 | res = Object.assign({data: null}, account.pwdError); 59 | } 60 | return res; 61 | } else { 62 | var res = Object.assign({data: null}, account.notUser); 63 | return res; 64 | } 65 | } catch (err) { 66 | logger.error('---------------The login action DB-Error:', err); 67 | console.log('---------------The login action DB-Error:', err); 68 | var res = Object.assign({data: null}, common.error); 69 | return res; 70 | } 71 | }, 72 | getUsers: async function (params) { 73 | // 页码查询参数 74 | var page = Number(params && params.page) || 1; 75 | var size = Number(params && params.size) || 10; 76 | var total = 0; 77 | // 筛选条件组装 78 | var { searchValue } = params.filter; 79 | var filter = {}; 80 | if (searchValue) { filter.$or = [{username: new RegExp(searchValue, 'i')}, {realName: new RegExp(searchValue, 'i')}] } // 根据[用户名]或[姓名]模糊查询 81 | 82 | try { 83 | total = await User.count(filter); 84 | var userList = await User.find(filter, null, {skip: (page - 1) * size, limit: size}); 85 | if (userList) { 86 | var list = []; 87 | for (var i = 0; i < userList.length; i++) { 88 | var obj = {}; 89 | var user = userList[i]; 90 | obj.id = user._id; 91 | obj.uid = user.uid; 92 | obj.username = user.username; 93 | obj.email = user.email; 94 | obj.gender = user.gender; 95 | obj.createTime = user.createTime; 96 | list.push(obj); 97 | } 98 | return { 99 | list: list, 100 | page: page, 101 | size: size, 102 | total: total 103 | } 104 | } 105 | } catch (err) { 106 | logger.error('---------------The getUsers action DB-Error:', err); 107 | return null; 108 | } 109 | } 110 | } 111 | 112 | export default userService; 113 | -------------------------------------------------------------------------------- /server/services/webIssues.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: webIssues service - Provide webIssues related database operations services. 3 | * @author: txBoy 4 | * @created 2017-07-04 5 | */ 6 | 7 | import mongoose from 'mongoose'; 8 | import { config } from '../config'; 9 | import WebIssuesModel from '../models/webIssues.model'; 10 | import getIncrementId from '../models/commons/counters'; 11 | 12 | // logger 13 | import loghelper from '../utils/loghelper'; 14 | const logger = loghelper('mongodb'); 15 | 16 | const COLLECTTION = 'cims_web_issues'; 17 | const WebIssues = mongoose.model(COLLECTTION); 18 | 19 | var webIssuesService = { 20 | add: async function (info) { 21 | try { 22 | info.updateTime = Date.now(); 23 | // console.log('-------info:', info) 24 | var doc = await WebIssues.findOne({title: info.title}); 25 | if (doc) { 26 | info.lastCrashedTime = doc.updateTime; 27 | var result = await WebIssues.updateOne({title: info.title}, {$set: info}); 28 | if (result && result.n) { 29 | var updatedDoc = await WebIssues.findOne({title: info.title}); 30 | // console.log('-------updatedDoc:', updatedDoc) 31 | return updatedDoc; 32 | } else { 33 | return null; 34 | } 35 | } else { 36 | info.createdTime = Date.now(); 37 | var logId = await getIncrementId(COLLECTTION); 38 | if (logId) { 39 | info.logId = logId; 40 | doc = await WebIssues.create(info); 41 | console.log('-------doc:', doc) 42 | return doc; 43 | } 44 | } 45 | } catch (err) { 46 | logger.error('---------------The webIssues add action DB-Error:', err); 47 | console.log('DB-Error:', err) 48 | return null; 49 | } 50 | } 51 | } 52 | 53 | export default webIssuesService; 54 | -------------------------------------------------------------------------------- /server/utils/loghelper.js: -------------------------------------------------------------------------------- 1 | /**************************************************************** 2 | * Purpose: handle log4js. 3 | * 4 | * @author: Tower. 5 | * @time: 2017-05-23. 6 | */ 7 | 8 | import { logConfig } from '../config'; 9 | import log4js from 'log4js'; 10 | log4js.configure(logConfig); 11 | 12 | let loggersMap = {}; 13 | const CATEGORY = 'category'; 14 | const APPENDERS = 'appenders'; 15 | 16 | function isEmptyObject(o) { 17 | for (let k in o) return false; 18 | return true; 19 | }; 20 | 21 | if (isEmptyObject(loggersMap)) { 22 | logConfig && logConfig[APPENDERS].forEach(function(appender) { 23 | if (appender.hasOwnProperty(CATEGORY)) { 24 | loggersMap[appender[CATEGORY]] = log4js.getLogger(appender[CATEGORY]); 25 | } 26 | }); 27 | } 28 | 29 | /* 30 | Purpose: logger public module 31 | @param {Object} category 32 | @return {Object} logger 33 | @example 34 | const logger = require('xx/utils/loghelper')('yourCategory'); 35 | logger.error('log error'); 36 | logger.info('log info'); 37 | */ 38 | export default function(category) { 39 | if (!category || category.trim() == '') { 40 | let stack = new Error().stack; 41 | let callLocation = stack.split('at')[2].split('/')[stack.split('at')[2].split('/').length-1].replace(/\)/g, ''); 42 | console.error(`Missing loghelper init params: category, at: ${callLocation}`); 43 | loggersMap['server'] && loggersMap['server'].error(new Error('Missing loghelper init params: category')); 44 | } 45 | if (!loggersMap.hasOwnProperty(category)) { 46 | console.error(`Can't find logger instance: ${category}`); 47 | loggersMap['server'] && loggersMap['server'].error(new Error(`Can't find logger instance: ${category}`)); 48 | } 49 | 50 | return loggersMap[category]; 51 | }; 52 | -------------------------------------------------------------------------------- /server/utils/sender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview: sender 3 | * @author: txBoy 4 | * @created 2017-04-07 5 | */ 6 | 7 | import axios from 'axios'; 8 | // import xRequest from './xRequest'; 9 | 10 | 11 | var sender = {}; 12 | 13 | 14 | sender.get = function (url, options) { 15 | options.method = 'GET'; 16 | options.params = options.data || null; 17 | return this.request(url, options); 18 | } 19 | 20 | 21 | sender.post = function (url, options) { 22 | options.method = 'POST'; 23 | return this.request(url, options); 24 | } 25 | 26 | sender.request = function (url, options) { 27 | var options = Object.assign({}, options); 28 | options.url = url; 29 | return xRequest(options); 30 | } 31 | 32 | function xRequest(options) { 33 | return axios(options); 34 | } 35 | 36 | export default sender; 37 | -------------------------------------------------------------------------------- /server/utils/util.js: -------------------------------------------------------------------------------- 1 | 2 | import crypto from 'crypto'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | /** 7 | * md5正常字符串加密 8 | */ 9 | function md5(str) { 10 | var ret = crypto.createHash('md5').update(str.toString()).digest('hex'); 11 | return ret; 12 | } 13 | 14 | /** 15 | * md5中文字符串加密 16 | * @desc 解决中文加密不同的问题 17 | */ 18 | function md5Pay(str) { 19 | str = (new Buffer(str)).toString("binary"); 20 |   var ret = crypto.createHash('md5').update(str).digest("hex"); 21 |   return ret; 22 | } 23 | 24 | /** 25 | * aes 加密 26 | */ 27 | function encryptByAES(key, iv, data) { 28 | var cipher = crypto.createCipheriv('aes-128-cbc', key, iv); 29 | var crypted = cipher.update(data, 'utf8', 'binary'); 30 | crypted += cipher.final('binary'); 31 | crypted = new Buffer(crypted, 'binary').toString('base64'); 32 | return crypted; 33 | } 34 | 35 | /** 36 | * aes 解密 37 | */ 38 | function decryptByAES(key, iv, crypted) { 39 | crypted = new Buffer(crypted, 'base64').toString('binary'); 40 | var decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); 41 | var decoded = decipher.update(crypted, 'binary', 'utf8'); 42 | decoded += decipher.final('utf8'); 43 | return decoded; 44 | } 45 | 46 | /** 47 | * 格式化时间戳 - 时间毫秒数转化为固定格式的时间字符串 48 | * @param timestamp 49 | * @param dateFormate 50 | * @description 其中dateFormate是有YYYY MM DD hh mm ss组成,可以是YYYY-MM-DD 也可以是YYYY-MM-DD hh:mm:ss 51 | * 如果不传的话,默认为: YYYY-MM-DD hh:mm:ss 52 | * @example formatTimestamp(1464314911403); 53 | * formatTimestamp(1464314911403, 'yyyyMMddhhmmss'); 54 | * formatTimestamp(1464314911403,'YYYY-MM-DD'); 55 | */ 56 | function formatTimestamp(timestamp, dateFormate) { 57 | var datemills = parseInt(timestamp); 58 | if (isNaN(datemills)) { 59 | console.error("Wrong timestamp, must be a number!"); 60 | return; 61 | } 62 | if (dateFormate == undefined) { 63 | dateFormate = "YYYY-MM-DD hh:mm:ss"; 64 | } 65 | var date = new Date(datemills); 66 | var year = "" + date.getFullYear(); 67 | var month = (date.getMonth() > 8 ? (date.getMonth() + 1) : "0" + (date.getMonth() + 1)); 68 | var day = date.getDate() > 9 ? date.getDate() : "0" + date.getDate(); 69 | var hour = date.getHours() > 9 ? date.getHours() : "0" + date.getHours(); 70 | var minute = date.getMinutes() > 9 ? date.getMinutes() : "0" + date.getMinutes(); 71 | var second = date.getSeconds() > 9 ? date.getSeconds() : "0" + date.getSeconds(); 72 | var datestr = dateFormate.replace(/yyyy/gi, year).replace('MM', month).replace(/dd/gi, day) 73 | .replace("hh", hour).replace("mm", minute).replace("ss", second); 74 | return datestr; 75 | } 76 | 77 | /** 78 | * 对象升序排序 79 | * @desc 对象按参数名称(key)进行升序排序 80 | * @param {Object} 81 | * @return {Object} 82 | */ 83 | function sortObj(obj) { 84 | var keys = Object.keys(obj); 85 | keys.sort(); 86 | var res = {}; 87 | for (var i = 0; i < keys.length; i++) { 88 | var _key = keys[i]; 89 | res[_key] = obj[_key]; 90 | } 91 | return res; 92 | } 93 | 94 | /** 95 | * 分转元 96 | */ 97 | function pennyToYuan(penny) { 98 | return (parseInt(penny) / 100).toFixed(2); 99 | }; 100 | 101 | /** 102 | * 金额格式化 103 | * @desc 将数值四舍五入(保留2位小数)后格式化成金额形式 104 | * @param sum 数值(Number或者String) 105 | * @return {String} 金额格式的字符串,如'1,234,567.45' 106 | */ 107 | function formatCurrency(sum) { 108 | if (!sum) return 0; 109 | var num = pennyToYuan(sum); 110 | num = num.toString().replace(/\$|\,/g, ''); 111 | if(isNaN(num)) { 112 | num = "0"; 113 | } 114 | var sign = (num == (num = Math.abs(num)) ); 115 | num = Math.floor(num * 100 + 0.50000000001); 116 | var cents = num % 100; 117 | num = Math.floor(num / 100).toString(); 118 | 119 | cents = cents < 10 ? '0' + cents : cents; 120 | 121 | for (var i = 0; i < Math.floor( (num.length-(1+i)) / 3 ); i++) { 122 | num = num.substring(0, num.length-(4*i+3)) + ',' + num.substring(num.length - (4 * i + 3) ); 123 | } 124 | return ( ( (sign) ? '' : '-' ) + num + '.' + cents); 125 | } 126 | 127 | /** 128 | * 金额转大写 129 | * @desc 将数字金额转为中文大写 130 | * @param {Number} 131 | * @return {String} 132 | * @example digitUppercase(7682.01); // 柒仟陆佰捌拾贰元壹分 133 | * digitUppercase(7682); //柒仟陆佰捌拾贰元整 134 | * digitUppercase(951434677682.00); //玖仟伍佰壹拾肆亿叁仟肆佰陆拾柒万柒仟陆佰捌拾贰元整 135 | */ 136 | function digitUppercase(n) { 137 | var fraction = ['角', '分']; 138 | var digit = [ 139 | '零', '壹', '贰', '叁', '肆', 140 | '伍', '陆', '柒', '捌', '玖' 141 | ]; 142 | var unit = [ 143 | ['元', '万', '亿'], 144 | ['', '拾', '佰', '仟'] 145 | ]; 146 | var head = n < 0 ? '欠' : ''; 147 | n = Math.abs(n); 148 | var s = ''; 149 | for (var i = 0; i < fraction.length; i++) { 150 | s += (digit[Math.floor(n * 10 * Math.pow(10, i)) % 10] + fraction[i]).replace(/零./, ''); 151 | } 152 | s = s || '整'; 153 | n = Math.floor(n); 154 | for (var i = 0; i < unit[0].length && n > 0; i++) { 155 | var p = ''; 156 | for (var j = 0; j < unit[1].length && n > 0; j++) { 157 | p = digit[n % 10] + unit[1][j] + p; 158 | n = Math.floor(n / 10); 159 | } 160 | s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s; 161 | } 162 | return head + s.replace(/(零.)*零元/, '元') 163 | .replace(/(零.)+/g, '零') 164 | .replace(/^整$/, '零元整'); 165 | }; 166 | 167 | /** 168 | * 处理模板 169 | * @desc 将模板文件替换为实际数据 170 | * @param temp字符串 171 | * @param data 实际数据 172 | * @return 返回已替换为真实数据的字符串 173 | * @example handleTemplate('张三的手机号是${phoneNo}', {phoneNo: '17712345678'}); // 张三的手机号是17712345678 174 | */ 175 | function handleTemplate(temp, data) { 176 | for (var key in data) { 177 | var reg = new RegExp("\\$\\{" + key + "\\}", "g"); 178 | temp = temp.replace(reg, data[key]); 179 | } 180 | return temp; 181 | } 182 | 183 | 184 | export { 185 | md5, 186 | md5Pay, 187 | encryptByAES, 188 | decryptByAES, 189 | formatTimestamp, 190 | sortObj, 191 | pennyToYuan, 192 | formatCurrency, 193 | digitUppercase, 194 | handleTemplate 195 | } 196 | // module.exports = util; 197 | -------------------------------------------------------------------------------- /src/entry/index.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Main Entry 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import {BrowserRouter as Router, BrowserRouter, Route, Link} from 'react-router-dom'; 10 | import 'scss/global.scss'; 11 | 12 | import { AppContainer } from 'react-hot-loader'; 13 | 14 | import App from '../routes/index.js'; 15 | 16 | 17 | const render = (Component) => { 18 | ReactDOM.render( 19 | 20 | 21 | , 22 | document.getElementById('App') 23 | ); 24 | }; 25 | 26 | 27 | render(App); 28 | 29 | // Hot Module Replacement API 30 | if (module && module.hot) { 31 | module.hot.accept('../routes/index.js', () => { 32 | render(App) 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/entry/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Main Entry 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import {BrowserRouter as Router, BrowserRouter, Route, Link} from 'react-router-dom'; 10 | import 'scss/global.scss'; 11 | 12 | 13 | import App from '../routes/index.js'; 14 | 15 | 16 | ReactDOM.render( 17 | , 18 | document.getElementById('App') 19 | ); 20 | -------------------------------------------------------------------------------- /src/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/libs/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Joyotang/React-Koa-Mongodb/aff8ddac9c2065e26619fc5d700af3a7ae61cc52/src/libs/index.js -------------------------------------------------------------------------------- /src/routes/Account/account.scss: -------------------------------------------------------------------------------- 1 | 2 | .login-module { 3 | width: 400px; 4 | padding: 20px 50px; 5 | margin: 100px auto 0; 6 | border: 1px solid #dbdbdb; 7 | border-radius: 4px; 8 | } 9 | 10 | .login-form-header { 11 | height: 50px; 12 | font-size: 18px; 13 | line-height: 50px; 14 | text-align: center; 15 | } 16 | 17 | .login-form-forgot { 18 | float: right; 19 | } 20 | .account-form-button { 21 | width: 100%; 22 | } 23 | 24 | 25 | .retrieve-module { 26 | width: 400px; 27 | margin: 100px auto 0; 28 | } 29 | -------------------------------------------------------------------------------- /src/routes/Account/changePwd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Login 3 | * @author txBoy 4 | * @created 2017-04-13 5 | */ 6 | 7 | import { Component } from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | import { Form, Icon, Input, Button, Checkbox } from 'antd'; 10 | const FormItem = Form.Item; 11 | 12 | import './account.scss'; 13 | 14 | class ForgetPassword extends Component { 15 | constructor(props) { 16 | super(props) 17 | this.state = { 18 | confirmDirty: false 19 | } 20 | } 21 | handleSubmit = (e) => { 22 | e.preventDefault(); 23 | this.props.form.validateFieldsAndScroll((err, values) => { 24 | if (!err) { 25 | console.log('Received values of form: ', values); 26 | } 27 | }); 28 | } 29 | handleConfirmBlur = (e) => { 30 | const value = e.target.value; 31 | this.setState({ 32 | confirmDirty: this.state.confirmDirty || !!value 33 | }); 34 | } 35 | oldPassword = (rule, value, callback) => { 36 | const form = this.props.form; 37 | if (value && this.state.confirmDirty) { 38 | form.validateFields(['oldPassword'], { 39 | force: true 40 | }); 41 | } 42 | callback(); 43 | } 44 | newPassword = (rule, value, callback) => { 45 | const form = this.props.form; 46 | if (value && this.state.confirmDirty) { 47 | form.validateFields(['newPassword'], { 48 | force: true 49 | }); 50 | } 51 | callback(); 52 | } 53 | checkPassword = (rule, value, callback) => { 54 | const form = this.props.form; 55 | if (value && value !== form.getFieldValue('password')) { 56 | callback('两次密码不一致!'); 57 | } else { 58 | callback(); 59 | } 60 | } 61 | render() { 62 | const { getFieldDecorator } = this.props.form; 63 | const formItemLayout = { 64 | labelCol: { 65 | xs: { 66 | span: 24 67 | }, 68 | sm: { 69 | span: 6 70 | }, 71 | }, 72 | wrapperCol: { 73 | xs: { 74 | span: 24 75 | }, 76 | sm: { 77 | span: 14 78 | }, 79 | }, 80 | }; 81 | const tailFormItemLayout = { 82 | wrapperCol: { 83 | xs: { 84 | span: 24, 85 | offset: 0, 86 | }, 87 | sm: { 88 | span: 14, 89 | offset: 6, 90 | }, 91 | }, 92 | }; 93 | return ( 94 |
95 |
修改密码
96 |
97 | 98 | { 99 | getFieldDecorator('oldPassword', { 100 | rules: [{ 101 | required: true, 102 | message: 'Please input your password!', 103 | }, { 104 | validator: this.oldPassword, 105 | }], 106 | })( ) 107 | } 108 | 109 | 110 | { 111 | getFieldDecorator('password', { 112 | rules: [{ 113 | required: true, 114 | message: 'Please input your password!', 115 | }, { 116 | validator: this.newPassword, 117 | }], 118 | })( ) 119 | } 120 | 121 | 122 | { 123 | getFieldDecorator('confirm', { 124 | rules: [{ 125 | required: true, 126 | message: 'Please confirm your password!', 127 | }, { 128 | validator: this.checkPassword, 129 | }], 130 | })( ) 131 | } 132 | 133 | 134 |
138 | ); 139 | } 140 | } 141 | 142 | const RetrieveFrom = Form.create()(ForgetPassword); 143 | 144 | export default RetrieveFrom; 145 | -------------------------------------------------------------------------------- /src/routes/Auth/auth.scss: -------------------------------------------------------------------------------- 1 | 2 | .login-module { 3 | width: 400px; 4 | padding: 20px 50px; 5 | margin: 100px auto 0; 6 | border: 1px solid #dbdbdb; 7 | border-radius: 4px; 8 | } 9 | 10 | .login-form-header { 11 | height: 50px; 12 | font-size: 18px; 13 | line-height: 50px; 14 | text-align: center; 15 | } 16 | 17 | .login-form-forgot { 18 | float: right; 19 | } 20 | .account-form-button { 21 | width: 100%; 22 | } 23 | 24 | 25 | 26 | .wrap-register, 27 | .reset-pwd-module { 28 | width: 400px; 29 | margin: 100px auto 0; 30 | } 31 | 32 | .register-module, 33 | .success-module { 34 | &.show { 35 | display: block; 36 | } 37 | &.hide { 38 | display: none; 39 | } 40 | } 41 | 42 | .success-module { 43 | 44 | .complete-text { 45 | font-size: 18px; 46 | text-align: center; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/routes/Auth/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Login 3 | * @author txBoy 4 | * @created 2017-04-13 5 | */ 6 | 7 | import { Component } from 'react'; 8 | import { BrowserRouter as Router, Route, BrowserRouter, Link} from 'react-router-dom'; 9 | import { Layout, Menu, Breadcrumb } from 'antd'; 10 | const { Header, Content, Footer } = Layout; 11 | import LazyRoute from 'lazy-route'; 12 | 13 | /*import LoginFrom from './login'; 14 | import ResetPwdForm from './resetPwd';*/ 15 | 16 | class AuthLayout extends Component { 17 | handleSubmit = (e) => { 18 | e.preventDefault(); 19 | this.props.form.validateFields((err, values) => { 20 | if (!err) { 21 | console.log('Received values of form: ', values); 22 | } 23 | }); 24 | } 25 | render() { 26 | return ( 27 | 28 |
29 |
30 |
31 | 32 |
33 | } /> 34 | } /> 35 | } /> 36 |
37 |
38 |
39 | Copyright © 2017 Thinktxt Inc. All Rights Reserved. 40 |
41 |
42 | ); 43 | } 44 | } 45 | 46 | 47 | export default AuthLayout; 48 | -------------------------------------------------------------------------------- /src/routes/Auth/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Login 3 | * @author txBoy 4 | * @created 2017-04-13 5 | */ 6 | 7 | import { Component } from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | import { Form, Icon, Input, Button, Checkbox } from 'antd'; 10 | const FormItem = Form.Item; 11 | 12 | import fetcher from 'utils/fetcher'; 13 | import { md5 } from 'utils/util'; 14 | import storage from 'utils/storage.js'; 15 | import './auth.scss'; 16 | 17 | class Login extends Component { 18 | constructor(props) { 19 | super(props) 20 | this.state = { 21 | userInfo: null 22 | } 23 | } 24 | handleSubmit = (e) => { 25 | e.preventDefault(); 26 | this.props.form.validateFields((err, values) => { 27 | if (!err) { 28 | console.log('Received values of form: ', values); 29 | values.password = md5(values.password); 30 | this.onLogin(values) 31 | } 32 | }); 33 | } 34 | onLogin(params) { 35 | fetcher.post('/api/user/login', {data: params}).then(res => { 36 | console.log(res) 37 | if (res && res.code === 10000) { 38 | var userInfo = res.data; 39 | storage.setCookie('userInfo', JSON.stringify(userInfo)); 40 | window.location.href = '/'; 41 | } 42 | }) 43 | } 44 | render() { 45 | const { getFieldDecorator } = this.props.form; 46 | return ( 47 |
48 |
用户登录
49 |
50 | 51 | {getFieldDecorator('username', { 52 | rules: [{ required: true, message: '请输入用户名!' }], 53 | })( 54 | } placeholder="用户名" /> 55 | )} 56 | 57 | 58 | {getFieldDecorator('password', { 59 | rules: [{ required: true, message: '请输入密码!' }], 60 | })( 61 | } type="password" placeholder="请输入密码" /> 62 | )} 63 | 64 | 65 | {getFieldDecorator('remember', { 66 | valuePropName: 'checked', 67 | initialValue: true, 68 | })( 69 | 记住我 70 | )} 71 | 忘记密码 72 | 73 | 74 |
75 |
76 | ); 77 | } 78 | } 79 | 80 | const LoginForm = Form.create()(Login); 81 | 82 | export default LoginForm; 83 | -------------------------------------------------------------------------------- /src/routes/Auth/register.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Login 3 | * @author txBoy 4 | * @created 2017-04-13 5 | */ 6 | 7 | import { Component } from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | import { Layout, Menu, Breadcrumb } from 'antd'; 10 | const { Header, Content, Footer } = Layout; 11 | import { Form, Icon, Input, Button, Checkbox } from 'antd'; 12 | const FormItem = Form.Item; 13 | 14 | import fetcher from 'utils/fetcher'; 15 | import { md5 } from 'utils/util'; 16 | import './auth.scss'; 17 | 18 | class Register extends Component { 19 | constructor(props) { 20 | super(props) 21 | this.state = { 22 | showSuccess: false, 23 | confirmDirty: false, 24 | userInfo: null 25 | } 26 | } 27 | handleSubmit = (e) => { 28 | e.preventDefault(); 29 | this.props.form.validateFieldsAndScroll((err, values) => { 30 | if (!err) { 31 | console.log('Received values of form: ', values); 32 | values.password = md5(values.password); 33 | delete values.confirm; 34 | var params = values; 35 | this._onRegiter(params) 36 | } 37 | }); 38 | } 39 | handleConfirmBlur = (e) => { 40 | const value = e.target.value; 41 | this.setState({ 42 | confirmDirty: this.state.confirmDirty || !!value 43 | }); 44 | } 45 | _onRegiter(params) { 46 | fetcher.post('/api/user/register', {data: params}).then(res => { 47 | console.log(res) 48 | if (res.code === 10000) { 49 | var userInfo = res.data; 50 | this.setState({ 51 | showSuccess: true, 52 | userInfo: userInfo 53 | }) 54 | } 55 | }) 56 | } 57 | checkPassword = (rule, value, callback) => { 58 | const form = this.props.form; 59 | if (value && value !== form.getFieldValue('password')) { 60 | callback('Two passwords that you enter is inconsistent!'); 61 | } else { 62 | callback(); 63 | } 64 | } 65 | checkConfirm = (rule, value, callback) => { 66 | const form = this.props.form; 67 | if (value && this.state.confirmDirty) { 68 | form.validateFields(['confirm'], { 69 | force: true 70 | }); 71 | } 72 | callback(); 73 | } 74 | render() { 75 | const { getFieldDecorator } = this.props.form; 76 | const formItemLayout = { 77 | labelCol: { 78 | xs: { 79 | span: 24 80 | }, 81 | sm: { 82 | span: 6 83 | }, 84 | }, 85 | wrapperCol: { 86 | xs: { 87 | span: 24 88 | }, 89 | sm: { 90 | span: 14 91 | }, 92 | }, 93 | }; 94 | const tailFormItemLayout = { 95 | wrapperCol: { 96 | xs: { 97 | span: 24, 98 | offset: 0, 99 | }, 100 | sm: { 101 | span: 14, 102 | offset: 6, 103 | }, 104 | }, 105 | }; 106 | var userInfo = this.state.userInfo; 107 | return ( 108 |
109 |
110 |
用户注册
111 |
112 | 113 | { 114 | getFieldDecorator('username', { 115 | rules: [{ 116 | type: 'string', 117 | message: 'The input is not valid username!', 118 | }, { 119 | required: true, 120 | message: 'Please input your username!', 121 | }], 122 | })() 123 | } 124 | 125 | 126 | { 127 | getFieldDecorator('email', { 128 | rules: [{ 129 | type: 'email', 130 | message: 'The input is not valid E-mail!', 131 | }, { 132 | required: true, 133 | message: 'Please input your E-mail!', 134 | }], 135 | })() 136 | } 137 | 138 | 139 | { 140 | getFieldDecorator('password', { 141 | rules: [{ 142 | required: true, 143 | message: 'Please input your password!', 144 | }, { 145 | validator: this.checkConfirm, 146 | }], 147 | })( ) 148 | } 149 | 150 | 151 | { 152 | getFieldDecorator('confirm', { 153 | rules: [{ 154 | required: true, 155 | message: 'Please confirm your password!', 156 | }, { 157 | validator: this.checkPassword, 158 | }], 159 | })( ) 160 | } 161 | 162 | 163 |
167 |
168 |
169 | 170 | 171 |
172 |
恭喜你{userInfo && userInfo.username},注册成功!
173 |
已发送验证至:{userInfo && userInfo.email} 立即登录
174 |
175 |
176 | ); 177 | } 178 | } 179 | 180 | const RegisterForm = Form.create()(Register); 181 | 182 | export default RegisterForm; 183 | -------------------------------------------------------------------------------- /src/routes/Auth/resetPwd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Login 3 | * @author txBoy 4 | * @created 2017-04-13 5 | */ 6 | 7 | import { Component } from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | import { Layout, Menu, Breadcrumb } from 'antd'; 10 | const { Header, Content, Footer } = Layout; 11 | import { Form, Icon, Input, Button, Checkbox } from 'antd'; 12 | const FormItem = Form.Item; 13 | 14 | import './auth.scss'; 15 | 16 | class ResetPwd extends Component { 17 | constructor(props) { 18 | super(props) 19 | this.state = { 20 | confirmDirty: false 21 | } 22 | } 23 | handleSubmit = (e) => { 24 | e.preventDefault(); 25 | this.props.form.validateFieldsAndScroll((err, values) => { 26 | if (!err) { 27 | console.log('Received values of form: ', values); 28 | } 29 | }); 30 | } 31 | handleConfirmBlur = (e) => { 32 | const value = e.target.value; 33 | this.setState({ 34 | confirmDirty: this.state.confirmDirty || !!value 35 | }); 36 | } 37 | checkPassword = (rule, value, callback) => { 38 | const form = this.props.form; 39 | if (value && value !== form.getFieldValue('password')) { 40 | callback('Two passwords that you enter is inconsistent!'); 41 | } else { 42 | callback(); 43 | } 44 | } 45 | checkConfirm = (rule, value, callback) => { 46 | const form = this.props.form; 47 | if (value && this.state.confirmDirty) { 48 | form.validateFields(['confirm'], { 49 | force: true 50 | }); 51 | } 52 | callback(); 53 | } 54 | render() { 55 | const { getFieldDecorator } = this.props.form; 56 | const formItemLayout = { 57 | labelCol: { 58 | xs: { 59 | span: 24 60 | }, 61 | sm: { 62 | span: 6 63 | }, 64 | }, 65 | wrapperCol: { 66 | xs: { 67 | span: 24 68 | }, 69 | sm: { 70 | span: 14 71 | }, 72 | }, 73 | }; 74 | const tailFormItemLayout = { 75 | wrapperCol: { 76 | xs: { 77 | span: 24, 78 | offset: 0, 79 | }, 80 | sm: { 81 | span: 14, 82 | offset: 6, 83 | }, 84 | }, 85 | }; 86 | return ( 87 |
88 |
找回密码
89 |
90 | 91 | { 92 | getFieldDecorator('email', { 93 | rules: [{ 94 | type: 'email', 95 | message: 'The input is not valid E-mail!', 96 | }, { 97 | required: true, 98 | message: 'Please input your E-mail!', 99 | }], 100 | })() 101 | } 102 | 103 | 104 | { 105 | getFieldDecorator('password', { 106 | rules: [{ 107 | required: true, 108 | message: 'Please input your password!', 109 | }, { 110 | validator: this.checkConfirm, 111 | }], 112 | })( ) 113 | } 114 | 115 | 116 | { 117 | getFieldDecorator('confirm', { 118 | rules: [{ 119 | required: true, 120 | message: 'Please confirm your password!', 121 | }, { 122 | validator: this.checkPassword, 123 | }], 124 | })( ) 125 | } 126 | 127 | 128 |
132 | ); 133 | } 134 | } 135 | 136 | const ResetPwdForm = Form.create()(ResetPwd); 137 | 138 | export default ResetPwdForm; 139 | -------------------------------------------------------------------------------- /src/routes/IndexPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview IndexPage 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | 7 | import { Component } from 'react'; 8 | 9 | // import logo from 'img/logo.svg'; 10 | import 'scss/global.scss'; 11 | 12 | class IndexPage extends Component { 13 | constructor(props) { 14 | super(props) 15 | this.state = {}; 16 | } 17 | componentDidMount() { 18 | 19 | } 20 | render() { 21 | return ( 22 |
23 |

欢迎使用!

24 |
25 | ); 26 | } 27 | } 28 | 29 | export default IndexPage; 30 | -------------------------------------------------------------------------------- /src/routes/LayoutPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview layoutPage 3 | * @author txBoy 4 | * @created 2017-04-05 5 | */ 6 | 7 | import { Component } from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | 10 | import { Layout, Menu, Breadcrumb, Icon, Button } from 'antd'; 11 | const { Header, Content, Footer, Sider } = Layout; 12 | const SubMenu = Menu.SubMenu; 13 | 14 | import { removeWithoutCopy } from 'utils/util'; 15 | import fetcher from 'utils/fetcher'; 16 | import storage from 'utils/storage.js'; 17 | import 'scss/global.scss'; 18 | 19 | const userInfo = JSON.parse(storage.getCookie('userInfo') || '{}'); 20 | 21 | class LayoutPage extends Component { 22 | constructor(props) { 23 | super(props) 24 | this.state = { 25 | isRenderSider: true, 26 | menuConfig: this.getNav(), 27 | collapsed: false, 28 | mode: 'inline', 29 | }; 30 | } 31 | componentDidMount () { 32 | //this.formatMenuConf() 33 | } 34 | onCollapse = (collapsed) => { 35 | console.log(collapsed); 36 | this.setState({ 37 | collapsed, 38 | mode: collapsed ? 'vertical' : 'inline', 39 | }); 40 | } 41 | getNav () { 42 | var menuConfig = this.formatMenuConf(); 43 | return menuConfig.map((x) => { 44 | return { 45 | key: x.path, 46 | name: x.name, 47 | icon: x.icon, 48 | subs: (x.childRoutes || []).map((y, i) => { 49 | let subKey = y.path.substr(1).replace(/\//, '-'); 50 | return { 51 | key: subKey, 52 | name: y.name, 53 | path: y.path 54 | } 55 | }) 56 | } 57 | }) 58 | } 59 | onLogout() { 60 | fetcher.get('/api/user/logout', function (err, res) { 61 | console.log(res) 62 | }) 63 | storage.setCookie('userInfo', '', 0); 64 | window.location.href = '/login'; 65 | } 66 | formatMenuConf () { 67 | var routes = this.props.menuConfig; 68 | let list = routes.slice(0); 69 | list.forEach((x, key) => { 70 | (x.childRoutes || []).map( y => { 71 | if (y.path.indexOf(':') > 0) { 72 | removeWithoutCopy(x.childRoutes, y); 73 | } 74 | }) 75 | }) 76 | return list; 77 | } 78 | getMenuKeys () { 79 | var path = location.pathname.replace(/^\//, ''); 80 | var menuKeys = []; 81 | if (!path) { 82 | menuKeys.push('index'); 83 | } else { 84 | menuKeys = path.split('/').slice(0, 2); 85 | } 86 | return menuKeys; 87 | } 88 | renderMenu (menuConfig) { 89 | var navs = menuConfig.slice(1); 90 | return navs.map(x => { 91 | if (x.path === '/') { 92 | return null; 93 | } else { 94 | return ( 95 | {x.name || x.key}} 98 | > 99 | {(x.subs || []).map(y => ( 100 | {y.name} 101 | ))} 102 | 103 | ); 104 | } 105 | }); 106 | } 107 | render() { 108 | var menuConfig = this.state.menuConfig; 109 | // console.log(menuConfig) 110 | var menuKeys = this.getMenuKeys(); 111 | return ( 112 | 113 | 118 |
React-Node-Demo
119 | 120 | 121 | 122 | 123 | 124 | 首页 125 | 126 | 127 | 128 | { this.renderMenu(menuConfig) } 129 | 130 |
131 | 132 |
133 |
Hello, 欢迎 {userInfo.realName || userInfo.username} ^_^
134 |
乐潇游【React-Node-Mongodb】技术栈实例平台
135 |
136 | 退 出 137 |
138 |
139 | 140 | 141 | User 142 | Content 143 | 144 |
145 | {this.props.routesChildren} 146 |
147 |
148 |
Copyright © 2017 Thinktxt Inc. All Rights Reserved.
149 |
150 |
151 | ); 152 | } 153 | } 154 | 155 | export default LayoutPage; 156 | -------------------------------------------------------------------------------- /src/routes/User/detail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Detail 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | 7 | import { Component } from 'react'; 8 | 9 | class DetailPage extends Component { 10 | render() { 11 | return ( 12 |
用户姓名: {this.props.match.params.name}
13 | ); 14 | } 15 | } 16 | 17 | export default DetailPage; 18 | -------------------------------------------------------------------------------- /src/routes/User/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview List 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | 7 | import { Component } from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | import { Table } from 'antd'; 10 | import fetcher from 'utils/fetcher'; 11 | import { md5, formatTimestamp, formatGender } from 'utils/util'; 12 | 13 | class ListPage extends Component { 14 | constructor(props) { 15 | super(props) 16 | this.state = { 17 | query: { 18 | page: 1, 19 | size: 10, 20 | filter: { 21 | searchValue: null 22 | } 23 | }, 24 | data: null, 25 | userList: [] 26 | } 27 | } 28 | componentDidMount() { 29 | 30 | this.getList(); 31 | 32 | var timestamp = new Date().getTime(); 33 | console.log(formatTimestamp(timestamp)) 34 | } 35 | // 获取列表数据 36 | getList() { 37 | var params = Object.assign({}, this.state.query); 38 | params.filter = JSON.stringify(params.filter); 39 | 40 | fetcher.get('/api/user/getUsers', {data: params}).then(res => { 41 | console.log(res) 42 | if (res && res.code === 10000) { 43 | var data = res.data; 44 | var userList = data.list || []; 45 | this.setState({ 46 | data: data, 47 | userList: userList 48 | }) 49 | } 50 | }) 51 | } 52 | 53 | renderTable() { 54 | var _this = this; 55 | var data = this.state.data; 56 | const pagination = { 57 | current: _this.state.query.page, 58 | total: data && data.total || 0, 59 | showSizeChanger: true, 60 | onShowSizeChange(current, pageSize) { 61 | _this.state.query.page = current; 62 | _this.state.query.size = pageSize; 63 | _this.forceUpdate(); 64 | _this.getList(); 65 | }, 66 | onChange(current) { 67 | _this.state.query.page = current; 68 | _this.getList(); 69 | } 70 | }; 71 | 72 | const columns = [{ 73 | title: '用户名', 74 | dataIndex: 'username', 75 | render: (text, item) => {text} 76 | }, { 77 | title: '性别', 78 | dataIndex: 'gender', 79 | render: (text, item) => formatGender(item.gender) 80 | }, { 81 | title: '邮箱', 82 | dataIndex: 'email', 83 | }, { 84 | title: '操作', 85 | dataIndex: 'uid', 86 | render: (text, item) => ( 87 | 88 | Action - {item.uid} 89 | 90 | Delete 91 | 92 | ) 93 | }]; 94 | 95 | // rowSelection object indicates the need for row selection 96 | const rowSelection = { 97 | onChange: (selectedRowKeys, selectedRows) => { 98 | console.log(`selectedRowKeys: ${selectedRowKeys}`, 99 | 'selectedRows: ', selectedRows); 100 | }, 101 | onSelect: (record, selected, selectedRows) => { 102 | console.log(record, selected, selectedRows); 103 | }, 104 | onSelectAll: (selected, selectedRows, changeRows) => { 105 | console.log(selected, selectedRows, changeRows); 106 | }, 107 | getCheckboxProps: record => ({ 108 | //disabled: record.name === 'Disabled User', // Column configuration not to be checked 109 | }), 110 | }; 111 | 112 | return () 113 | } 114 | 115 | render() { 116 | return ( 117 |
118 | {this.renderTable()} 119 |
120 | ); 121 | } 122 | } 123 | 124 | export default ListPage; 125 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Router Index 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | import React, { Component } from 'react'; 7 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 8 | import LazyRoute from 'lazy-route'; 9 | 10 | import AuthLayout from './Auth'; 11 | import LayoutPage from './LayoutPage'; 12 | 13 | import routesConfig from './routes'; 14 | import storage from 'utils/storage'; 15 | 16 | const userInfo = storage.getCookie('userInfo') ? JSON.parse(storage.getCookie('userInfo')) : ''; 17 | const accessReg = userInfo.accessUrls && userInfo.accessUrls.length ? new RegExp(userInfo.accessUrls.map(url => '^' + url).join('|')) : null; 18 | 19 | // 定义路由组件 20 | const RouteWithSubRoutes = (route) => ( 21 | { 22 | // route.onEnter(); // 校验准入权限 23 | return ; 24 | }}/> 25 | ) 26 | 27 | // 渲染所有路由 28 | const renderRoutes = (routes) => { 29 | return routes.map((route, i) => { 30 | if(route.childRoutes) { 31 | return renderRoutes(route.childRoutes) 32 | } else { 33 | return () 34 | } 35 | }) 36 | } 37 | 38 | function checkAccess() { 39 | var pathname = window.location.pathname; 40 | console.log('-----------pathname:', pathname) 41 | if (pathname != '/' && accessReg && !accessReg.test(pathname)) { 42 | location.replace('/error/403'); 43 | } 44 | } 45 | 46 | function formatRoutes(routes) { 47 | let list = routes.slice(0); 48 | while (list.length) { 49 | let route = list.shift(); 50 | if (!route.onEnter) { 51 | route.onEnter = checkAccess; 52 | } 53 | if (route.childRoutes && route.childRoutes.length) { 54 | list.push.apply(list, route.childRoutes); 55 | } 56 | } 57 | return routes; 58 | } 59 | 60 | var routes = formatRoutes(routesConfig); 61 | 62 | class App extends Component { 63 | constructor(props) { 64 | super(props) 65 | this.state = { 66 | isRenderSider: false, 67 | simplePage: false 68 | }; 69 | } 70 | componentWillMount() { 71 | var pathname = window.location.pathname; 72 | if(pathname.indexOf('/login') > -1){ 73 | storage.setCookie('userInfo', ''); 74 | } 75 | // this.hideSider(); 76 | } 77 | hideSider() { 78 | var pathname = window.location.pathname; 79 | var pathArr = ['login', 'register', 'resetPwd', 'error'] 80 | for (var i = 0; i < pathArr.length; i++) { 81 | if (pathname.indexOf(pathArr[i]) != -1) { 82 | this.setState({ 83 | isRenderSider: false 84 | }) 85 | } 86 | } 87 | } 88 | renderLayoutPage() { 89 | var pathname = window.location.pathname; 90 | if(userInfo){ 91 | this.state.isRenderSider = true; 92 | } 93 | if (pathname.indexOf('error') != -1) { 94 | this.state.simplePage = true; 95 | } 96 | var isRender = this.state.isRenderSider; 97 | var simplePage = this.state.simplePage; 98 | if (isRender && !simplePage) { 99 | return ( 100 | 101 | ) 102 | } else if (simplePage) { 103 | return ( 104 | 105 | ) 106 | } else { 107 | var pathname = window.location.pathname; 108 | if(pathname.indexOf('/login') > -1 || pathname.indexOf('/register') > -1 || pathname.indexOf('/resetPwd') > -1){ 109 | return ( 110 | 111 | ) 112 | } else { 113 | window.location.href = '/login'; 114 | } 115 | } 116 | } 117 | render() { 118 | return ( 119 | 120 | {this.renderLayoutPage()} 121 | 122 | ); 123 | } 124 | } 125 | export default App; 126 | -------------------------------------------------------------------------------- /src/routes/index.x.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Router Index 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | 7 | import React, { Component } from 'react'; 8 | import { BrowserRouter as Router, Route, BrowserRouter, Link} from 'react-router-dom'; 9 | import LazyRoute from 'lazy-route'; 10 | 11 | import AuthLayout from './Auth'; 12 | /*import ResetPwd from './Auth/resetPwd';*/ 13 | import LayoutPage from './LayoutPage'; 14 | 15 | import routesConfig from './routes'; 16 | 17 | // 定义路由组件 18 | const RouteWithSubRoutes = (route) => ( 19 | ( 20 | 21 | )}/> 22 | ) 23 | 24 | // 渲染所有路由 25 | const renderRoutes = (routes) => { 26 | return routes.map((route, i) => { 27 | if(route.childRoutes) { 28 | return renderRoutes(route.childRoutes) 29 | } else { 30 | return () 31 | } 32 | }) 33 | } 34 | 35 | class App extends Component { 36 | constructor(props) { 37 | super(props) 38 | this.state = { 39 | isRenderSider: true, 40 | }; 41 | } 42 | componentWillMount() { 43 | this.hideSider(); 44 | } 45 | hideSider() { 46 | var pathname = window.location.pathname; 47 | var pathArr = ['login', 'register', 'resetPwd'] 48 | for (var i = 0; i < pathArr.length; i++) { 49 | if (pathname.indexOf(pathArr[i]) >= 0) { 50 | this.setState({ 51 | isRenderSider: false 52 | }) 53 | } 54 | } 55 | console.log(location.pathname) 56 | } 57 | renderLayoutPage() { 58 | var isRender = this.state.isRenderSider; 59 | if (isRender) { 60 | return ( 61 | 62 | ) 63 | } else { 64 | return ( 65 |
66 | 67 |
68 | ) 69 | } 70 | } 71 | render() { 72 | return ( 73 | 74 | {this.renderLayoutPage()} 75 | 76 | ); 77 | } 78 | } 79 | 80 | 81 | // 82 | // }/> 83 | // }/> 84 | 85 | export default App; 86 | -------------------------------------------------------------------------------- /src/routes/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview Router Config 3 | * @author txBoy 4 | * @created 2017-03-21 5 | */ 6 | 7 | const routesConfig = [ 8 | { 9 | path: '/', 10 | exact: true, 11 | icon: 'home', 12 | name: '首页', 13 | component: './IndexPage/index' 14 | }, 15 | { 16 | path: 'user', 17 | icon: 'user', 18 | name: '用户', 19 | childRoutes: [ 20 | { 21 | name: '列表', 22 | path: '/user/list', 23 | component: './User/list' 24 | }, 25 | { 26 | name: '详情', 27 | path: '/user/detail/:name', 28 | component: './User/detail' 29 | } 30 | ] 31 | }, 32 | { 33 | path: 'account', 34 | icon: 'solution', 35 | name: '账户', 36 | childRoutes: [ 37 | { 38 | name: '修改密码', 39 | path: '/account/changePwd', 40 | component: './Account/changePwd' 41 | } 42 | ] 43 | } 44 | ] 45 | 46 | export default routesConfig; 47 | -------------------------------------------------------------------------------- /src/scss/animation.scss: -------------------------------------------------------------------------------- 1 | /* success animation */ 2 | @keyframes success-short-Animation { 3 | 0% { 4 | width: 0; 5 | left: 1px; 6 | top: 19px 7 | } 8 | 54% { 9 | width: 0; 10 | left: 1px; 11 | top: 19px 12 | } 13 | 70% { 14 | width: 50px; 15 | left: -4px; 16 | top: 37px 17 | } 18 | 84% { 19 | width: 17px; 20 | left: 21px; 21 | top: 48px 22 | } 23 | 100% { 24 | width: 25px; 25 | left: 14px; 26 | top: 45px 27 | } 28 | } 29 | @keyframes success-long-Animation { 30 | 0% { 31 | width: 0; 32 | right: 46px; 33 | top: 54px 34 | } 35 | 65% { 36 | width: 0; 37 | right: 46px; 38 | top: 54px 39 | } 40 | 84% { 41 | width: 55px; 42 | right: 0; 43 | top: 35px 44 | } 45 | 100% { 46 | width: 47px; 47 | right: 8px; 48 | top: 38px 49 | } 50 | } 51 | /* lose animation */ 52 | @keyframes lose-Animation { 53 | 0% { 54 | transform: scale(0.6); 55 | opacity: .2 56 | } 57 | 50% { 58 | transform: scale(0.6); 59 | opacity: .5 60 | } 61 | 80% { 62 | transform: scale(1.15); 63 | opacity: .8 64 | } 65 | 100% { 66 | transform: scale(1); 67 | opacity: 1.0 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/scss/global.scss: -------------------------------------------------------------------------------- 1 | table { border-spacing:0; border-collapse:collapse;} 2 | 3 | .wrap { 4 | display: flex; 5 | height: 100%; 6 | } 7 | 8 | .simple-wrap { 9 | display: flex; 10 | width: 100%; 11 | } 12 | 13 | /* float and clearfix */ 14 | .flt { 15 | float: left; 16 | } 17 | 18 | .frt { 19 | float: right; 20 | } 21 | .clearfix:before, 22 | .clearfix:after {content: " "; display: table;} 23 | .clearfix:after {clear: both;} 24 | .clearfix {*zoom: 1;} 25 | 26 | .ant-layout-sider .logo { 27 | height: 32px; 28 | font-size: 14px; 29 | line-height: 32px; 30 | color: #fff; 31 | text-align: center; 32 | white-space: nowrap; 33 | text-overflow: ellipsis; 34 | overflow: hidden; 35 | background: #333; 36 | border-radius: 6px; 37 | margin: 16px; 38 | } 39 | 40 | .ant-layout-sider .ant-layout-sider-collapsed .anticon { 41 | font-size: 16px; 42 | margin-left: 8px; 43 | } 44 | 45 | .ant-layout-sider .ant-layout-sider-collapsed .nav-text { 46 | display: none; 47 | } 48 | 49 | .ant-layout-sider .ant-layout-sider-collapsed .ant-menu-submenu-vertical > .ant-menu-submenu-title:after { 50 | display: none; 51 | } 52 | 53 | .ant-layout-header { 54 | display: flex; 55 | justify-content: space-between; 56 | padding: 0 20px !important; 57 | font-size: 16px; 58 | color: #fff; 59 | background: #454545 !important; 60 | .user-info {} 61 | .header-content { 62 | flex-grow: 1; 63 | text-align: center; 64 | } 65 | .login-box { 66 | .logout-btn { 67 | padding: 5px 15px; 68 | font-size: 14px; 69 | color: #fff; 70 | border: 1px solid #fff; 71 | border-radius: 4px; 72 | &:hover { 73 | color: #108ee9; 74 | border-color: #108ee9; 75 | } 76 | } 77 | } 78 | } 79 | 80 | .ant-layout-breadcrumb { 81 | margin: 12px 0; 82 | } 83 | 84 | .index-content { 85 | .welcome-text { 86 | margin-top: 15px; 87 | font-size: 18px; 88 | text-align: center; 89 | } 90 | } 91 | 92 | 93 | /* text style */ 94 | .text-blue, 95 | .text-normal, { 96 | color: #108ee9; 97 | } 98 | 99 | .text-success { 100 | color: #6c0; 101 | } 102 | 103 | .text-red, 104 | .text-error { 105 | color: #f00; 106 | } 107 | 108 | .text-warn { 109 | color: #ffbf00; 110 | } 111 | 112 | .text-center { 113 | text-align: center; 114 | } 115 | 116 | 117 | /* icon */ 118 | .iconbox { 119 | position: relative; 120 | width: 88px; 121 | height: 88px; 122 | border-radius: 50px; 123 | border: 4px solid #6c3; 124 | margin: 15px auto; 125 | } 126 | .icon-lose {border-color: #fc454c;} 127 | .icon-success .line-short { 128 | width: 25px; 129 | height: 5px; 130 | position: absolute; 131 | left: 14px; 132 | top: 46px; 133 | border-radius: 4px; 134 | background-color: #6c3; 135 | -webkit-transform: rotate(45deg); 136 | -moz-transform: rotate(45deg); 137 | transform: rotate(45deg); 138 | -webkit-animation: success-short-Animation .65s; 139 | -moz-animation: success-short-Animation .65s; 140 | animation: success-short-Animation .65s; 141 | } 142 | .icon-success .line-long { 143 | width: 45px; 144 | height: 5px; 145 | position: absolute; 146 | right: 8px; 147 | top: 38px; 148 | border-radius: 4px; 149 | background-color: #6c3; 150 | -webkit-transform: rotate(-45deg); 151 | -moz-transform: rotate(-45deg); 152 | transform: rotate(-45deg); 153 | -webkit-animation: success-long-Animation .65s; 154 | -moz-animation: success-long-Animation .65s; 155 | animation: success-long-Animation .65s; 156 | } 157 | .losebox { 158 | position: relative; 159 | width: 88px; 160 | height: 88px; 161 | margin: 0 auto; 162 | text-align: center; 163 | } 164 | .losebox { 165 | -webkit-animation: lose-Animation .5s; 166 | -moz-animation: lose-Animation .5s; 167 | animation: lose-Animation .5s; 168 | } 169 | .icon-lose .line-left, .icon-lose .line-right { 170 | position: absolute; 171 | left: 17px; 172 | top: 37px; 173 | width: 47px; 174 | height: 5px; 175 | border-radius: 4px; 176 | background: #fc454c; 177 | -webkit-transform: rotate(45deg); 178 | -moz-transform: rotate(45deg); 179 | transform: rotate(45deg); 180 | } 181 | .icon-lose .line-right { 182 | right: 11px; 183 | top: 37px; 184 | -webkit-transform: rotate(-45deg); 185 | -moz-transform: rotate(-45deg); 186 | transform: rotate(-45deg); 187 | } 188 | -------------------------------------------------------------------------------- /src/utils/fetcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 封装fetch 3 | * @author txBoy 4 | * @created 2017-03-14 5 | */ 6 | 7 | import xFetch from './xfetch.js'; 8 | 9 | var fetcher = {}; 10 | 11 | /** 12 | * 项目fetch请求方法的封装,涉及到API请求的地方都走这里, 便于统一处理 13 | * @param url 14 | * @param options 15 | */ 16 | fetcher.request = (function () { 17 | var memCache = {}; 18 | return function (url, options) { 19 | if (options.memCache) { 20 | var key = url + JSON.stringify(options.data); 21 | var cache = memCache[key]; 22 | } 23 | if (cache) { 24 | return new Promise(function (resolve) { 25 | setTimeout(resolve.bind(null, cache), 0); 26 | }) 27 | } else { 28 | const headers = {}; 29 | headers['Content-Type'] = options.contentType || 'application/json'; 30 | options.headers = Object.assign((options && options.headers) || {}, headers); 31 | 32 | let data = options && options.data || {}; 33 | // 公司业务需要的统一参数,如不需要可删除 34 | /*data.source = data.source || 'web'; 35 | data.sv = data.sv || '1.0'; 36 | data.version = data.version || '1.0'; 37 | data.macId = data.macId || 'WEB';*/ 38 | 39 | options.body = data; 40 | delete options.data; 41 | delete options.memCache; 42 | 43 | return xFetch(url, options).then( (res) => { 44 | if (res.status === 'success' && options.memCache) { 45 | memCache[key] = res; 46 | } 47 | if (typeof options.success === 'function') { 48 | options.success(res); 49 | } 50 | return res; 51 | }); 52 | } 53 | } 54 | })(); 55 | 56 | /** 57 | * fetcher get 方法封装 58 | * @param url 59 | * @param options //特殊接口需要headers可通过options传递 60 | * @param callback //可省略,如果省略则通过then获取回调,推荐使用通过Promise.then方式获取回调 61 | * @returns Promise 62 | * @example fetcher.get('/api/getList', {data: {page:1, size:10} }).then(res => {cosnole.log(res)}) 63 | */ 64 | fetcher.get = function (url, options, success) { 65 | return this.request(url, { 66 | method: 'GET', 67 | headers: options && options.headers, 68 | contentType: options && options.contentType, 69 | data: options && options.data, 70 | success: success 71 | }); 72 | } 73 | 74 | /** 75 | * fetcher post 方法封装 76 | * @param url 77 | * @param params 78 | * @param callback //可省略,如果省略则通过then获取回调,推荐使用通过Promise.then方式获取回调 79 | * @returns Promise 80 | * @example fetcher.post('/api/edit', {data: {name:'txBoy'} }).then(res => {cosnole.log(res)}) 81 | */ 82 | fetcher.post = function (url, options, success) { 83 | return this.request(url, { 84 | method: 'POST', 85 | headers: options && options.headers, 86 | contentType: options && options.contentType, 87 | data: options && options.data, 88 | success: success 89 | }); 90 | } 91 | 92 | fetcher.all = function (urls, options) { 93 | var options = options || {}; 94 | var method = options.method || 'GET'; 95 | var data = options.data; 96 | 97 | if (method === 'POST') { 98 | return Promise.all(urls.map(url => { 99 | return fetcher.post(url, data); 100 | })) 101 | } else { 102 | return Promise.all(urls.map(url => { 103 | return fetcher.get(url, data); 104 | })); 105 | } 106 | } 107 | 108 | 109 | export default fetcher; 110 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | var Storage = { 2 | setSession: function(key, val) { 3 | try { 4 | var val_str = JSON.stringify(val || {}); 5 | window.sessionStorage.setItem('$' + key, val_str); 6 | } 7 | catch (ex) { 8 | console.error(ex); 9 | } 10 | }, 11 | getSession: function(key) { 12 | try { 13 | var val = window.sessionStorage.getItem('$' + key) || '{}'; 14 | return JSON.parse(val); 15 | } 16 | catch (ex) { 17 | console.error(ex); 18 | } 19 | }, 20 | setLocal: function(key, val) { 21 | try { 22 | var val_str = JSON.stringify(val || {}); 23 | window.localStorage.setItem('$' + key, val_str); 24 | } 25 | catch (ex) { 26 | console.error(ex); 27 | } 28 | }, 29 | getLocal: function(key) { 30 | try { 31 | var val = window.localStorage.getItem('$' + key); 32 | return val ? JSON.parse(val):''; 33 | } 34 | catch (ex) { 35 | console.error(ex); 36 | } 37 | }, 38 | delLocal: function(key) { 39 | try { 40 | window.localStorage.removeItem('$' + key); 41 | return; 42 | } 43 | catch (ex) { 44 | console.error(ex); 45 | } 46 | }, 47 | setCookie: function(name, value, ds) { 48 | var Days = ds ? ds:30; 49 | var exp = new Date(); 50 | exp.setTime(exp.getTime() + Days*24*60*60*1000); 51 | document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString()+";path=/;"; 52 | }, 53 | getCookie: function(name) { 54 | var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)"); 55 | if(arr=document.cookie.match(reg)) 56 | return decodeURIComponent(arr[2]); 57 | else { 58 | return null; 59 | } 60 | } 61 | }; 62 | 63 | module.exports = Storage; 64 | -------------------------------------------------------------------------------- /src/utils/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 封装util 3 | * @author txBoy 4 | * @created 2017-03-23 5 | */ 6 | 7 | import crypto from 'crypto'; 8 | 9 | /*** 10 | * 去除字符串两侧空格 11 | * @param str String 字符串 12 | */ 13 | function trim(str) { 14 | return str.replace(/(^\s+)|(\s+$)/g, ''); 15 | }; 16 | 17 | /** 18 | * md5正常字符串加密 19 | */ 20 | function md5(str) { 21 | var ret = crypto.createHash('md5').update(str.toString()).digest('hex'); 22 | return ret; 23 | } 24 | 25 | /** 26 | * md5中文字符串加密 27 | * @desc 解决中文加密不同的问题 28 | */ 29 | function md5Pay(str) { 30 | str = (new Buffer(str)).toString("binary"); 31 |   var ret = crypto.createHash('md5').update(str).digest("hex"); 32 |   return ret; 33 | } 34 | 35 | /** 36 | * 验证手机号码的方法 37 | * @param phoneNo 传入号码 38 | * @return Boolean 39 | */ 40 | function isPhone(phoneNo) { 41 | var reg = /^1(3|4|5|7|8)[0-9]\d{8}$/; 42 | if (!res.test(phoneNo)) { 43 | return false; 44 | } 45 | return true; 46 | } 47 | 48 | /** 49 | * 获取日期字符串 50 | * @param {addDayCount) 获取AddDayCount天后的日期 51 | * @return String 格式为:YYYY-MM-DD 52 | * @example getDateStr(0) //今天 getDateStr(-1) //昨天 53 | */ 54 | function getDateStr(addDayCount) { 55 | var dd = new Date(); 56 | dd.setDate(dd.getDate() + addDayCount); 57 | var y = dd.getFullYear(); 58 | var m = dd.getMonth() + 1; 59 | var d = dd.getDate(); 60 | m = m < 10 ? '0' + m : m; 61 | d = d < 10 ? '0' + d : d; 62 | return y + "-" + m + "-" + d; 63 | }; 64 | 65 | /** 66 | * 格式化时间戳 - 时间毫秒数转化为固定格式的时间字符串 67 | * @param timestamp 68 | * @param dateFormate 69 | * @description 其中dateFormate是有YYYY MM DD hh mm ss组成,可以是YYYY-MM-DD 也可以是YYYY-MM-DD hh:mm:ss 70 | * 如果不传的话,默认为: YYYY-MM-DD hh:mm:ss 71 | * @example formatTimestamp(1464314911403); 72 | * formatTimestamp(1464314911403, 'yyyyMMddhhmmss'); 73 | * formatTimestamp(1464314911403,'YYYY-MM-DD'); 74 | */ 75 | function formatTimestamp(timestamp, dateFormate) { 76 | var datemills = parseInt(timestamp); 77 | if (isNaN(datemills)) { 78 | console.error("Wrong timestamp, must be a number!"); 79 | return; 80 | } 81 | if (dateFormate == undefined) { 82 | dateFormate = "YYYY-MM-DD hh:mm:ss"; 83 | } 84 | var date = new Date(datemills); 85 | var year = "" + date.getFullYear(); 86 | var month = (date.getMonth() > 8 ? (date.getMonth() + 1) : "0" + (date.getMonth() + 1)); 87 | var day = date.getDate() > 9 ? date.getDate() : "0" + date.getDate(); 88 | var hour = date.getHours() > 9 ? date.getHours() : "0" + date.getHours(); 89 | var minute = date.getMinutes() > 9 ? date.getMinutes() : "0" + date.getMinutes(); 90 | var second = date.getSeconds() > 9 ? date.getSeconds() : "0" + date.getSeconds(); 91 | var datestr = dateFormate.replace(/yyyy/gi, year).replace('MM', month).replace(/dd/gi, day) 92 | .replace("hh", hour).replace("mm", minute).replace("ss", second); 93 | return datestr; 94 | } 95 | 96 | 97 | /** 98 | * 格式化卡号 99 | * @param {cardNo) 卡号 100 | * @return {String} 101 | * @example formatCardNo(201622512580888818) //2016 2251 2580 8888 18 102 | */ 103 | function formatCardNo(cardNo) { 104 | if (cardNo) { 105 | return cardNo.replace(/(\d{4})(\d{4})(\d{4})(\d{4})(\d{2})/, '$1-$2-$3-$4-$5'); 106 | } 107 | return '未知卡号'; 108 | }; 109 | 110 | /** 111 | * 格式化手机号码 112 | * @param {phoneNo) 手机号 113 | * @return {String} 114 | * @example formatCardNo(17722512580) //177 2251 2580 115 | */ 116 | function formatTelPhone(phone) { 117 | return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1 $2 $3'); 118 | }; 119 | 120 | /** 121 | * 数组去重 122 | */ 123 | function arrayUnique(arr) { 124 | var result = [], hash = {}; 125 | for (var i = 0, elem; (elem = arr[i]) != null; i++) { 126 | if (!hash[elem]) { 127 | result.push(elem); 128 | hash[elem] = true; 129 | } 130 | } 131 | return result; 132 | }; 133 | 134 | /** 135 | * 移除数组中指定元素,改变原数组 136 | * @param {arr} 需要处理的数组 137 | * @param {item} 该数组中要移除的元素 138 | */ 139 | function removeWithoutCopy(arr, item) { 140 | for (var i = 0; i < arr.length; i++) { 141 | if (arr[i] == item) { 142 | arr.splice(i, 1); 143 | i--; 144 | } 145 | } 146 | return arr; 147 | } 148 | 149 | /** 150 | * 格式化性别 151 | */ 152 | function formatGender(type) { 153 | switch (type) { 154 | case 0: 155 | return '保密'; 156 | break; 157 | case 1: 158 | return '男'; 159 | break; 160 | case 2: 161 | return '女'; 162 | break; 163 | case 3: 164 | return '保密'; 165 | break; 166 | default : 167 | return '未知' 168 | } 169 | } 170 | 171 | /** 172 | * URL参数转JSON 173 | * @param {url} 174 | * @return {JSONString} 175 | * @example var url = "http://www.example.com/user/getInfo?username&sid=12580&t"; 176 | * getJSONByURL(url) // {"username":"txboy","sid":"12580", "t": undefined} 177 | */ 178 | function getJSONByURL(url) { 179 | if(url == ''){ 180 | url = window.location.href; 181 | } 182 | var index = url.indexOf('?'); 183 | var str = url.substr(index + 1); 184 | var arr = str.split('&'); 185 | var json = '{'; 186 | for (var i = 0; i < arr.length; i++){ 187 | var s = arr[i].indexOf('='); 188 | if(s == -1){ 189 | json += ',"' + arr[i] + '":"undefined"'; 190 | continue; 191 | } 192 | var key = arr[i].substr(0, s); 193 | var val = arr[i].substr(s+1); 194 | var comma = ','; 195 | if(i == 0) { 196 | comma = ''; 197 | } 198 | json += comma + '\"' + key + '\":' + '\"' + val + '\"'; 199 | } 200 | return json += '}'; 201 | } 202 | 203 | export { 204 | trim, 205 | md5, 206 | md5Pay, 207 | isPhone, 208 | getDateStr, 209 | formatTimestamp, 210 | formatCardNo, 211 | formatTelPhone, 212 | arrayUnique, 213 | removeWithoutCopy, 214 | formatGender, 215 | getJSONByURL 216 | } 217 | -------------------------------------------------------------------------------- /src/utils/xfetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @overview 封装fetch 3 | * @author txBoy 4 | * @created 2017-03-14 5 | */ 6 | 7 | import 'es6-promise'; 8 | import 'whatwg-fetch'; 9 | import 'fetch-detector'; 10 | // import 'fetch-ie8'; 11 | // import 'formdata-emulate'; 12 | import queryString from 'querystring'; 13 | 14 | const encodeReg = /application\/x-www-form-urlencoded/; 15 | const JSONReg = /application\/json/; 16 | 17 | function checkStatus(response) { 18 | if (response.status >= 200 && response.status < 300) { 19 | return response; 20 | } else { 21 | var error = new Error(response.statusText); 22 | error.status = response.status; 23 | error.code = -500; //用-500标示http错误,非业务逻辑异常 24 | throw error; 25 | } 26 | } 27 | 28 | function parseJSON(res) { 29 | if (res.status === 200) { 30 | return res.json(); 31 | } else { 32 | return { 33 | status: 'error', 34 | msg: '网络异常,请稍后再试!', 35 | code: -500 36 | }; 37 | } 38 | } 39 | 40 | /** 41 | * fetch api的包装 42 | * @param url 请求url 43 | * @param options 44 | * - method 请求方法 45 | * - body 请求参数,get请求会将请求参数拼接到url, post请求则会放到body 46 | * @returns {Promise} 47 | */ 48 | function BaseFetch(url, options) { 49 | options = options || {}; 50 | if (options.method) options.method = options.method.toUpperCase(); 51 | if (!options.credentials) options.credentials = 'include'; 52 | 53 | var headers = { 54 | "X-Requested-With": "XMLHttpRequest", 55 | "Accept": "application/json" 56 | } 57 | options.headers = Object.assign(options.headers || {}, headers); 58 | 59 | if (options.method === 'POST') { 60 | if (typeof options.body === 'object' && !(options.body instanceof FormData)) { 61 | let type = options.headers['Content-Type']; 62 | if (JSONReg.test(type)) { 63 | options.body = JSON.stringify(options.body); 64 | } else if (encodeReg.test(type) || !type) { 65 | options.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; 66 | options.body = queryString.stringify(options.body); 67 | } 68 | } 69 | } else { 70 | if (typeof options.body === 'object') { 71 | url = url + (url.indexOf('?') > 0 ? '&' : '?') + queryString.stringify(options.body); 72 | options.body = null; 73 | } 74 | } 75 | 76 | return fetch(url, options).then(checkStatus).then(parseJSON); 77 | } 78 | 79 | export default function xFetch(url, options) { 80 | return new BaseFetch(url, options) 81 | } 82 | -------------------------------------------------------------------------------- /src/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 计算机集成管理系统 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/webpack.dev.conf.js'); 2 | --------------------------------------------------------------------------------