├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── client ├── README.md ├── build │ ├── build.js │ ├── dev-client.js │ ├── dev-server.js │ ├── utils.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── dist │ ├── index.html │ └── static │ │ ├── css │ │ ├── app.da188180f9367da1c17a31f85753b179.css │ │ └── app.da188180f9367da1c17a31f85753b179.css.map │ │ ├── favicon.ico │ │ ├── fonts │ │ ├── element-icons.a61be9c.eot │ │ └── element-icons.b02bdc1.ttf │ │ ├── img │ │ ├── element-icons.09162bc.svg │ │ └── login-bg.bd93cbf.jpg │ │ └── js │ │ ├── 0.dec371543c6055348ce2.js │ │ ├── 0.dec371543c6055348ce2.js.map │ │ ├── 1.c70df63b0d01f3a929cc.js │ │ ├── 1.c70df63b0d01f3a929cc.js.map │ │ ├── 2.6d969129a747a4e40bf0.js │ │ ├── 2.6d969129a747a4e40bf0.js.map │ │ ├── 3.05da50d96c1f3ee80635.js │ │ ├── 3.05da50d96c1f3ee80635.js.map │ │ ├── 4.e078cf75b7acf6431fd6.js │ │ ├── 4.e078cf75b7acf6431fd6.js.map │ │ ├── 5.5e06e08af2c686feb16f.js │ │ ├── 5.5e06e08af2c686feb16f.js.map │ │ ├── app.efb84e40ec52e9950145.js │ │ ├── app.efb84e40ec52e9950145.js.map │ │ ├── element.3adcff0352076e548f47.js │ │ ├── element.3adcff0352076e548f47.js.map │ │ ├── manifest.6109db3e9bc7ca4d54f0.js │ │ ├── manifest.6109db3e9bc7ca4d54f0.js.map │ │ ├── vendor.0420fe1bed7617ed3588.js │ │ └── vendor.0420fe1bed7617ed3588.js.map ├── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── css │ │ │ ├── animate.styl │ │ │ ├── flex.styl │ │ │ └── variable.styl │ │ ├── fonts │ │ │ ├── demo.css │ │ │ ├── demo_fontclass.html │ │ │ ├── iconfont.css │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.js │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.ttf │ │ │ └── iconfont.woff │ │ ├── images │ │ │ └── login-bg.jpg │ │ └── logo.png │ ├── components │ │ ├── ContentModule.vue │ │ ├── DataTable.vue │ │ ├── Header.vue │ │ ├── Menu.vue │ │ ├── NProgress.vue │ │ ├── NavMenu.vue │ │ ├── Pagination.vue │ │ └── RouterLoading.vue │ ├── constants.js │ ├── element-ui.js │ ├── http │ │ └── index.js │ ├── locale │ │ ├── en.js │ │ ├── en_US.js │ │ ├── index.js │ │ ├── things │ │ │ ├── en.js │ │ │ ├── en_US.js │ │ │ ├── zh-CN.js │ │ │ └── zh_CN.js │ │ ├── users │ │ │ ├── en.js │ │ │ ├── en_US.js │ │ │ ├── zh-CN.js │ │ │ └── zh_CN.js │ │ ├── zh-CN.js │ │ └── zh_CN.js │ ├── main.js │ ├── resources.js │ ├── router │ │ ├── index.js │ │ └── module.js │ ├── socket │ │ └── index.js │ ├── storage │ │ └── index.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── global-config.js │ │ │ ├── route.js │ │ │ ├── user.api.js │ │ │ └── user.js │ ├── stored.js │ └── view │ │ ├── CommonView.vue │ │ ├── Dashboard.vue │ │ ├── ThingList.vue │ │ ├── UserList.vue │ │ └── auth │ │ └── Login.vue └── static │ └── favicon.ico ├── config.js ├── nginx.example.conf ├── package.json ├── server ├── README.md ├── api │ ├── paging.js │ ├── thing │ │ ├── index.js │ │ ├── thing.controller.js │ │ ├── thing.model.js │ │ ├── thing.socket.js │ │ └── thing.spec.js │ └── user │ │ ├── index.js │ │ ├── user.controller.js │ │ ├── user.model.js │ │ └── user.model.spec.js ├── app.js ├── auth │ ├── auth.service.js │ ├── index.js │ └── local │ │ ├── index.js │ │ └── passport.js ├── components │ └── errors │ │ └── index.js ├── config │ ├── express.js │ ├── seed.js │ └── socketio.js ├── routes.js └── views │ └── 404.html └── tasks └── mock.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime", ["component", [ 4 | { 5 | "libraryName": "element-ui", 6 | "styleLibraryName": "theme-default" 7 | } 8 | ]]], 9 | "comments": false 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | client/src/assets/fonts 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // https://github.com/vuejs/eslint-config-vue 4 | extends: 'vue', 5 | // required to lint *.vue files 6 | plugins: [ 7 | 'html' 8 | ], 9 | // add your custom rules here 10 | 'rules': { 11 | "no-sequences": [0], 12 | "no-debugger": process.env.NODE_ENV === 'production' ? 2 : 0 13 | }, 14 | globals: { 15 | "it": true, 16 | "describe": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 erguotou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server/app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-fullstack-demo 2 | **Deprecated! Now demo project was generated by script automatically. Please check [https://github.com/erguotou520/vue-fullstack/tree/vf-backend](https://github.com/erguotou520/vue-fullstack/tree/vf-backend) for backend server and [https://github.com/erguotou520/vue-fullstack/tree/vf-mock](https://github.com/erguotou520/vue-fullstack/tree/vf-mock) for mock server** 3 | 4 | This is a NodeJs fullstack project using `express`, `mongodb`, `passport`, `vue`, `vue-router`, `vuex`, etc. 5 | 6 | ## Feature 7 | - Separate for backend and frontend when development 8 | - Configurable 9 | - Restfull api 10 | 11 | ## Before dev 12 | 1. Install `mongodb` follow [official manual](https://docs.mongodb.com/manual/installation/). It's recommend to use [MongoChef](3t.io/mongochef/) as the db client. 13 | 2. NodeJs installed. 14 | 15 | ## Dev step 16 | 1. Open terminal and run `npm install`, if you don't choose i18n when initialization, you need to run `npm run remove:i18n` here manually 17 | 2. Run `npm run server`, this will initial the db and `User` document if not exists 18 | 3. Open other terminal and run `npm run client`, you can combine the two command with `npm run dev` 19 | 4. Open browser and nav to `localhost:9001` (the default port is 9001, if you change this, change the port) 20 | 21 | ## Build 22 | Run `npm run build` 23 | 24 | ## App structure 25 | ``` 26 | ├─client # frontend source folder 27 | │ ├─build # frontend dev scripts 28 | │ ├─src # frontend src 29 | │ │ ├─assets 30 | │ │ │ ├─css 31 | │ │ │ ├─fonts 32 | │ │ │ └─images 33 | │ │ ├─components # vue components 34 | │ │ ├─http # vue-resource configuration 35 | │ │ ├─locale # vue-i18n configuration 36 | │ │ ├─router # vue-router configuration 37 | │ │ ├─socket # socket.io configuration 38 | │ │ ├─storage # web storage api 39 | │ │ ├─store # vuex store 40 | │ │ │ └─modules 41 | │ │ └─view # app pages 42 | │ │ └─auth 43 | │ └─static # static folder 44 | └─server # backend server folder 45 | ├─api # backend api list 46 | │ ├─thing 47 | │ └─user 48 | ├─auth # user auth logical 49 | │ └─local 50 | ├─components # server components 51 | │ └─errors 52 | ├─config # server configs, contains express socket.io, etc. 53 | └─views # server servered pages 54 | ``` 55 | 56 | ## Configuration 57 | Most of the configuration is concentrated in the `config.js` file, and most of them have explicit comments, you need to take a look at it first. 58 | 59 | Here is some important/frequently-used configuration: 60 | - `frontend.port` port that frontend server listens at 61 | - `backend.port` port that backend server listen at 62 | - `backend.secrets.session` secret for session, important when you deploy your app, make sure it's complex enough 63 | - `backend.mongo.uri` change this if your mongodb uri is not matched 64 | - `backend.serverFrontend` whether to server the frontend code. If set to `true` the express server servers the frontend code, otherwise you may need a http server like nginx to server frontend code and there is a nginx configuration at `nginx.example.conf` (default true) 65 | 66 | ## Environment variable 67 | When you deploy your app to you cloud server, it's easy to config you app with `environment variable`, here is the supported: 68 | - `APP_port` or `PORT`: set to `backend.port` 69 | - `APP_HOST` or `APP_IP` or `HOST` or `IP`: set to `backend.ip` 70 | - `MONGODB_URI` or `MONGOHQ_URI`: set to `backend.mongo.uri` 71 | - `SECRET`: set to `backend.secrets.session` 72 | 73 | ## Notice 74 | The generated app is just a template to build your app system fast, maybe it can't meet your needs, so you need to do some change at this issue. 75 | 76 | ## License 77 | Under [MIT license](./LICENSE) 78 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Frontend folder 2 | -------------------------------------------------------------------------------- /client/build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('shelljs/global') 3 | var path = require('path') 4 | process.env.NODE_ENV = 'production' 5 | var config = require('../../config').frontend 6 | var ora = require('ora') 7 | var webpack = require('webpack') 8 | var webpackConfig = require('./webpack.prod.conf') 9 | 10 | console.log( 11 | ' Tip:\n' + 12 | ' Built files are meant to be served over an HTTP server.\n' + 13 | ' Opening index.html over file:// won\'t work.\n' 14 | ) 15 | 16 | var spinner = ora('building for production...') 17 | spinner.start() 18 | 19 | var assetsPath = path.join(config.assetsRoot, config.assetsSubDirectory) 20 | rm('-rf', assetsPath) // eslint-disable-line 21 | mkdir('-p', assetsPath) // eslint-disable-line 22 | cp('-R', 'client/static/', assetsPath) // eslint-disable-line 23 | 24 | webpack(webpackConfig, function (err, stats) { 25 | spinner.stop() 26 | if (err) throw err 27 | process.stdout.write(stats.toString({ 28 | colors: true, 29 | modules: false, 30 | children: false, 31 | chunks: false, 32 | chunkModules: false 33 | }) + '\n') 34 | }) 35 | -------------------------------------------------------------------------------- /client/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /client/build/dev-server.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development' 2 | var config = require('../../config').frontend 3 | var path = require('path') 4 | var express = require('express') 5 | var webpack = require('webpack') 6 | var proxyMiddleware = require('http-proxy-middleware') 7 | var webpackConfig = require('./webpack.dev.conf') 8 | 9 | // default port where dev server listens for incoming traffic 10 | var port = config.port || 8080 11 | // Define HTTP proxies to your custom API backend 12 | // https://github.com/chimurai/http-proxy-middleware 13 | var proxyTable = config.proxyTable 14 | 15 | var app = express() 16 | var compiler = webpack(webpackConfig) 17 | 18 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 19 | publicPath: webpackConfig.output.publicPath, 20 | stats: { 21 | colors: true, 22 | chunks: false 23 | } 24 | }) 25 | 26 | var hotMiddleware = require('webpack-hot-middleware')(compiler) 27 | // force page reload when html-webpack-plugin template changes 28 | compiler.plugin('compilation', function (compilation) { 29 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 30 | hotMiddleware.publish({ action: 'reload' }) 31 | cb() 32 | }) 33 | }) 34 | 35 | // proxy api requests 36 | Object.keys(proxyTable).forEach(function (context) { 37 | var options = proxyTable[context] 38 | if (typeof options === 'string') { 39 | options = { target: options } 40 | } 41 | app.use(proxyMiddleware(context, options)) 42 | }) 43 | 44 | // handle fallback for HTML5 history API 45 | app.use(require('connect-history-api-fallback')()) 46 | 47 | // serve webpack bundle output 48 | app.use(devMiddleware) 49 | 50 | // enable hot-reload and state-preserving 51 | // compilation error display 52 | app.use(hotMiddleware) 53 | 54 | // serve pure static assets 55 | var staticPath = path.posix.join(config.assetsPublicPath, config.assetsSubDirectory) 56 | app.use(staticPath, express.static(path.join(__dirname, '../static'))) 57 | 58 | module.exports = app.listen(port, function (err) { 59 | if (err) { 60 | console.log(err) 61 | return 62 | } 63 | var uri = 'http://localhost:' + port 64 | console.log('Listening at ' + uri + '\n') 65 | }) 66 | -------------------------------------------------------------------------------- /client/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../../config').frontend 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = config.assetsSubDirectory 7 | return path.posix.join(assetsSubDirectory, _path) 8 | } 9 | 10 | exports.cssLoaders = function (options) { 11 | options = options || {} 12 | // generate loader string to be used with extract text plugin 13 | function generateLoaders (loaders) { 14 | var sourceLoader = loaders.map(function (loader) { 15 | var extraParamChar 16 | if (/\?/.test(loader)) { 17 | loader = loader.replace(/\?/, '-loader?') 18 | extraParamChar = '&' 19 | } else { 20 | loader = loader + '-loader' 21 | extraParamChar = '?' 22 | } 23 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 24 | }).join('!') 25 | 26 | // Extract CSS when that option is specified 27 | // (which is the case during production build) 28 | if (options.extract) { 29 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader) 30 | } else { 31 | return ['vue-style-loader', sourceLoader].join('!') 32 | } 33 | } 34 | 35 | // http://vuejs.github.io/vue-loader/configurations/extract-css.html 36 | return { 37 | css: generateLoaders(['css']), 38 | postcss: generateLoaders(['css']), 39 | less: generateLoaders(['css', 'less']), 40 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 41 | scss: generateLoaders(['css', 'sass']), 42 | stylus: generateLoaders(['css', 'stylus']), 43 | styl: generateLoaders(['css', 'stylus']) 44 | } 45 | } 46 | 47 | // Generate loaders for standalone style files (outside of .vue) 48 | exports.styleLoaders = function (options) { 49 | var output = [] 50 | var loaders = exports.cssLoaders(options) 51 | for (var extension in loaders) { 52 | var loader = loaders[extension] 53 | output.push({ 54 | test: new RegExp('\\.' + extension + '$'), 55 | loader: loader 56 | }) 57 | } 58 | return output 59 | } 60 | -------------------------------------------------------------------------------- /client/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../../config').frontend 3 | var utils = require('./utils') 4 | var projectRoot = path.resolve(__dirname, '../') 5 | 6 | module.exports = { 7 | entry: { 8 | app: path.resolve(__dirname, '../src/main.js') 9 | }, 10 | output: { 11 | path: config.assetsRoot, 12 | publicPath: config.assetsPublicPath, 13 | filename: '[name].js' 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.vue'], 17 | fallback: [path.join(__dirname, '../../node_modules')], 18 | alias: { 19 | 'src': path.resolve(__dirname, '../src'), 20 | 'assets': path.resolve(__dirname, '../src/assets'), 21 | 'components': path.resolve(__dirname, '../src/components'), 22 | 'resources': path.resolve(__dirname, '../src/resources') 23 | } 24 | }, 25 | resolveLoader: { 26 | fallback: [path.join(__dirname, '../../node_modules')] 27 | }, 28 | module: { 29 | preLoaders: [ 30 | { 31 | test: /\.vue$/, 32 | loader: 'eslint', 33 | include: projectRoot, 34 | exclude: /node_modules/ 35 | }, 36 | { 37 | test: /\.js$/, 38 | loader: 'eslint', 39 | include: projectRoot, 40 | exclude: /node_modules/ 41 | } 42 | ], 43 | loaders: [ 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue' 47 | }, 48 | { 49 | test: /\.js$/, 50 | loader: 'babel', 51 | include: projectRoot, 52 | exclude: /node_modules/ 53 | }, 54 | { 55 | test: /\.json$/, 56 | loader: 'json' 57 | }, 58 | { 59 | test: /\.html$/, 60 | loader: 'vue-html' 61 | }, 62 | { 63 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 64 | loader: 'url', 65 | query: { 66 | limit: 10000, 67 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url', 73 | query: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | eslint: { 81 | formatter: require('eslint-friendly-formatter') 82 | }, 83 | vue: { 84 | loaders: utils.cssLoaders({ sourceMap: config.cssSourceMap }), 85 | postcss: [ 86 | require('autoprefixer')({ 87 | browsers: ['last 2 versions'] 88 | }) 89 | ] 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /client/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var config = require('../../config').frontend 2 | var webpack = require('webpack') 3 | var merge = require('webpack-merge') 4 | var utils = require('./utils') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | 8 | // add hot-reload related code to entry chunks 9 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 10 | baseWebpackConfig.entry[name] = ['./client/build/dev-client'].concat(baseWebpackConfig.entry[name]) 11 | }) 12 | 13 | module.exports = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.cssSourceMap }) 16 | }, 17 | // eval-source-map is faster for development 18 | devtool: '#source-map', 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env': '"development"' 22 | }), 23 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 24 | new webpack.optimize.OccurenceOrderPlugin(), 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'client/index.html', 31 | inject: true 32 | }) 33 | ] 34 | }) 35 | -------------------------------------------------------------------------------- /client/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../../config').frontend 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | 10 | var webpackConfig = merge(baseWebpackConfig, { 11 | module: { 12 | loaders: utils.styleLoaders({ sourceMap: config.cssSourceMap, extract: true }) 13 | }, 14 | devtool: config.cssSourceMap ? '#source-map' : false, 15 | output: { 16 | path: config.assetsRoot, 17 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 18 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 19 | }, 20 | vue: { 21 | loaders: utils.cssLoaders({ 22 | sourceMap: config.productionSourceMap, 23 | extract: true 24 | }) 25 | }, 26 | plugins: [ 27 | // http://vuejs.github.io/vue-loader/workflow/production.html 28 | new webpack.DefinePlugin({ 29 | 'process.env': '"production"' 30 | }), 31 | new webpack.optimize.UglifyJsPlugin({ 32 | compress: { 33 | warnings: false 34 | } 35 | }), 36 | new webpack.optimize.OccurenceOrderPlugin(), 37 | // extract css into its own file 38 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 39 | // generate dist index.html with correct asset hash for caching. 40 | // you can customize output by editing /index.html 41 | // see https://github.com/ampedandwired/html-webpack-plugin 42 | new HtmlWebpackPlugin({ 43 | filename: config.index, 44 | template: 'client/index.html', 45 | inject: true, 46 | minify: { 47 | removeComments: true, 48 | collapseWhitespace: true, 49 | removeAttributeQuotes: true 50 | // more options: 51 | // https://github.com/kangax/html-minifier#options-quick-reference 52 | }, 53 | chunks: ['manifest', 'vendor', 'element', 'app'], 54 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 55 | chunksSortMode: function (a, b) { 56 | var orders = ['manifest', 'vendor', 'element', 'app'] 57 | var order1 = orders.indexOf(a.names[0]) 58 | var order2 = orders.indexOf(b.names[0]) 59 | if (order1 > order2) { 60 | return 1 61 | } else if (order1 < order2) { 62 | return -1 63 | } else { 64 | return 0 65 | } 66 | } 67 | }), 68 | // split vendor js into its own file 69 | new webpack.optimize.CommonsChunkPlugin({ 70 | name: 'vendor', 71 | minChunks: function (module, count) { 72 | // any required modules inside node_modules are extracted to vendor 73 | return ( 74 | module.resource && 75 | /\.js$/.test(module.resource) && 76 | module.resource.indexOf( 77 | path.join(__dirname, '../../node_modules') 78 | ) === 0 79 | ) 80 | } 81 | }), 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'element', 84 | minChunks: function (module, count) { 85 | // element-ui will extracted to element 86 | return ( 87 | module.resource.indexOf( 88 | path.join(__dirname, '../../node_modules/element-ui') 89 | ) === 0 90 | ) 91 | } 92 | }), 93 | // extract webpack runtime and module manifest to its own file in order to 94 | // prevent vendor hash from being updated whenever app bundle is updated 95 | new webpack.optimize.CommonsChunkPlugin({ 96 | name: 'manifest', 97 | chunks: ['vendor', 'element'] 98 | }) 99 | ] 100 | }) 101 | 102 | if (config.productionGzip) { 103 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 104 | 105 | webpackConfig.plugins.push( 106 | new CompressionWebpackPlugin({ 107 | asset: '[path].gz[query]', 108 | algorithm: 'gzip', 109 | test: new RegExp( 110 | '\\.(' + 111 | config.productionGzipExtensions.join('|') + 112 | ')$' 113 | ), 114 | threshold: 10240, 115 | minRatio: 0.8 116 | }) 117 | ) 118 | } 119 | 120 | module.exports = webpackConfig 121 | -------------------------------------------------------------------------------- /client/dist/index.html: -------------------------------------------------------------------------------- 1 | vue-fullstack-demo
-------------------------------------------------------------------------------- /client/dist/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/dist/static/favicon.ico -------------------------------------------------------------------------------- /client/dist/static/fonts/element-icons.a61be9c.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/dist/static/fonts/element-icons.a61be9c.eot -------------------------------------------------------------------------------- /client/dist/static/fonts/element-icons.b02bdc1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/dist/static/fonts/element-icons.b02bdc1.ttf -------------------------------------------------------------------------------- /client/dist/static/img/element-icons.09162bc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Tue Sep 6 12:16:07 2016 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 57 | 60 | 63 | 66 | 69 | 72 | 75 | 77 | 79 | 83 | 85 | 87 | 90 | 93 | 96 | 99 | 101 | 104 | 107 | 109 | 112 | 117 | 121 | 125 | 128 | 131 | 134 | 138 | 142 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /client/dist/static/img/login-bg.bd93cbf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/dist/static/img/login-bg.bd93cbf.jpg -------------------------------------------------------------------------------- /client/dist/static/js/0.dec371543c6055348ce2.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0,6],{101:function(e,t,a){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var n=a(4),i=r(n),o=a(7),s=a(6),l=a(203),c=r(l);t.default={props:{data:{type:Array,required:!0},rowKey:String,showPagination:{type:Boolean,default:!0}},data:function(){return{avaliableHeight:0,pending:!1,page:{current:1,limit:0,total:0}}},computed:(0,i.default)({},(0,o.mapGetters)(["globalConfig"])),watch:{"globalConfig.pageLimit":function(e){this.page.current=1,this.page.limit=e,this.$emit("page-change",1)}},components:{Pagination:c.default},methods:{calcTableAvaliableHeight:function(){var e=this;this.$nextTick(function(){e.avaliableHeight=e.$refs.wrapper.clientHeight})},query:function(e,t){var a=this;t=t||this.page.current,this.pending=!0;for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i0&&void 0!==arguments[0]?arguments[0]:1;this.$refs.users.query(o.user,t,{search:this.search}).then(function(t){e.users=t}).catch(function(e){console.error(e)})},createUser:function(){this.formVisible=!0},cancelForm:function(){this.form.username="",this.form.password="",this.formVisible=!1},saveForm:function(){var e=this;this.$refs.form.validate(function(t){t&&o.user.save(null,e.form).then(function(){e.cancelForm(),e.$message({type:"success",message:e.$t("message.created")}),e.fetch()}).catch(function(t){e.$message({type:"error",message:422===t.status?e.$t("users.action.userExisted"):e.$t("message.createFailed")})})})},deleteUser:function(e){var t=this;this.$confirm("This action will remove the selected user: "+e.username+" forever, still going on?",this.$t("message.confirm.title"),{type:"warning"}).then(function(){o.user.delete({_id:e._id}).then(function(){t.$message({type:"success",message:t.$t("message.removed")}),t.fetch()})}).catch(function(){})}},mounted:function(){var e=this;this.$nextTick(function(){e.fetch()})}}},149:function(e,t,a){t=e.exports=a(15)(),t.push([e.id,".ui-data-table .toolbar{margin-bottom:.5rem}.ui-data-table .toolbar .el-form-item{margin-bottom:0}.ui-data-table .data-table{position:relative;margin:0 -1rem;font-size:.75rem;border-left:none;border-right:none}.ui-data-table .data-table .el-table:before{background:none}.ui-data-table .data-table>.data-loading{position:absolute;left:0;right:0;top:0;bottom:0;background-color:hsla(0,0%,100%,.9);z-index:10}.ui-data-table .data-table>.data-loading .loader-inner{position:relative}.ui-data-table .data-table>.data-loading .loader-inner>div{position:absolute;left:0;top:0;width:35px;height:35px;border:2px solid #20a0ff;border-bottom-color:transparent;border-top-color:transparent;border-radius:100%;animation:rotate 1s 0s ease-in-out infinite;animation-fill-mode:both}.ui-data-table .data-table>.data-loading .loader-inner>div:last-child{display:inline-block;left:10px;top:10px;width:15px;height:15px;animation-duration:.5s;border-color:#20a0ff transparent;animation-direction:reverse}.ui-data-table .data-table .actions{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.ui-data-table .data-table .actions .el-tooltip{margin-left:1rem}.ui-data-table .data-table .actions .el-tooltip:first-child{margin-left:0}.ui-data-table .data-table .actions .el-tooltip .el-tooltip__rel{display:inline-block;line-height:1rem}.ui-data-table .data-table .actions .icon{margin-left:.5rem;line-height:1rem;font-size:1rem;color:#20a0ff;cursor:pointer}.ui-data-table .data-table .actions .icon:first-child{margin-left:0}.ui-data-table .data-table .actions .icon:hover{color:#58b7ff}.ui-data-table .data-table .fade-enter-active,.ui-data-table .data-table .fade-leave-active{transition:opacity .5s}.ui-data-table .data-table .fade-enter,.ui-data-table .data-table .fade-leave-active{opacity:0}@media (max-width:64rem){.ui-data-table .data-table .actions .el-tooltip{margin-left:.5rem}}",""])},150:function(e,t,a){t=e.exports=a(15)(),t.push([e.id,".ui-pagination{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:center;align-items:center;margin:0 -1rem -1rem;padding:.75rem 1rem;border-top:1px solid #d3dce6;background-color:#fff}.ui-pagination .navs>a{float:left;width:2rem;height:1.75rem;line-height:1.75rem;text-align:center;border-right:1px solid #d3dce6;background-color:#fff;color:#475669;border:1px solid #d3dce6;border-right:none;cursor:pointer}.ui-pagination .navs>a:last-child{border-right:1px solid #d3dce6}.ui-pagination .navs>a[disabled]{color:#c0ccda;cursor:not-allowed;background-color:#eff2f7}.ui-pagination .navs>a:hover{background-color:#20a0ff;color:#fff;border-color:#20a0ff}",""])},171:function(e,t,a){var r=a(149);"string"==typeof r&&(r=[[e.id,r,""]]);a(16)(r,{});r.locals&&(e.exports=r.locals)},172:function(e,t,a){var r=a(150);"string"==typeof r&&(r=[[e.id,r,""]]);a(16)(r,{});r.locals&&(e.exports=r.locals)},199:function(e,t,a){var r,n;a(171),r=a(101);var i=a(209);n=r=r||{},"object"!=typeof r.default&&"function"!=typeof r.default||(n=r=r.default),"function"==typeof n&&(n=n.options),n.render=i.render,n.staticRenderFns=i.staticRenderFns,e.exports=r},203:function(e,t,a){var r,n;a(172),r=a(105);var i=a(210);n=r=r||{},"object"!=typeof r.default&&"function"!=typeof r.default||(n=r=r.default),"function"==typeof n&&(n=n.options),n.render=i.render,n.staticRenderFns=i.staticRenderFns,e.exports=r},207:function(e,t,a){var r,n;r=a(108);var i=a(218);n=r=r||{},"object"!=typeof r.default&&"function"!=typeof r.default||(n=r=r.default),"function"==typeof n&&(n=n.options),n.render=i.render,n.staticRenderFns=i.staticRenderFns,e.exports=r},209:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("div",{staticClass:"ui-data-table flex flex-1 flex-column"},[e.$slots.toolbar?a("div",{staticClass:"toolbar"},[e._t("toolbar")],2):e._e(),e._v(" "),a("div",{ref:"wrapper",staticClass:"data-table flex flex-1"},[a("el-table",{attrs:{data:e.data,border:"",height:e.avaliableHeight,"row-key":function(t){return t[e.rowKey]}}},[e._t("default")],2),e._v(" "),e._t("table"),e._v(" "),a("transition",{attrs:{name:"fade"}},[a("div",{directives:[{name:"show",rawName:"v-show",value:e.pending,expression:"pending"}],staticClass:"data-loading flex flex-main-center flex-cross-center"},[a("div",{staticClass:"loader-inner"},[a("div"),e._v(" "),a("div")])])])],2),e._v(" "),e.showPagination?a("pagination",{attrs:{current:e.page.current,total:e.page.total||0},on:{"page-change":e.onPageChange}}):e._e()],1)},staticRenderFns:[]}},210:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("div",{staticClass:"ui-pagination"},[a("span",{staticClass:"current"},[e._v(e._s(e.$t("pagination.current"))+" "+e._s(e.current)+" "+e._s(e.$t("pagination.currentAppend"))+"  "+e._s(e.$t("pagination.pages"))+" "+e._s(e.pages)+" "+e._s(e.$t("pagination.pagesAppend")))]),e._v(" "),a("div",{staticClass:"navs"},[a("a",{attrs:{disabled:e.current<=1},on:{click:function(t){e.change(1,e.current<=1)}}},[a("span",{staticClass:"iconfont icon-home-page"})]),e._v(" "),a("a",{attrs:{disabled:e.current<=1},on:{click:function(t){e.change(e.current-1,e.current<=1)}}},[a("el-icon",{attrs:{name:"arrow-left"}})],1),e._v(" "),a("a",{attrs:{disabled:e.current>=e.pages},on:{click:function(t){e.change(e.current+1,e.current>=e.pages)}}},[a("el-icon",{attrs:{name:"arrow-right"}})],1),e._v(" "),a("a",{attrs:{disabled:e.current>=e.pages},on:{click:function(t){e.change(e.pages,e.current>=e.pages)}}},[a("span",{staticClass:"iconfont icon-last-page"})]),e._v(" "),a("a",{on:{click:function(t){e.$emit("page-change",e.current)}}},[a("span",{staticClass:"iconfont icon-refresh"})])])])},staticRenderFns:[]}},218:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("content-module",{attrs:{name:"users"}},[a("el-breadcrumb",{staticStyle:{"margin-bottom":".5rem"},attrs:{separator:"/"}},[a("el-breadcrumb-item",{attrs:{to:"/dashboard"}},[e._v(e._s(e.$t("users.breadcrumb.home")))]),e._v(" "),a("el-breadcrumb-item",[e._v(e._s(e.$t("users.breadcrumb.current")))])],1),e._v(" "),a("data-table",{ref:"users",attrs:{data:e.users,"row-key":"_id"},on:{"page-change":e.fetch}},[a("div",{slot:"toolbar"},[a("el-button",{attrs:{type:"primary",icon:"plus"},nativeOn:{click:function(t){e.createUser(t)}}},[e._v(e._s(e.$t("toolbar.create")))])],1),e._v(" "),a("el-table-column",{attrs:{property:"_id",label:"ID",sortable:"","min-width":"120"}}),e._v(" "),a("el-table-column",{attrs:{property:"username",label:e.$t("users.model.username"),sortable:"","min-width":"120"}}),e._v(" "),a("el-table-column",{attrs:{property:"role",label:e.$t("users.model.role"),"min-width":"90"}}),e._v(" "),a("el-table-column",{attrs:{label:e.$t("datatable.operate"),align:"center",width:"100"},scopedSlots:{default:function(t){return[a("el-button",{attrs:{type:"text"},nativeOn:{click:function(a){e.deleteUser(t.row)}}},[e._v(e._s(e.$t("toolbar.remove")))])]}}})],1),e._v(" "),a("el-dialog",{directives:[{name:"model",rawName:"v-model",value:e.formVisible,expression:"formVisible"}],attrs:{title:e.$t("users.create.title")},domProps:{value:e.formVisible},on:{close:e.cancelForm,input:function(t){e.formVisible=t}}},[a("el-form",{ref:"form",attrs:{model:e.form,rules:e.rules,"close-on-click-modal":!1,"close-on-press-escape":!1}},[a("el-form-item",{attrs:{label:e.$t("users.model.username"),prop:"username"}},[a("el-input",{directives:[{name:"model",rawName:"v-model",value:e.form.username,expression:"form.username"}],domProps:{value:e.form.username},on:{input:function(t){e.form.username=t}}})],1),e._v(" "),a("el-form-item",{attrs:{label:e.$t("users.model.password"),prop:"password"}},[a("el-input",{directives:[{name:"model",rawName:"v-model",value:e.form.password,expression:"form.password"}],attrs:{type:"password"},domProps:{value:e.form.password},on:{input:function(t){e.form.password=t}}})],1)],1),e._v(" "),a("span",{staticClass:"dialog-footer",slot:"footer"},[a("el-button",{nativeOn:{click:function(t){e.formVisible=!1}}},[e._v(e._s(e.$t("message.confirm.cancel")))]),e._v(" "),a("el-button",{attrs:{type:"primary"},nativeOn:{click:function(t){e.saveForm(t)}}},[e._v(e._s(e.$t("message.confirm.ok")))])],1)],1)],1)},staticRenderFns:[]}}}); 2 | //# sourceMappingURL=0.dec371543c6055348ce2.js.map -------------------------------------------------------------------------------- /client/dist/static/js/1.c70df63b0d01f3a929cc.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([1,6],{46:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={breadcrumb:{home:"Home",current:"Things"},model:{name:"name",description:"description"},rules:{name:"Please input the name"},edit:{create:"Add thing",update:"Update thing"}}},47:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={breadcrumb:{home:"Home",current:"Things"},model:{name:"name",description:"description"},rules:{name:"Please input the name"},edit:{create:"Add thing",update:"Update thing"}}},48:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={breadcrumb:{home:"首页",current:"事件管理"},model:{name:"名字",description:"描述"},rules:{name:"请输入名称"},edit:{create:"新增事件",update:"编辑事件"}}},50:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={breadcrumb:{home:"Home",current:"Users"},model:{username:"Username",role:"Role",password:"Password"},create:{title:"Create a user"},rules:{username:"Please input the username",password:"Please input the password"},action:{userExisted:"User existed"}}},51:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={breadcrumb:{home:"Home",current:"Users"},model:{username:"Username",role:"Role",password:"Password"},create:{title:"Create a user"},rules:{username:"Please input the username",password:"Please input the password"},action:{userExisted:"User existed"}}},52:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={breadcrumb:{home:"首页",current:"用户管理"},model:{username:"用户名",role:"角色",password:"密码"},create:{title:"创建用户"},rules:{username:"请输入用户名",password:"请输入密码"},action:{userExisted:"用户已存在"}}},88:function(e,t,s){function r(e){return s(a(e))}function a(e){return n[e]||function(){throw new Error("Cannot find module '"+e+"'.")}()}var n={"./en.js":92,"./en_US.js":93,"./index.js":45,"./things/en.js":46,"./things/en_US.js":47,"./things/zh-CN.js":48,"./things/zh_CN.js":49,"./users/en.js":50,"./users/en_US.js":51,"./users/zh-CN.js":52,"./users/zh_CN.js":53,"./zh-CN.js":94,"./zh_CN.js":54};r.keys=function(){return Object.keys(n)},r.resolve=a,e.exports=r,r.id=88},92:function(e,t,s){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=s(6),n=s(50),o=r(n),d=s(46),i=r(d),u={el:{select:{noData:"no matched data"}},config:{title:"xxx Backend System",description:"Description"},message:{save:{ok:"Saved!",err:"Error occured when saving!"},confirm:{title:"warning",ok:"save",cancel:"cancel"},error:"Error",created:"Create successed",createFailed:"Create failed",updated:"Update successed",updateFailed:"Update failed",removed:"Delete successed",removeFailed:"Delete failed"},http:{error:{E401:"Not authorized",E403:"Permission not allowed",E404:"Url not found",E500:"Server error",others:"Some error occured, please try again"}},header:{settings:"User settings",password:"Password",logout:"Logout",langSetting:"Lang",pageLimit:"Data count limit per page",_password:{description:"Change your password. It's strongly recommend that you should pick a complex password.",old:"Old password",_new:"New password",newConfirm:"Confirm new password",rules:{old:"Please input old password",_new:"Please input new password",newConfirm:"Please input new password again",notMatch:"The two new password not matched"},afterChange:"Password has changed, system will logout automaticaly, please re-login with the new password."}},menu:{users:"Users",things:"Things"},toolbar:{create:"Add",remove:"Delete"},datatable:{operate:"Operate"},pagination:{current:"current",currentAppend:"page",total:"total",pages:"total",pagesAppend:"page"},login:{username:"Please input the username",password:"Please input the password",button:"Log in",authFail:"Username or password is not correct"}};t.default=(0,a.assign)({},u,{users:o.default,things:i.default})},93:function(e,t,s){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=s(6),n=s(51),o=r(n),d=s(47),i=r(d),u={el:{select:{noData:"no matched data"}},config:{title:"xxx Backend System",description:"Description"},message:{save:{ok:"Saved!",err:"Error occured when saving!"},confirm:{title:"warning",ok:"save",cancel:"cancel"},created:"Create successed",createFailed:"Create failed",updated:"Update successed",updateFailed:"Update failed",removed:"Delete successed",removeFailed:"Delete failed"},http:{error:{E401:"Not authorized",E404:"Url not found",E500:"Server error",others:"Some error occured, please try again"}},header:{settings:"User settings",password:"Password",logout:"Logout",localeSetting:"Locale",pageLimit:"Data count limit per page",_password:{description:"Change your password. It's strongly recommend that you should pick a complex password.",old:"Old password",_new:"New password",newConfirm:"Confirm new password",rules:{old:"Please input old password",_new:"Please input new password",newConfirm:"Please input new password again",notMatch:"The two new password not matched"},afterChange:"Password has changed, system will logout automaticaly, please re-login with the new password."}},menu:{users:"Users",things:"Things"},toolbar:{create:"Add",remove:"Delete"},datatable:{operate:"Operate"},pagination:{current:"current",currentAppend:"page",total:"total",pages:"total",pagesAppend:"page"},login:{username:"Please input the username",password:"Please input the password",button:"Log in"}};t.default=(0,a.assign)({},u,{users:o.default,things:i.default})},94:function(e,t,s){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=s(6),n=s(52),o=r(n),d=s(48),i=r(d),u={el:{select:{noData:"无匹配数据"}},config:{title:"xxx 后台管理系统",description:"描述"},message:{save:{ok:"已保存!",err:"保存失败!"},confirm:{title:"提示",ok:"确 定",cancel:"取 消"},error:"错误",created:"新增成功",createFailed:"新增失败",updated:"已保存更改",updateFailed:"更新失败",removed:"删除成功",removeFailed:"删除失败"},http:{error:{E401:"身份认证失败",E403:"权限不足",E404:"请求路径错误",E500:"后台错误",others:"操作失败,请重试"}},header:{settings:"用户设置",password:"修改密码",logout:"退出",langSetting:"语言",pageLimit:"每页条目数",_password:{description:"修改你的密码。强烈建议您选择一个复杂密码。",old:"旧密码",_new:"新密码",newConfirm:"确认新密码",rules:{old:"请输入旧密码",_new:"请输入新密码",newConfirm:"请再次确认新密码",notMatch:"两次输入的新密码不一致"},afterChange:"密码已修改,将自动退出,请使用新密码重新登录。"}},menu:{users:"用户管理",things:"事件管理"},toolbar:{create:"添加",remove:"删除"},datatable:{operate:"操作"},pagination:{current:"当前第",currentAppend:"页",pages:"共",pagesAppend:"页"},login:{username:"请输入用户名",password:"请输入密码",button:"登录",authFail:"用户名或密码错误"}};t.default=(0,a.assign)({},u,{users:o.default,things:i.default})}}); 2 | //# sourceMappingURL=1.c70df63b0d01f3a929cc.js.map -------------------------------------------------------------------------------- /client/dist/static/js/2.6d969129a747a4e40bf0.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([2,6],{109:function(e,r,o){"use strict";function t(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(r,"__esModule",{value:!0});var n=o(4),i=t(n),s=o(7);r.default={data:function(){return{form:{username:"",password:""},rules:{username:[{required:!0,message:this.$t("login.username"),trigger:"blur"}],password:[{required:!0,message:this.$t("login.password"),trigger:"blur"}]},loading:!1,valid:!1,loginError:!1}},computed:(0,i.default)({},(0,s.mapGetters)(["loggedIn"])),methods:(0,i.default)({},(0,s.mapActions)(["login"]),{onSubmit:function(){var e=this;this.$refs.form.validate(function(r){r&&(e.loading=!0,e.login({username:e.form.username,password:e.form.password}).then(function(r){e.loading=!1,e.$router.push(e.$route.query.redirect||"/")}).catch(function(r){e.$notify({title:e.$t("message.error"),message:r.message||e.$t("login.authFail"),type:"error",duration:1500}),e.loading=!1,e.loginError=!0,setTimeout(function(){e.loginError=!1},500)}))})}})}},151:function(e,r,o){r=e.exports=o(15)(),r.push([e.id,".login-wrapper{-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.login-wrapper .bg{position:absolute;left:0;right:0;top:0;bottom:0;width:100%;height:100%;background-size:cover;background-position:100%;background-image:url("+o(195)+")}.login-wrapper>h1{position:relative;margin:0 0 1rem;text-align:center;z-index:1}.login-wrapper>form{width:15rem;margin:0 auto}.login-wrapper>form .el-input__inner{color:#475669;border-color:#99a9bf;background-color:transparent}.login-wrapper>form .el-input__inner:focus{color:#1f2d3d;border-color:#1f2d3d}.login-wrapper .login-button{width:100%}.login-wrapper .login-button.error{animation:shake .5s}",""])},175:function(e,r,o){var t=o(151);"string"==typeof t&&(t=[[e.id,t,""]]);o(16)(t,{});t.locals&&(e.exports=t.locals)},195:function(e,r,o){e.exports=o.p+"static/img/login-bg.bd93cbf.jpg"},208:function(e,r,o){var t,n;o(175),t=o(109);var i=o(214);n=t=t||{},"object"!=typeof t.default&&"function"!=typeof t.default||(n=t=t.default),"function"==typeof n&&(n=n.options),n.render=i.render,n.staticRenderFns=i.staticRenderFns,e.exports=t},214:function(e,r){e.exports={render:function(){var e=this,r=e.$createElement,o=e._self._c||r;return o("div",{directives:[{name:"show",rawName:"v-show",value:!e.loggedIn,expression:"!loggedIn"}],staticClass:"login-wrapper"},[o("div",{staticClass:"bg"}),e._v(" "),o("h1",[e._v(e._s(e.$t("config.title")))]),e._v(" "),o("el-form",{ref:"form",attrs:{model:e.form,rules:e.rules},nativeOn:{submit:function(r){r.preventDefault(),e.onSubmit(r)}}},[o("el-form-item",{attrs:{prop:"username"}},[o("el-input",{directives:[{name:"model",rawName:"v-model",value:e.form.username,expression:"form.username"}],attrs:{placeholder:e.$t("login.username")},domProps:{value:e.form.username},on:{input:function(r){e.form.username=r}}})],1),e._v(" "),o("el-form-item",{attrs:{prop:"password"}},[o("el-input",{directives:[{name:"model",rawName:"v-model",value:e.form.password,expression:"form.password"}],attrs:{type:"password",placeholder:e.$t("login.password")},domProps:{value:e.form.password},on:{input:function(r){e.form.password=r}}})],1),e._v(" "),o("el-form-item",[o("el-button",{staticClass:"login-button",class:{error:e.loginError},attrs:{type:"success","native-type":"submit",loading:e.loading}},[e._v(e._s(e.$t("login.button")))])],1)],1)],1)},staticRenderFns:[]}}}); 2 | //# sourceMappingURL=2.6d969129a747a4e40bf0.js.map -------------------------------------------------------------------------------- /client/dist/static/js/3.05da50d96c1f3ee80635.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([3,6],{107:function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var o=i(58),r=n(o),s=i(25);t.default={data:function(){return{formVisible:!1,form:{_id:"",name:"",info:""},rules:{name:[{required:!0,message:this.$t("things.rules.name"),trigger:"blur"}]},things:[]}},methods:{fetch:function(){var e=this;s.thing.query().then(function(e){return e.json()}).then(function(t){e.things=t.results}).catch(function(e){console.error(e)})},createThing:function(){this.formVisible=!0},cancelForm:function(){this.form._id="",this.form.name="",this.form.info="",this.formVisible=!1},saveForm:function(){var e=this,t=void 0;t=this.form._id?s.thing.update({_id:this.form._id},this.form):s.thing.save({},{name:this.form.name,info:this.form.info}),t.then(function(){e.cancelForm(),e.$message({type:"success",message:e.form._id?e.$t("message.updated"):e.$t("message.created")}),e.fetch()}).catch(function(){})},editThing:function(e){(0,r.default)(this.form,e),this.formVisible=!0},deleteThing:function(e){var t=this;this.$confirm("This action will remove the selected thing: "+e.name+" forever, still going on?",this.$t("message.confirm.title"),{type:"warning"}).then(function(){s.thing.delete({_id:e._id}).then(function(){t.$message({type:"success",message:t.$t("message.removed")}),t.fetch()})}).catch(function(){})}},mounted:function(){var e=this;this.$nextTick(function(){e.fetch()})}}},153:function(e,t,i){t=e.exports=i(15)(),t.push([e.id,".box-card[data-v-d79a5d16]{display:inline-block;width:20rem;height:15rem;margin:.5rem}.box-card .icon[data-v-d79a5d16]{float:right;margin-left:.5rem;color:#8492a6;cursor:pointer}.box-card .icon[data-v-d79a5d16]:hover{color:#20a0ff}",""])},178:function(e,t,i){var n=i(153);"string"==typeof n&&(n=[[e.id,n,""]]);i(16)(n,{});n.locals&&(e.exports=n.locals)},206:function(e,t,i){var n,o;i(178),n=i(107);var r=i(217);o=n=n||{},"object"!=typeof n.default&&"function"!=typeof n.default||(o=n=n.default),"function"==typeof o&&(o=o.options),o.render=r.render,o.staticRenderFns=r.staticRenderFns,o._scopeId="data-v-d79a5d16",e.exports=n},217:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement,i=e._self._c||t;return i("content-module",{attrs:{name:"things"}},[i("el-breadcrumb",{staticStyle:{"margin-bottom":".5rem"},attrs:{separator:"/"}},[i("el-breadcrumb-item",{attrs:{to:"/dashboard"}},[e._v(e._s(e.$t("things.breadcrumb.home")))]),e._v(" "),i("el-breadcrumb-item",[e._v(e._s(e.$t("things.breadcrumb.current")))])],1),e._v(" "),i("div",{staticStyle:{"margin-bottom":".5rem"}},[i("el-button",{attrs:{type:"primary",icon:"plus"},nativeOn:{click:function(t){e.createThing(t)}}},[e._v(e._s(e.$t("toolbar.create")))])],1),e._v(" "),i("div",e._l(e.things,function(t){return i("el-card",{staticClass:"box-card"},[i("div",{staticClass:"clearfix",slot:"header"},[i("span",[e._v(e._s(t.name))]),e._v(" "),i("i",{staticClass:"el-icon-delete icon",on:{click:function(i){e.deleteThing(t)}}}),e._v(" "),i("i",{staticClass:"el-icon-edit icon",on:{click:function(i){e.editThing(t)}}})]),e._v(" "),i("p",[e._v("\n "+e._s(t.info)+"\n ")])])})),e._v(" "),i("el-dialog",{directives:[{name:"model",rawName:"v-model",value:e.formVisible,expression:"formVisible"}],attrs:{title:e.form._id?e.$t("things.edit.update"):e.$t("things.edit.create")},domProps:{value:e.formVisible},on:{input:function(t){e.formVisible=t}}},[i("el-form",{attrs:{model:e.form,rules:e.rules}},[i("el-form-item",{attrs:{label:e.$t("things.model.name"),prop:"name"}},[i("el-input",{directives:[{name:"model",rawName:"v-model",value:e.form.name,expression:"form.name"}],domProps:{value:e.form.name},on:{input:function(t){e.form.name=t}}})],1),e._v(" "),i("el-form-item",{attrs:{label:e.$t("things.model.description")}},[i("el-input",{directives:[{name:"model",rawName:"v-model",value:e.form.info,expression:"form.info"}],domProps:{value:e.form.info},on:{input:function(t){e.form.info=t}}})],1)],1),e._v(" "),i("span",{staticClass:"dialog-footer",slot:"footer"},[i("el-button",{nativeOn:{click:function(t){e.cancelForm(t)}}},[e._v(e._s(e.$t("message.confirm.cancel")))]),e._v(" "),i("el-button",{attrs:{type:"primary"},nativeOn:{click:function(t){e.saveForm(t)}}},[e._v(e._s(e.$t("message.confirm.ok")))])],1)],1)],1)},staticRenderFns:[]}}}); 2 | //# sourceMappingURL=3.05da50d96c1f3ee80635.js.map -------------------------------------------------------------------------------- /client/dist/static/js/4.e078cf75b7acf6431fd6.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([4,6],{152:function(t,e,r){e=t.exports=r(15)(),e.push([t.id,".router-enter-active,.router-leave-active{transition:all .4s cubic-bezier(0,0,0,1)}.router-enter,.router-leave-active{opacity:.1}.router-enter{transform:translate3D(100%,0,0)}.router-leave-active{transform:translate3D(-100%,0,0)}",""])},176:function(t,e,r){var n=r(152);"string"==typeof n&&(n=[[t.id,n,""]]);r(16)(n,{});n.locals&&(t.exports=n.locals)},204:function(t,e,r){var n,a;r(176);var o=r(215);a=n=n||{},"object"!=typeof n.default&&"function"!=typeof n.default||(a=n=n.default),"function"==typeof a&&(a=a.options),a.render=o.render,a.staticRenderFns=o.staticRenderFns,t.exports=n},215:function(t,e){t.exports={render:function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("div",{staticClass:"flex flex-1",staticStyle:{width:"100%"}},[r("transition",{attrs:{name:"router",mode:"out-in"}},[r("router-view")],1)],1)},staticRenderFns:[]}}}); 2 | //# sourceMappingURL=4.e078cf75b7acf6431fd6.js.map -------------------------------------------------------------------------------- /client/dist/static/js/4.e078cf75b7acf6431fd6.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///static/js/4.e078cf75b7acf6431fd6.js","webpack:///./client/src/view/CommonView.vue?a357","webpack:///./client/src/view/CommonView.vue?2344","webpack:///./client/src/view/CommonView.vue","webpack:///./client/src/view/CommonView.vue?7b20"],"names":["webpackJsonp","152","module","exports","__webpack_require__","push","id","176","content","locals","204","__vue_exports__","__vue_options__","__vue_template__","default","options","render","staticRenderFns","215","_vm","this","_h","$createElement","_c","_self","staticClass","staticStyle","width","attrs","name","mode"],"mappings":"AAAAA,cAAc,EAAE,IAEVC,IACA,SAASC,EAAQC,EAASC,GCHhCD,EAAAD,EAAAC,QAAAC,EAAA,MAKAD,EAAAE,MAAAH,EAAAI,GAAA,wOAA+P,MDYzPC,IACA,SAASL,EAAQC,EAASC,GEfhC,GAAAI,GAAAJ,EAAA,IACA,iBAAAI,SAAAN,EAAAI,GAAAE,EAAA,KAEAJ,GAAA,IAAAI,KACAA,GAAAC,SAAAP,EAAAC,QAAAK,EAAAC,SFqCMC,IACA,SAASR,EAAQC,EAASC,GG7ChC,GAAAO,GAAAC,CAIAR,GAAA,IAGA,IAAAS,GAAAT,EAAA,IACAQ,GAAAD,QAEA,gBAAAA,GAAAG,SACA,kBAAAH,GAAAG,UAEAF,EAAAD,IAAAG,SAEA,kBAAAF,KACAA,IAAAG,SAGAH,EAAAI,OAAAH,EAAAG,OACAJ,EAAAK,gBAAAJ,EAAAI,gBAEAf,EAAAC,QAAAQ,GHoDMO,IACA,SAAShB,EAAQC,GI3EvBD,EAAAC,SAAgBa,OAAA,WAAmB,GAAAG,GAAAC,KAAaC,EAAAF,EAAAG,eAA0BC,EAAAJ,EAAAK,MAAAD,IAAAF,CAC1E,OAAAE,GAAA,OACAE,YAAA,cACAC,aACAC,MAAA,UAEGJ,EAAA,cACHK,OACAC,KAAA,SACAC,KAAA,YAEGP,EAAA,wBACFN","file":"static/js/4.e078cf75b7acf6431fd6.js","sourcesContent":["webpackJsonp([4,6],{\n\n/***/ 152:\n/***/ function(module, exports, __webpack_require__) {\n\n\texports = module.exports = __webpack_require__(15)();\n\t// imports\n\t\n\t\n\t// module\n\texports.push([module.id, \".router-enter-active,.router-leave-active{transition:all .4s cubic-bezier(0,0,0,1)}.router-enter,.router-leave-active{opacity:.1}.router-enter{transform:translate3D(100%,0,0)}.router-leave-active{transform:translate3D(-100%,0,0)}\", \"\"]);\n\t\n\t// exports\n\n\n/***/ },\n\n/***/ 176:\n/***/ function(module, exports, __webpack_require__) {\n\n\t// style-loader: Adds some css to the DOM by adding a 90 | -------------------------------------------------------------------------------- /client/src/assets/css/animate.styl: -------------------------------------------------------------------------------- 1 | @keyframes shake 2 | from, to 3 | transform translate3d(0, 0, 0) 4 | 10%, 30%, 50%, 70%, 90% 5 | transform translate3d(-10px, 0, 0) 6 | 20%, 40%, 60%, 80% 7 | transform translate3d(10px, 0, 0) 8 | 9 | @keyframes rotate 10 | 0% 11 | transform rotate(0deg) scale(1) 12 | 50% 13 | transform rotate(180deg) scale(0.6) 14 | 100% 15 | transform rotate(360deg) scale(1) 16 | -------------------------------------------------------------------------------- /client/src/assets/css/flex.styl: -------------------------------------------------------------------------------- 1 | /** 2 | * FLex display helper 3 | */ 4 | .flex 5 | display flex 6 | .flex-inline 7 | display inline-flex 8 | .flex-1 9 | flex 1 10 | .flex-row 11 | flex-direction row 12 | .flex-column 13 | flex-direction column 14 | .flex-main-center 15 | justify-content center 16 | .flex-main-start 17 | justify-content flex-start 18 | .flex-main-end 19 | justify-content flex-end 20 | .flex-cross-center 21 | align-items center 22 | .flex-between 23 | justify-content space-between 24 | -------------------------------------------------------------------------------- /client/src/assets/css/variable.styl: -------------------------------------------------------------------------------- 1 | // primary color 2 | $color-primary = #20a0ff 3 | $color-primary-light = #58B7FF 4 | $color-primary-dark = #1D8CE0 5 | 6 | $color-success = #13CE66 7 | $color-warning = #F7BA2A 8 | $color-danger = #FF4949 9 | 10 | $color-black = #1F2D3D 11 | $color-black-light = #324057 12 | $color-black-exact-light = #475669 13 | 14 | $color-silver = #8492A6 15 | $color-silver-light = #99A9BF 16 | $color-silver-exact-light = #C0CCDA 17 | 18 | $color-gray = #D3DCE6 19 | $color-gray-light = #E5E9F2 20 | $color-gray-exact-light = #EFF2F7 21 | 22 | $color-white = #fff 23 | $color-white-dark = #F9FAFC 24 | 25 | $header-height = 3.5rem 26 | $menu-width = 12.5rem 27 | -------------------------------------------------------------------------------- /client/src/assets/fonts/demo.css: -------------------------------------------------------------------------------- 1 | *{margin: 0;padding: 0;list-style: none;} 2 | /* 3 | KISSY CSS Reset 4 | 理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。 5 | 2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。 6 | 3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。 7 | 特色:1. 适应中文;2. 基于最新主流浏览器。 8 | 维护:玉伯, 正淳 9 | */ 10 | 11 | /** 清除内外边距 **/ 12 | body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */ 13 | dl, dt, dd, ul, ol, li, /* list elements 列表元素 */ 14 | pre, /* text formatting elements 文本格式元素 */ 15 | form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */ 16 | th, td /* table elements 表格元素 */ { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | /** 设置默认字体 **/ 22 | body, 23 | button, input, select, textarea /* for ie */ { 24 | font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif; 25 | } 26 | h1, h2, h3, h4, h5, h6 { font-size: 100%; } 27 | address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */ 28 | code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */ 29 | small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */ 30 | 31 | /** 重置列表元素 **/ 32 | ul, ol { list-style: none; } 33 | 34 | /** 重置文本格式元素 **/ 35 | a { text-decoration: none; } 36 | a:hover { text-decoration: underline; } 37 | 38 | 39 | /** 重置表单元素 **/ 40 | legend { color: #000; } /* for ie6 */ 41 | fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */ 42 | button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */ 43 | /* 注:optgroup 无法扶正 */ 44 | 45 | /** 重置表格元素 **/ 46 | table { border-collapse: collapse; border-spacing: 0; } 47 | 48 | /* 清除浮动 */ 49 | .ks-clear:after, .clear:after { 50 | content: '\20'; 51 | display: block; 52 | height: 0; 53 | clear: both; 54 | } 55 | .ks-clear, .clear { 56 | *zoom: 1; 57 | } 58 | 59 | .main { 60 | padding: 30px 100px; 61 | width: 960px; 62 | margin: 0 auto; 63 | } 64 | .main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;} 65 | 66 | .helps{margin-top:40px;} 67 | .helps pre{ 68 | padding:20px; 69 | margin:10px 0; 70 | border:solid 1px #e7e1cd; 71 | background-color: #fffdef; 72 | overflow: auto; 73 | } 74 | 75 | .icon_lists{ 76 | width: 100% !important; 77 | 78 | } 79 | 80 | .icon_lists li{ 81 | float:left; 82 | width: 100px; 83 | height:180px; 84 | text-align: center; 85 | list-style: none !important; 86 | } 87 | .icon_lists .icon{ 88 | font-size: 42px; 89 | line-height: 100px; 90 | margin: 10px 0; 91 | color:#333; 92 | -webkit-transition: font-size 0.25s ease-out 0s; 93 | -moz-transition: font-size 0.25s ease-out 0s; 94 | transition: font-size 0.25s ease-out 0s; 95 | 96 | } 97 | .icon_lists .icon:hover{ 98 | font-size: 100px; 99 | } 100 | 101 | 102 | 103 | .markdown { 104 | color: #666; 105 | font-size: 14px; 106 | line-height: 1.8; 107 | } 108 | 109 | .highlight { 110 | line-height: 1.5; 111 | } 112 | 113 | .markdown img { 114 | vertical-align: middle; 115 | max-width: 100%; 116 | } 117 | 118 | .markdown h1 { 119 | color: #404040; 120 | font-weight: 500; 121 | line-height: 40px; 122 | margin-bottom: 24px; 123 | } 124 | 125 | .markdown h2, 126 | .markdown h3, 127 | .markdown h4, 128 | .markdown h5, 129 | .markdown h6 { 130 | color: #404040; 131 | margin: 1.6em 0 0.6em 0; 132 | font-weight: 500; 133 | clear: both; 134 | } 135 | 136 | .markdown h1 { 137 | font-size: 28px; 138 | } 139 | 140 | .markdown h2 { 141 | font-size: 22px; 142 | } 143 | 144 | .markdown h3 { 145 | font-size: 16px; 146 | } 147 | 148 | .markdown h4 { 149 | font-size: 14px; 150 | } 151 | 152 | .markdown h5 { 153 | font-size: 12px; 154 | } 155 | 156 | .markdown h6 { 157 | font-size: 12px; 158 | } 159 | 160 | .markdown hr { 161 | height: 1px; 162 | border: 0; 163 | background: #e9e9e9; 164 | margin: 16px 0; 165 | clear: both; 166 | } 167 | 168 | .markdown p, 169 | .markdown pre { 170 | margin: 1em 0; 171 | } 172 | 173 | .markdown > p, 174 | .markdown > blockquote, 175 | .markdown > .highlight, 176 | .markdown > ol, 177 | .markdown > ul { 178 | width: 80%; 179 | } 180 | 181 | .markdown ul > li { 182 | list-style: circle; 183 | } 184 | 185 | .markdown > ul li, 186 | .markdown blockquote ul > li { 187 | margin-left: 20px; 188 | padding-left: 4px; 189 | } 190 | 191 | .markdown > ul li p, 192 | .markdown > ol li p { 193 | margin: 0.6em 0; 194 | } 195 | 196 | .markdown ol > li { 197 | list-style: decimal; 198 | } 199 | 200 | .markdown > ol li, 201 | .markdown blockquote ol > li { 202 | margin-left: 20px; 203 | padding-left: 4px; 204 | } 205 | 206 | .markdown code { 207 | margin: 0 3px; 208 | padding: 0 5px; 209 | background: #eee; 210 | border-radius: 3px; 211 | } 212 | 213 | .markdown pre { 214 | border-radius: 6px; 215 | background: #f7f7f7; 216 | padding: 20px; 217 | } 218 | 219 | .markdown pre code { 220 | border: none; 221 | background: #f7f7f7; 222 | margin: 0; 223 | } 224 | 225 | .markdown strong, 226 | .markdown b { 227 | font-weight: 600; 228 | } 229 | 230 | .markdown > table { 231 | border-collapse: collapse; 232 | border-spacing: 0px; 233 | empty-cells: show; 234 | border: 1px solid #e9e9e9; 235 | width: 95%; 236 | margin-bottom: 24px; 237 | } 238 | 239 | .markdown > table th { 240 | white-space: nowrap; 241 | color: #333; 242 | font-weight: 600; 243 | 244 | } 245 | 246 | .markdown > table th, 247 | .markdown > table td { 248 | border: 1px solid #e9e9e9; 249 | padding: 8px 16px; 250 | text-align: left; 251 | } 252 | 253 | .markdown > table th { 254 | background: #F7F7F7; 255 | } 256 | 257 | .markdown blockquote { 258 | font-size: 90%; 259 | color: #999; 260 | border-left: 4px solid #e9e9e9; 261 | padding-left: 0.8em; 262 | margin: 1em 0; 263 | font-style: italic; 264 | } 265 | 266 | .markdown blockquote p { 267 | margin: 0; 268 | } 269 | 270 | .markdown .anchor { 271 | opacity: 0; 272 | transition: opacity 0.3s ease; 273 | margin-left: 8px; 274 | } 275 | 276 | .markdown .waiting { 277 | color: #ccc; 278 | } 279 | 280 | .markdown h1:hover .anchor, 281 | .markdown h2:hover .anchor, 282 | .markdown h3:hover .anchor, 283 | .markdown h4:hover .anchor, 284 | .markdown h5:hover .anchor, 285 | .markdown h6:hover .anchor { 286 | opacity: 1; 287 | display: inline-block; 288 | } 289 | 290 | .markdown > br, 291 | .markdown > p > br { 292 | clear: both; 293 | } 294 | 295 | 296 | .hljs { 297 | display: block; 298 | background: white; 299 | padding: 0.5em; 300 | color: #333333; 301 | overflow-x: auto; 302 | } 303 | 304 | .hljs-comment, 305 | .hljs-meta { 306 | color: #969896; 307 | } 308 | 309 | .hljs-string, 310 | .hljs-variable, 311 | .hljs-template-variable, 312 | .hljs-strong, 313 | .hljs-emphasis, 314 | .hljs-quote { 315 | color: #df5000; 316 | } 317 | 318 | .hljs-keyword, 319 | .hljs-selector-tag, 320 | .hljs-type { 321 | color: #a71d5d; 322 | } 323 | 324 | .hljs-literal, 325 | .hljs-symbol, 326 | .hljs-bullet, 327 | .hljs-attribute { 328 | color: #0086b3; 329 | } 330 | 331 | .hljs-section, 332 | .hljs-name { 333 | color: #63a35c; 334 | } 335 | 336 | .hljs-tag { 337 | color: #333333; 338 | } 339 | 340 | .hljs-title, 341 | .hljs-attr, 342 | .hljs-selector-id, 343 | .hljs-selector-class, 344 | .hljs-selector-attr, 345 | .hljs-selector-pseudo { 346 | color: #795da3; 347 | } 348 | 349 | .hljs-addition { 350 | color: #55a532; 351 | background-color: #eaffea; 352 | } 353 | 354 | .hljs-deletion { 355 | color: #bd2c00; 356 | background-color: #ffecec; 357 | } 358 | 359 | .hljs-link { 360 | text-decoration: underline; 361 | } 362 | 363 | pre{ 364 | background: #fff; 365 | } 366 | 367 | 368 | 369 | 370 | 371 | -------------------------------------------------------------------------------- /client/src/assets/fonts/demo_fontclass.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IconFont 7 | 8 | 9 | 10 | 11 |
12 |

IconFont 图标

13 |
    14 | 15 |
  • 16 | 17 |
    刷新
    18 |
    .icon-refresh
    19 |
  • 20 | 21 |
  • 22 | 23 |
    trailer-page
    24 |
    .icon-last-page
    25 |
  • 26 | 27 |
  • 28 | 29 |
    home-page
    30 |
    .icon-home-page
    31 |
  • 32 | 33 |
34 | 35 |

font-class引用

36 |
37 | 38 |

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

39 |

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

40 |
    41 |
  • 兼容性良好,支持ie8+,及所有现代浏览器。
  • 42 |
  • 相比于unicode语意明确,书写更直观。可以很容易分辨这个icon是什么。
  • 43 |
  • 因为使用class来定义图标,所以当要替换图标时,只需要修改class里面的unicode引用。
  • 44 |
  • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。
  • 45 |
46 |

使用步骤如下:

47 |

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

48 | 49 | 50 |
<link rel="stylesheet" type="text/css" href="./iconfont.css">
51 |

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

52 |
<i class="iconfont icon-xxx"></i>
53 |
54 |

实际情况中"iconfont"(font-family)需要修改为你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /client/src/assets/fonts/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1478412965459'); /* IE9*/ 4 | src: url('iconfont.eot?t=1478412965459#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('iconfont.woff?t=1478412965459') format('woff'), /* chrome, firefox */ 6 | url('iconfont.ttf?t=1478412965459') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg?t=1478412965459#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family:"iconfont" !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -webkit-text-stroke-width: 0.2px; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | .icon-refresh:before { content: "\e623"; } 20 | 21 | .icon-last-page:before { content: "\e81a"; } 22 | 23 | .icon-home-page:before { content: "\e626"; } 24 | 25 | -------------------------------------------------------------------------------- /client/src/assets/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/src/assets/fonts/iconfont.eot -------------------------------------------------------------------------------- /client/src/assets/fonts/iconfont.js: -------------------------------------------------------------------------------- 1 | ;(function(window) { 2 | 3 | var svgSprite = '' + 4 | ''+ 5 | ''+ 6 | ''+ 7 | ''+ 8 | ''+ 9 | ''+ 10 | ''+ 11 | ''+ 12 | ''+ 13 | ''+ 14 | ''+ 15 | ''+ 16 | ''+ 17 | ''+ 18 | ''+ 19 | ''+ 20 | ''+ 21 | ''+ 22 | ''+ 23 | ''+ 24 | ''+ 25 | ''+ 26 | ''+ 27 | '' 28 | var script = function() { 29 | var scripts = document.getElementsByTagName('script') 30 | return scripts[scripts.length - 1] 31 | }() 32 | var shouldInjectCss = script.getAttribute("data-injectcss") 33 | 34 | /** 35 | * document ready 36 | */ 37 | var ready = function(fn){ 38 | if(document.addEventListener){ 39 | document.addEventListener("DOMContentLoaded",function(){ 40 | document.removeEventListener("DOMContentLoaded",arguments.callee,false) 41 | fn() 42 | },false) 43 | }else if(document.attachEvent){ 44 | IEContentLoaded (window, fn) 45 | } 46 | 47 | function IEContentLoaded (w, fn) { 48 | var d = w.document, done = false, 49 | // only fire once 50 | init = function () { 51 | if (!done) { 52 | done = true 53 | fn() 54 | } 55 | } 56 | // polling for no errors 57 | ;(function () { 58 | try { 59 | // throws errors until after ondocumentready 60 | d.documentElement.doScroll('left') 61 | } catch (e) { 62 | setTimeout(arguments.callee, 50) 63 | return 64 | } 65 | // no errors, fire 66 | 67 | init() 68 | })() 69 | // trying to always fire before onload 70 | d.onreadystatechange = function() { 71 | if (d.readyState == 'complete') { 72 | d.onreadystatechange = null 73 | init() 74 | } 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Insert el before target 81 | * 82 | * @param {Element} el 83 | * @param {Element} target 84 | */ 85 | 86 | var before = function (el, target) { 87 | target.parentNode.insertBefore(el, target) 88 | } 89 | 90 | /** 91 | * Prepend el to target 92 | * 93 | * @param {Element} el 94 | * @param {Element} target 95 | */ 96 | 97 | var prepend = function (el, target) { 98 | if (target.firstChild) { 99 | before(el, target.firstChild) 100 | } else { 101 | target.appendChild(el) 102 | } 103 | } 104 | 105 | function appendSvg(){ 106 | var div,svg 107 | 108 | div = document.createElement('div') 109 | div.innerHTML = svgSprite 110 | svg = div.getElementsByTagName('svg')[0] 111 | if (svg) { 112 | svg.setAttribute('aria-hidden', 'true') 113 | svg.style.position = 'absolute' 114 | svg.style.width = 0 115 | svg.style.height = 0 116 | svg.style.overflow = 'hidden' 117 | prepend(svg,document.body) 118 | } 119 | } 120 | 121 | if(shouldInjectCss && !window.__iconfont__svg__cssinject__){ 122 | window.__iconfont__svg__cssinject__ = true 123 | try{ 124 | document.write(""); 125 | }catch(e){ 126 | console && console.log(e) 127 | } 128 | } 129 | 130 | ready(appendSvg) 131 | 132 | 133 | })(window) 134 | -------------------------------------------------------------------------------- /client/src/assets/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Sun Nov 6 14:16:05 2016 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 34 | 38 | 42 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /client/src/assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/src/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /client/src/assets/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/src/assets/fonts/iconfont.woff -------------------------------------------------------------------------------- /client/src/assets/images/login-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/src/assets/images/login-bg.jpg -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/src/components/ContentModule.vue: -------------------------------------------------------------------------------- 1 | 8 | 18 | -------------------------------------------------------------------------------- /client/src/components/DataTable.vue: -------------------------------------------------------------------------------- 1 | 24 | 99 | 181 | -------------------------------------------------------------------------------- /client/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 68 | 171 | 199 | -------------------------------------------------------------------------------- /client/src/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 16 | 24 | 62 | -------------------------------------------------------------------------------- /client/src/components/NProgress.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 35 | -------------------------------------------------------------------------------- /client/src/components/NavMenu.vue: -------------------------------------------------------------------------------- 1 | 16 | 24 | 62 | -------------------------------------------------------------------------------- /client/src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 21 | 53 | 87 | -------------------------------------------------------------------------------- /client/src/components/RouterLoading.vue: -------------------------------------------------------------------------------- 1 | 8 | 16 | 68 | -------------------------------------------------------------------------------- /client/src/constants.js: -------------------------------------------------------------------------------- 1 | // user info 2 | export const STORE_KEY_USERNAME = 'user.username' 3 | export const STORE_KEY_ACCESS_TOKEN = 'user.access_token' 4 | export const STORE_KEY_REFRESH_TOKEN = 'user.refresh_token' 5 | 6 | // user config 7 | export const STORE_KEY_CONFIG_LOCALE = 'config.locale' 8 | export const STORE_KEY_CONFIG_PAGE_LIMIT = 'config.page.limit' 9 | -------------------------------------------------------------------------------- /client/src/element-ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { Dialog, Autocomplete, Dropdown, DropdownMenu, DropdownItem, Input, InputNumber, 3 | Radio, RadioGroup, RadioButton, Checkbox, CheckboxGroup, Switch, Select, Option, 4 | OptionGroup, Button, ButtonGroup, Table, TableColumn, DatePicker, TimeSelect, TimePicker, 5 | Popover, Tooltip, MessageBox, Breadcrumb, BreadcrumbItem, Form, FormItem, Tabs, TabPane, Tag, Tree, Notification, Slider, 6 | Loading, Icon, Row, Col, Upload, Progress, Spinner, Message, Badge, Card, Steps, Step } from 'element-ui' 7 | 8 | Vue.component(Dialog.name, Dialog) 9 | Vue.component(Autocomplete.name, Autocomplete) 10 | Vue.component(Dropdown.name, Dropdown) 11 | Vue.component(DropdownMenu.name, DropdownMenu) 12 | Vue.component(DropdownItem.name, DropdownItem) 13 | Vue.component(Input.name, Input) 14 | Vue.component(InputNumber.name, InputNumber) 15 | Vue.component(Radio.name, Radio) 16 | Vue.component(RadioGroup.name, RadioGroup) 17 | Vue.component(RadioButton.name, RadioButton) 18 | Vue.component(Checkbox.name, Checkbox) 19 | Vue.component(CheckboxGroup.name, CheckboxGroup) 20 | Vue.component(Switch.name, Switch) 21 | Vue.component(Select.name, Select) 22 | Vue.component(Option.name, Option) 23 | Vue.component(OptionGroup.name, OptionGroup) 24 | Vue.component(Button.name, Button) 25 | Vue.component(ButtonGroup.name, ButtonGroup) 26 | Vue.component(Table.name, Table) 27 | Vue.component(TableColumn.name, TableColumn) 28 | Vue.component(DatePicker.name, DatePicker) 29 | Vue.component(TimeSelect.name, TimeSelect) 30 | Vue.component(TimePicker.name, TimePicker) 31 | Vue.component(Popover.name, Popover) 32 | Vue.component(Tooltip.name, Tooltip) 33 | Vue.component(Breadcrumb.name, Breadcrumb) 34 | Vue.component(BreadcrumbItem.name, BreadcrumbItem) 35 | Vue.component(Form.name, Form) 36 | Vue.component(FormItem.name, FormItem) 37 | Vue.component(Tabs.name, Tabs) 38 | Vue.component(TabPane.name, TabPane) 39 | Vue.component(Tag.name, Tag) 40 | Vue.component(Tree.name, Tree) 41 | Vue.component(Slider.name, Slider) 42 | Vue.component(Icon.name, Icon) 43 | Vue.component(Row.name, Row) 44 | Vue.component(Col.name, Col) 45 | Vue.component(Upload.name, Upload) 46 | Vue.component(Progress.name, Progress) 47 | Vue.component(Spinner.name, Spinner) 48 | Vue.component(Badge.name, Badge) 49 | Vue.component(Card.name, Card) 50 | Vue.component(Steps.name, Steps) 51 | Vue.component(Step.name, Step) 52 | 53 | Vue.use(Loading.directive) 54 | try { 55 | var a = MessageBox 56 | var b = Message 57 | var c = Notification 58 | Vue.prototype.$loading = Loading.service 59 | Vue.prototype.$msgbox = a 60 | Vue.prototype.$alert = a.alert 61 | Vue.prototype.$confirm = a.confirm 62 | Vue.prototype.$prompt = a.prompt 63 | Vue.prototype.$notify = c 64 | Vue.prototype.$message = b 65 | } catch (e) { 66 | console.log(e) 67 | } finally { 68 | // 69 | } 70 | -------------------------------------------------------------------------------- /client/src/http/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '../store' 3 | import router from '../router' 4 | import VueResource from 'vue-resource' 5 | import { Message } from 'element-ui' 6 | Vue.use(VueResource) 7 | // Vue.http.options.emulateJSON = true 8 | Vue.http.options.root = '/api' 9 | Vue.http.interceptors.push((request, next) => { 10 | // if logged in, add the token to the header 11 | if (store.getters.loggedIn) { 12 | request.headers.set('Authorization', `Bearer ${store.getters.accessToken}`) 13 | } 14 | next() 15 | }) 16 | // response interceptor 17 | Vue.http.interceptors.push((request, next) => { 18 | next((response) => { 19 | // don't handle for login page 20 | if (store.state.route.path === '/login') { 21 | return 22 | } 23 | if (response.status === 401) { 24 | store.dispatch('logout').then(() => { 25 | router.push({ path: '/login', query: { redirect: store.state.route.fullpath }}) 26 | }) 27 | return 28 | } 29 | if (response.status === 404) { 30 | Message.error(Vue.t('http.error.E404')) 31 | return 32 | } 33 | if (response.status === 500) { 34 | Message.error(Vue.t('http.error.E500')) 35 | return 36 | } 37 | // other errors 38 | if (!response.ok) { 39 | response.json().then(data => { 40 | Message.error({ 41 | message: data.message || Vue.t('http.error.E404') 42 | }) 43 | }).catch(() => { 44 | Message.error({ 45 | message: response || Vue.t('http.error.E404') 46 | }) 47 | }) 48 | } 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /client/src/locale/en.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'lodash' 2 | 3 | import users from './users/en' 4 | import things from './things/en' 5 | const common = { 6 | el: { 7 | select: { 8 | noData: 'no matched data' 9 | } 10 | }, 11 | config: { 12 | title: 'xxx Backend System', 13 | description: 'Description' 14 | }, 15 | message: { 16 | save: { 17 | ok: 'Saved!', 18 | err: 'Error occured when saving!' 19 | }, 20 | confirm: { 21 | title: 'warning', 22 | ok: 'save', 23 | cancel: 'cancel' 24 | }, 25 | error: 'Error', 26 | created: 'Create successed', 27 | createFailed: 'Create failed', 28 | updated: 'Update successed', 29 | updateFailed: 'Update failed', 30 | removed: 'Delete successed', 31 | removeFailed: 'Delete failed' 32 | }, 33 | http: { 34 | error: { 35 | E401: 'Not authorized', 36 | E403: 'Permission not allowed', 37 | E404: 'Url not found', 38 | E500: 'Server error', 39 | others: 'Some error occured, please try again' 40 | } 41 | }, 42 | header: { 43 | settings: 'User settings', 44 | password: 'Password', 45 | logout: 'Logout', 46 | langSetting: 'Lang', 47 | pageLimit: 'Data count limit per page', 48 | _password: { 49 | description: 'Change your password. It\'s strongly recommend that you should pick a complex password.', 50 | old: 'Old password', 51 | _new: 'New password', 52 | newConfirm: 'Confirm new password', 53 | rules: { 54 | old: 'Please input old password', 55 | _new: 'Please input new password', 56 | newConfirm: 'Please input new password again', 57 | notMatch: 'The two new password not matched' 58 | }, 59 | afterChange: 'Password has changed, system will logout automaticaly, please re-login with the new password.' 60 | } 61 | }, 62 | menu: { 63 | users: 'Users', 64 | things: 'Things' 65 | }, 66 | toolbar: { 67 | create: 'Add', 68 | remove: 'Delete' 69 | }, 70 | datatable: { 71 | operate: 'Operate' 72 | }, 73 | pagination: { 74 | current: 'current', 75 | currentAppend: 'page', 76 | total: 'total', 77 | pages: 'total', 78 | pagesAppend: 'page' 79 | }, 80 | login: { 81 | username: 'Please input the username', 82 | password: 'Please input the password', 83 | button: 'Log in', 84 | authFail: 'Username or password is not correct' 85 | } 86 | } 87 | 88 | export default assign({}, common, { users, things }) 89 | -------------------------------------------------------------------------------- /client/src/locale/en_US.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'lodash' 2 | 3 | import users from './users/en_US' 4 | import things from './things/en_US' 5 | const common = { 6 | el: { 7 | select: { 8 | noData: 'no matched data' 9 | } 10 | }, 11 | config: { 12 | title: 'xxx Backend System', 13 | description: 'Description' 14 | }, 15 | message: { 16 | save: { 17 | ok: 'Saved!', 18 | err: 'Error occured when saving!' 19 | }, 20 | confirm: { 21 | title: 'warning', 22 | ok: 'save', 23 | cancel: 'cancel' 24 | }, 25 | created: 'Create successed', 26 | createFailed: 'Create failed', 27 | updated: 'Update successed', 28 | updateFailed: 'Update failed', 29 | removed: 'Delete successed', 30 | removeFailed: 'Delete failed' 31 | }, 32 | http: { 33 | error: { 34 | E401: 'Not authorized', 35 | E404: 'Url not found', 36 | E500: 'Server error', 37 | others: 'Some error occured, please try again' 38 | } 39 | }, 40 | header: { 41 | settings: 'User settings', 42 | password: 'Password', 43 | logout: 'Logout', 44 | localeSetting: 'Locale', 45 | pageLimit: 'Data count limit per page', 46 | _password: { 47 | description: 'Change your password. It\'s strongly recommend that you should pick a complex password.', 48 | old: 'Old password', 49 | _new: 'New password', 50 | newConfirm: 'Confirm new password', 51 | rules: { 52 | old: 'Please input old password', 53 | _new: 'Please input new password', 54 | newConfirm: 'Please input new password again', 55 | notMatch: 'The two new password not matched' 56 | }, 57 | afterChange: 'Password has changed, system will logout automaticaly, please re-login with the new password.' 58 | } 59 | }, 60 | menu: { 61 | users: 'Users', 62 | things: 'Things' 63 | }, 64 | toolbar: { 65 | create: 'Add', 66 | remove: 'Delete' 67 | }, 68 | datatable: { 69 | operate: 'Operate' 70 | }, 71 | pagination: { 72 | current: 'current', 73 | currentAppend: 'page', 74 | total: 'total', 75 | pages: 'total', 76 | pagesAppend: 'page' 77 | }, 78 | login: { 79 | username: 'Please input the username', 80 | password: 'Please input the password', 81 | button: 'Log in' 82 | } 83 | } 84 | 85 | export default assign({}, common, { users, things }) 86 | -------------------------------------------------------------------------------- /client/src/locale/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | // import store from '../store' 4 | Vue.use(VueI18n) 5 | 6 | import zh from './zh_CN' 7 | Vue.config.lang = 'zh_CN' 8 | Vue.locale('zh_CN', zh) 9 | // store.dispatch('setupLocale') 10 | -------------------------------------------------------------------------------- /client/src/locale/things/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breadcrumb: { 3 | home: 'Home', 4 | current: 'Things' 5 | }, 6 | model: { 7 | name: 'name', 8 | description: 'description' 9 | }, 10 | rules: { 11 | name: 'Please input the name' 12 | }, 13 | edit: { 14 | create: 'Add thing', 15 | update: 'Update thing' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/locale/things/en_US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breadcrumb: { 3 | home: 'Home', 4 | current: 'Things' 5 | }, 6 | model: { 7 | name: 'name', 8 | description: 'description' 9 | }, 10 | rules: { 11 | name: 'Please input the name' 12 | }, 13 | edit: { 14 | create: 'Add thing', 15 | update: 'Update thing' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/locale/things/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breadcrumb: { 3 | home: '首页', 4 | current: '事件管理' 5 | }, 6 | model: { 7 | name: '名字', 8 | description: '描述' 9 | }, 10 | rules: { 11 | name: '请输入名称' 12 | }, 13 | edit: { 14 | create: '新增事件', 15 | update: '编辑事件' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/locale/things/zh_CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breadcrumb: { 3 | home: '首页', 4 | current: '事件管理' 5 | }, 6 | model: { 7 | name: '名字', 8 | description: '描述' 9 | }, 10 | rules: { 11 | name: '请输入名称' 12 | }, 13 | edit: { 14 | create: '新增事件', 15 | update: '编辑事件' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/locale/users/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breadcrumb: { 3 | home: 'Home', 4 | current: 'Users' 5 | }, 6 | model: { 7 | username: 'Username', 8 | role: 'Role', 9 | password: 'Password' 10 | }, 11 | create: { 12 | title: 'Create a user' 13 | }, 14 | rules: { 15 | username: 'Please input the username', 16 | password: 'Please input the password' 17 | }, 18 | action: { 19 | userExisted: 'User existed' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/locale/users/en_US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breadcrumb: { 3 | home: 'Home', 4 | current: 'Users' 5 | }, 6 | model: { 7 | username: 'Username', 8 | role: 'Role', 9 | password: 'Password' 10 | }, 11 | create: { 12 | title: 'Create a user' 13 | }, 14 | rules: { 15 | username: 'Please input the username', 16 | password: 'Please input the password' 17 | }, 18 | action: { 19 | userExisted: 'User existed' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/locale/users/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breadcrumb: { 3 | home: '首页', 4 | current: '用户管理' 5 | }, 6 | model: { 7 | username: '用户名', 8 | role: '角色', 9 | password: '密码' 10 | }, 11 | create: { 12 | title: '创建用户' 13 | }, 14 | rules: { 15 | username: '请输入用户名', 16 | password: '请输入密码' 17 | }, 18 | action: { 19 | userExisted: '用户已存在' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/locale/users/zh_CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breadcrumb: { 3 | home: '首页', 4 | current: '用户管理' 5 | }, 6 | model: { 7 | username: '用户名', 8 | role: '角色', 9 | password: '密码' 10 | }, 11 | create: { 12 | title: '创建用户' 13 | }, 14 | rules: { 15 | username: '请输入用户名', 16 | password: '请输入密码' 17 | }, 18 | action: { 19 | userExisted: '用户已存在' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/locale/zh-CN.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'lodash' 2 | 3 | import users from './users/zh-CN' 4 | import things from './things/zh-CN' 5 | 6 | const common = { 7 | el: { 8 | select: { 9 | noData: '无匹配数据' 10 | } 11 | }, 12 | config: { 13 | title: 'xxx 后台管理系统', 14 | description: '描述' 15 | }, 16 | message: { 17 | save: { 18 | ok: '已保存!', 19 | err: '保存失败!' 20 | }, 21 | confirm: { 22 | title: '提示', 23 | ok: '确 定', 24 | cancel: '取 消' 25 | }, 26 | error: '错误', 27 | created: '新增成功', 28 | createFailed: '新增失败', 29 | updated: '已保存更改', 30 | updateFailed: '更新失败', 31 | removed: '删除成功', 32 | removeFailed: '删除失败' 33 | }, 34 | http: { 35 | error: { 36 | E401: '身份认证失败', 37 | E403: '权限不足', 38 | E404: '请求路径错误', 39 | E500: '后台错误', 40 | others: '操作失败,请重试' 41 | } 42 | }, 43 | header: { 44 | settings: '用户设置', 45 | password: '修改密码', 46 | logout: '退出', 47 | langSetting: '语言', 48 | pageLimit: '每页条目数', 49 | _password: { 50 | description: '修改你的密码。强烈建议您选择一个复杂密码。', 51 | old: '旧密码', 52 | _new: '新密码', 53 | newConfirm: '确认新密码', 54 | rules: { 55 | old: '请输入旧密码', 56 | _new: '请输入新密码', 57 | newConfirm: '请再次确认新密码', 58 | notMatch: '两次输入的新密码不一致' 59 | }, 60 | afterChange: '密码已修改,将自动退出,请使用新密码重新登录。' 61 | } 62 | }, 63 | menu: { 64 | users: '用户管理', 65 | things: '事件管理' 66 | }, 67 | toolbar: { 68 | create: '添加', 69 | remove: '删除' 70 | }, 71 | datatable: { 72 | operate: '操作' 73 | }, 74 | pagination: { 75 | current: '当前第', 76 | currentAppend: '页', 77 | pages: '共', 78 | pagesAppend: '页' 79 | }, 80 | login: { 81 | username: '请输入用户名', 82 | password: '请输入密码', 83 | button: '登录', 84 | authFail: '用户名或密码错误' 85 | } 86 | } 87 | 88 | export default assign({}, common, { users, things }) 89 | -------------------------------------------------------------------------------- /client/src/locale/zh_CN.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'lodash' 2 | 3 | import users from './users/zh_CN' 4 | import things from './things/zh_CN' 5 | 6 | const common = { 7 | el: { 8 | select: { 9 | noData: '无匹配数据' 10 | } 11 | }, 12 | config: { 13 | title: 'xxx 后台管理系统', 14 | description: '描述' 15 | }, 16 | message: { 17 | save: { 18 | ok: '已保存!', 19 | err: '保存失败!' 20 | }, 21 | confirm: { 22 | title: '提示', 23 | ok: '确 定', 24 | cancel: '取 消' 25 | }, 26 | created: '新增成功', 27 | createFailed: '新增失败', 28 | updated: '已保存更改', 29 | updateFailed: '更新失败', 30 | removed: '删除成功', 31 | removeFailed: '删除失败' 32 | }, 33 | http: { 34 | error: { 35 | E401: '身份认证失败', 36 | E404: '请求路径错误', 37 | E500: '后台错误', 38 | others: '操作失败,请重试' 39 | } 40 | }, 41 | header: { 42 | settings: '用户设置', 43 | password: '修改密码', 44 | logout: '退出', 45 | localeSetting: '语言', 46 | pageLimit: '每页条目数', 47 | _password: { 48 | description: '修改你的密码。强烈建议您选择一个复杂密码。', 49 | old: '旧密码', 50 | _new: '新密码', 51 | newConfirm: '确认新密码', 52 | rules: { 53 | old: '请输入旧密码', 54 | _new: '请输入新密码', 55 | newConfirm: '请再次确认新密码', 56 | notMatch: '两次输入的新密码不一致' 57 | }, 58 | afterChange: '密码已修改,将自动退出,请使用新密码重新登录。' 59 | } 60 | }, 61 | menu: { 62 | users: '用户管理', 63 | things: '事件管理' 64 | }, 65 | toolbar: { 66 | create: '添加', 67 | remove: '删除' 68 | }, 69 | datatable: { 70 | operate: '操作' 71 | }, 72 | pagination: { 73 | current: '当前第', 74 | currentAppend: '页', 75 | pages: '共', 76 | pagesAppend: '页' 77 | }, 78 | login: { 79 | username: '请输入用户名', 80 | password: '请输入密码', 81 | button: '登录' 82 | } 83 | } 84 | 85 | export default assign({}, common, { users, things }) 86 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | // router and store 3 | import store from './store' 4 | import router from './router' 5 | import { sync } from 'vuex-router-sync' 6 | sync(store, router) 7 | 8 | // locale 9 | import './locale' 10 | // ui library 11 | import Element from 'element-ui' 12 | Vue.use(Element) 13 | 14 | // ajax 15 | import './http' 16 | 17 | // init store data 18 | store.dispatch('initGlobalConfig') 19 | store.dispatch('initUserInfo') 20 | 21 | // main component 22 | import App from './App' 23 | 24 | import './socket' 25 | 26 | const app = new Vue({ 27 | router, 28 | store, 29 | ...App // Object spread copying everything from App.vue 30 | }) 31 | // actually mount to DOM 32 | app.$mount('#app') 33 | -------------------------------------------------------------------------------- /client/src/resources.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | // things resource 3 | export const thing = Vue.resource('things{/_id}') 4 | // users resource 5 | export const user = Vue.resource('users{/_id}', {}, { 6 | changePassword: { method: 'put', url: 'users{/id}/password' } 7 | }) 8 | -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * App router config 3 | */ 4 | import Vue from 'vue' 5 | import VueRouter from 'vue-router' 6 | import store from '../store' 7 | import { userInitPromise } from '../store/modules/user' 8 | import otherModuleRoutes from './module' 9 | Vue.use(VueRouter) 10 | 11 | const routes = [{ 12 | path: '/login', 13 | component: (resolve) => { 14 | require(['../view/auth/Login.vue'], resolve) 15 | }, 16 | meta: { 17 | skipAuth: true 18 | } 19 | }, { 20 | path: '/', 21 | component: (resolve) => { 22 | require(['../view/CommonView.vue'], resolve) 23 | }, 24 | children: [...otherModuleRoutes, { 25 | path: '/', redirect: '/dashboard' 26 | }] 27 | }, { 28 | path: '*', 29 | component: { 30 | render (h) { 31 | return h('div', { staticClass: 'flex flex-main-center', attrs: { style: 'width:100%;font-size:32px' }}, 'Page not found') 32 | } 33 | } 34 | }] 35 | 36 | const router = new VueRouter({ 37 | mode: 'history', 38 | linkActiveClass: 'active', 39 | scrollBehavior: () => ({ y: 0 }), 40 | routes 41 | }) 42 | 43 | // router 44 | router.beforeEach((to, from, next) => { 45 | // 确保用户身份信息已获取 46 | userInitPromise.then(() => { 47 | store.dispatch('changeRouteLoading', true).then(() => { 48 | // has logged in, redirect 49 | if (to.path === '/login' && store.getters.loggedIn) { 50 | return next(false) 51 | } 52 | if (!to.meta.skipAuth) { 53 | // this route requires auth, check if logged in 54 | // if not, redirect to login page. 55 | if (!store.getters.loggedIn) { 56 | next({ 57 | path: '/login', 58 | query: { redirect: to.fullPath } 59 | }) 60 | } else { 61 | next() 62 | } 63 | } else { 64 | next() 65 | } 66 | }) 67 | }) 68 | }) 69 | 70 | router.afterEach(() => { 71 | store.dispatch('changeRouteLoading', false) 72 | }) 73 | 74 | export default router 75 | -------------------------------------------------------------------------------- /client/src/router/module.js: -------------------------------------------------------------------------------- 1 | export default [{ 2 | path: '/dashboard', 3 | component: (resolve) => { 4 | require(['../view/Dashboard.vue'], resolve) 5 | } 6 | }, { 7 | path: '/users', 8 | component: (resolve) => { 9 | require(['../view/UserList.vue'], resolve) 10 | } 11 | }, { 12 | path: '/things', 13 | component: (resolve) => { 14 | require(['../view/ThingList.vue'], resolve) 15 | } 16 | }] 17 | -------------------------------------------------------------------------------- /client/src/socket/index.js: -------------------------------------------------------------------------------- 1 | import IO from 'socket.io-client' 2 | import Vue from 'vue' 3 | 4 | const socket = IO.connect() 5 | Vue.prototype.$socket = socket 6 | 7 | export default socket 8 | export function authSocket (token, cb) { 9 | socket 10 | .on('authenticated', () => { 11 | cb() 12 | }) 13 | .emit('authenticate', { token: token }) 14 | } 15 | -------------------------------------------------------------------------------- /client/src/storage/index.js: -------------------------------------------------------------------------------- 1 | const storage = window.localStorage 2 | 3 | export function save (key, value) { 4 | storage.setItem(key, value) 5 | } 6 | 7 | export function saveMulti (datas) { 8 | datas.forEach(data => save(data.key, data.value)) 9 | } 10 | 11 | export function read (key) { 12 | return storage.getItem(key) 13 | } 14 | 15 | export function readMulti (keys) { 16 | return keys.map(key => read(key)) 17 | } 18 | 19 | export function clear (key, clearAll = false) { 20 | if (clearAll) { 21 | storage.clear() 22 | } else { 23 | storage.removeItem(key) 24 | } 25 | } 26 | 27 | export function clearMulti (keys) { 28 | keys.forEach(key => clear(key)) 29 | } 30 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import routeLoading from './modules/route' 4 | import config from './modules/global-config' 5 | import user from './modules/user' 6 | Vue.use(Vuex) 7 | 8 | const store = new Vuex.Store({ 9 | strict: process.env.NODE_ENV !== 'production', 10 | modules: { 11 | user, 12 | config, 13 | routeLoading 14 | } 15 | }) 16 | 17 | export default store 18 | -------------------------------------------------------------------------------- /client/src/store/modules/global-config.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { read, save } from '../../storage' 3 | import { STORE_KEY_CONFIG_LOCALE, STORE_KEY_CONFIG_PAGE_LIMIT } from '../../constants' 4 | 5 | const state = { 6 | locale: 'zh_CN', 7 | pageLimit: 20 8 | } 9 | 10 | const mutations = { 11 | UPDATE (state, config) { 12 | state.locale = config.locale || state.locale 13 | state.pageLimit = config.pageLimit || state.pageLimit 14 | } 15 | } 16 | 17 | const actions = { 18 | updateLocale ({ commit }, lang) { 19 | require([`../../locale/${lang}.js`], (langConfig) => { 20 | Vue.locale(lang, langConfig.default) 21 | Vue.config.lang = lang 22 | save(STORE_KEY_CONFIG_LOCALE, lang) 23 | }) 24 | }, 25 | initGlobalConfig ({ commit, dispatch, state }) { 26 | commit('UPDATE', { 27 | locale: read(STORE_KEY_CONFIG_LOCALE), 28 | pageLimit: +read(STORE_KEY_CONFIG_PAGE_LIMIT) 29 | }) 30 | if (state.locale !== 'zh_CN') { 31 | dispatch('updateLocale', state.locale) 32 | } 33 | }, 34 | updateGlobalConfig ({ commit, state, dispatch }, config) { 35 | if (config.locale !== state.locale) { 36 | dispatch('updateLocale', config.locale) 37 | } 38 | commit('UPDATE', config) 39 | save(STORE_KEY_CONFIG_LOCALE, state.locale) 40 | save(STORE_KEY_CONFIG_PAGE_LIMIT, state.pageLimit) 41 | } 42 | } 43 | 44 | const getters = { 45 | globalConfig (state) { 46 | return state 47 | } 48 | } 49 | 50 | export default { 51 | state, 52 | mutations, 53 | actions, 54 | getters 55 | } 56 | -------------------------------------------------------------------------------- /client/src/store/modules/route.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | loading: false 3 | } 4 | 5 | const mutations = { 6 | CHANGE (state, status) { 7 | state.loading = !!status 8 | } 9 | } 10 | 11 | const actions = { 12 | changeRouteLoading ({ commit }, status) { 13 | commit('CHANGE', status) 14 | } 15 | } 16 | 17 | const getters = { 18 | routeLoadingStatus (state) { 19 | return state.loading 20 | } 21 | } 22 | 23 | export default { 24 | state, 25 | mutations, 26 | actions, 27 | getters 28 | } 29 | -------------------------------------------------------------------------------- /client/src/store/modules/user.api.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { authSocket } from '../../socket' 3 | import { readMulti } from '../../storage' 4 | import { STORE_KEY_USERNAME, STORE_KEY_ACCESS_TOKEN, STORE_KEY_REFRESH_TOKEN } from '../../constants' 5 | 6 | export function init () { 7 | return readMulti([STORE_KEY_USERNAME, STORE_KEY_ACCESS_TOKEN, STORE_KEY_REFRESH_TOKEN]) 8 | } 9 | 10 | export function login (username, password) { 11 | return Vue.http.post('auth/local', { 12 | username, 13 | password 14 | }).then(res => res.json()) 15 | } 16 | 17 | export function getUserInfo (token) { 18 | return new Promise((resolve) => { 19 | Vue.http.get('users/me', { 20 | headers: { 21 | 'Authorization': `Bearer ${token}` 22 | } 23 | }).then(data => data.json()).then(data => { 24 | authSocket(token, () => { 25 | console.log('Token authenticated.') 26 | }) 27 | resolve(data) 28 | }).catch(() => { 29 | resolve({}) 30 | }) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /client/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'lodash' 2 | import { saveMulti, clearMulti } from '../../storage' 3 | import { init, login, getUserInfo } from './user.api' 4 | import { STORE_KEY_USERNAME, STORE_KEY_ACCESS_TOKEN, STORE_KEY_REFRESH_TOKEN } from '../../constants' 5 | 6 | const stored = init() 7 | 8 | const state = { 9 | _id: '', 10 | role: 'guest', 11 | username: stored[0] || '', 12 | access_token: stored[1] || '', 13 | refresh_token: stored[2] || '' 14 | } 15 | 16 | let userInitPromise = null 17 | 18 | const mutations = { 19 | // set user info 20 | SET_USER_INFO (state, userInfo) { 21 | state._id = userInfo._id 22 | state.role = userInfo.role 23 | state.username = userInfo.username 24 | state.access_token = userInfo.access_token 25 | state.refresh_token = userInfo.refresh_token 26 | }, 27 | // update stored token 28 | UPDATE_TOKEN (state, payload) { 29 | state.access_token = payload.access_token 30 | state.refresh_token = payload.refresh_token 31 | }, 32 | // after logout 33 | LOGOUT (state) { 34 | state._id = '' 35 | state.username = '' 36 | state.role = 'guest' 37 | state.access_token = '' 38 | state.refresh_token = '' 39 | } 40 | } 41 | 42 | const actions = { 43 | // login action 44 | login ({ commit, dispatch }, payload) { 45 | return new Promise((resolve, reject) => { 46 | login(payload.username, payload.password).then(data => { 47 | getUserInfo(data.token).then(user => { 48 | const userInfo = assign({}, user, { 49 | username: payload.username, 50 | access_token: data.token, 51 | refresh_token: '' 52 | }) 53 | commit('SET_USER_INFO', userInfo) 54 | saveMulti([{ 55 | key: STORE_KEY_USERNAME, 56 | value: userInfo.username 57 | }, { 58 | key: STORE_KEY_ACCESS_TOKEN, 59 | value: userInfo.access_token 60 | }, { 61 | key: STORE_KEY_REFRESH_TOKEN, 62 | value: userInfo.refresh_token 63 | }]) 64 | resolve() 65 | }).catch(() => {}) 66 | }).catch(err => { reject(err) }) 67 | }) 68 | }, 69 | // refresh token action 70 | refreToken ({ commit }, payload) { 71 | commit('REFERE_TOKEN') 72 | saveMulti[{ 73 | key: STORE_KEY_ACCESS_TOKEN, 74 | value: payload.access_token 75 | }, { 76 | key: STORE_KEY_REFRESH_TOKEN, 77 | value: payload.refresh_token 78 | }] 79 | }, 80 | // logout action 81 | logout ({ commit }, payload) { 82 | commit('LOGOUT') 83 | clearMulti([STORE_KEY_USERNAME, STORE_KEY_ACCESS_TOKEN, STORE_KEY_REFRESH_TOKEN]) 84 | }, 85 | // init user info 86 | initUserInfo ({ commit, dispatch, state }) { 87 | userInitPromise = new Promise((resolve, reject) => { 88 | // token 89 | if (stored[1]) { 90 | getUserInfo(stored[1]).then(data => { 91 | let userInfo 92 | if (data._id) { 93 | userInfo = assign({}, data, { 94 | username: stored[0], 95 | access_token: stored[1], 96 | refresh_token: stored[2] 97 | }) 98 | commit('SET_USER_INFO', userInfo) 99 | } 100 | resolve(userInfo) 101 | }).catch(err => { reject(err) }) 102 | } else { 103 | resolve() 104 | } 105 | }) 106 | return userInitPromise 107 | } 108 | } 109 | 110 | const getters = { 111 | userId (state) { 112 | return state._id 113 | }, 114 | userRole (state) { 115 | return state.role 116 | }, 117 | accessToken (state) { 118 | return state.access_token 119 | }, 120 | username (state) { 121 | return state.username 122 | }, 123 | loggedIn (state) { 124 | return !!(state.username && state.access_token) 125 | } 126 | } 127 | 128 | export default { 129 | state, 130 | mutations, 131 | actions, 132 | getters 133 | } 134 | 135 | export { userInitPromise } 136 | -------------------------------------------------------------------------------- /client/src/stored.js: -------------------------------------------------------------------------------- 1 | import { read } from './storage' 2 | import { STORE_KEY_USERNAME, STORE_KEY_ACCESS_TOKEN, STORE_KEY_REFRESH_TOKEN, 3 | STORE_KEY_CONFIG_LANG, STORE_KEY_CONFIG_PAGE_LIMIT } from './constants' 4 | 5 | export const username = read(STORE_KEY_USERNAME) || '' 6 | // eslint-disable-next-line camelcase 7 | export const access_token = read(STORE_KEY_ACCESS_TOKEN) || '' 8 | // eslint-disable-next-line camelcase 9 | export const refresh_token = read(STORE_KEY_REFRESH_TOKEN) || '' 10 | // lang order: localStorage -> browser language -> default 11 | export const lang = read(STORE_KEY_CONFIG_LANG) || navigator.language || 'zh-CN' 12 | export const pageLimit = +read(STORE_KEY_CONFIG_PAGE_LIMIT) || 20 13 | -------------------------------------------------------------------------------- /client/src/view/CommonView.vue: -------------------------------------------------------------------------------- 1 | 8 | 20 | -------------------------------------------------------------------------------- /client/src/view/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 7 | 21 | -------------------------------------------------------------------------------- /client/src/view/ThingList.vue: -------------------------------------------------------------------------------- 1 | 38 | 116 | 131 | -------------------------------------------------------------------------------- /client/src/view/UserList.vue: -------------------------------------------------------------------------------- 1 | 37 | 120 | -------------------------------------------------------------------------------- /client/src/view/auth/Login.vue: -------------------------------------------------------------------------------- 1 | 20 | 76 | 112 | -------------------------------------------------------------------------------- /client/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack-demo/c823d74cb3e4a5bd70c1f6ec93a424460cb62bbc/client/static/favicon.ico -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Project config file includes dev/prod and frontend/backend 3 | */ 4 | var path = require('path') 5 | var _ = require('lodash') 6 | 7 | var backendBase = { 8 | // Root path of server 9 | root: path.normalize(__dirname), 10 | 11 | // Server port 12 | port: process.env.PORT || 9000, 13 | 14 | // Secret for session, you will want to change this and make it an environment variable 15 | secrets: { 16 | session: process.env.SECRET || 'vue-fullstack-demo-secret' 17 | }, 18 | 19 | // List of user roles 20 | userRoles: ['admin', 'user'], 21 | 22 | // MongoDB connection options 23 | mongo: { 24 | options: { 25 | db: { 26 | safe: true 27 | } 28 | } 29 | } 30 | } 31 | 32 | var development = { 33 | frontend: { 34 | port: 9001, 35 | assetsRoot: path.resolve(__dirname, './client/src'), 36 | assetsSubDirectory: 'static', 37 | assetsPublicPath: '/', 38 | proxyTable: { 39 | '/api': { target: 'http://localhost:' + backendBase.port, changeOrigin: true }, 40 | '/socket.io': { target: 'http://localhost:' + backendBase.port, changeOrigin: true, ws: true } 41 | }, 42 | // CSS Sourcemaps off by default because relative paths are "buggy" 43 | // with this option, according to the CSS-Loader README 44 | // (https://github.com/webpack/css-loader#sourcemaps) 45 | // In our experience, they generally work as expected, 46 | // just be aware of this issue when enabling this option. 47 | cssSourceMap: false 48 | }, 49 | backend: _.merge({}, backendBase, { 50 | mongo: { 51 | uri: 'mongodb://localhost/vue-fullstack-demo-dev' 52 | } 53 | }) 54 | } 55 | var production = { 56 | frontend: { 57 | index: path.resolve(__dirname, './client/dist/index.html'), 58 | assetsRoot: path.resolve(__dirname, './client/dist'), 59 | assetsSubDirectory: 'static', 60 | assetsPublicPath: '/', 61 | cssSourceMap: true, 62 | // Gzip off by default as many popular static hosts such as 63 | // Surge or Netlify already gzip all static assets for you. 64 | // Before setting to `true`, make sure to: 65 | // npm install --save-dev compression-webpack-plugin 66 | productionGzip: false, 67 | productionGzipExtensions: ['js', 'css'] 68 | }, 69 | backend: _.merge({}, backendBase, { 70 | // whether backend servers the frontend, you can use nginx to server frontend and proxy to backend services 71 | // if set to true, you need no web services like nginx 72 | serverFrontend: true, 73 | // Server IP 74 | ip: process.env.APP_HOST || process.env.APP_IP || process.env.HOST || process.env.IP, 75 | // Server port 76 | port: process.env.APP_PORT || process.env.PORT, 77 | // MongoDB connection options 78 | mongo: { 79 | uri: process.env.MONGODB_URI || process.env.MONGOHQ_URI || 80 | 'mongodb://localhost/vue-fullstack-demo' 81 | }, 82 | 83 | // frontend folder 84 | frontend: path.resolve(__dirname, './client/dist') 85 | }) 86 | } 87 | 88 | var config = process.env.NODE_ENV === 'production' ? production : development 89 | 90 | module.exports = _.assign({}, config) 91 | -------------------------------------------------------------------------------- /nginx.example.conf: -------------------------------------------------------------------------------- 1 | # enable gzip in nginx.conf 2 | # gzip on; 3 | 4 | # gzip_min_length 1k; 5 | 6 | # gzip_comp_level 4; 7 | 8 | # gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; 9 | 10 | # gzip_vary on; 11 | 12 | # gzip_disable "MSIE [1-6]\."; 13 | 14 | server { 15 | listen 80; 16 | server_name fullstack.io; 17 | 18 | charset utf-8; 19 | root /your/path/to/client/dist; 20 | 21 | access_log logs/vue-fullstack-demo.access.log main; 22 | error_log logs/vue-fullstack-demo.error.log debug; 23 | 24 | location /api { 25 | proxy_set_header X-Real-IP $remote_addr; 26 | proxy_set_header Host $http_host; 27 | # change this to your server host 28 | proxy_pass http://127.0.0.1:9000; 29 | } 30 | 31 | location ~* ^.+\.(ico|gif|jpg|jpeg|png)$ { 32 | access_log off; 33 | expires 30d; 34 | } 35 | 36 | location ~* ^.+\.(css|js|txt|xml|swf|wav)$ { 37 | access_log off; 38 | expires 24h; 39 | } 40 | 41 | location ~* ^.+\.(html|htm)$ { 42 | expires 1h; 43 | } 44 | 45 | location ~* ^.+\.(eot|ttf|otf|woff|svg)$ { 46 | access_log off; 47 | expires max; 48 | } 49 | 50 | location / { 51 | try_files $uri /index.html; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-fullstack-demo", 3 | "version": "0.0.1", 4 | "keywords": [ 5 | "vue", 6 | "vuejs", 7 | "vuex", 8 | "vue-router", 9 | "vue-i18n", 10 | "vue-resource", 11 | "fullstack", 12 | "mongodb", 13 | "mongoose", 14 | "socket.io", 15 | "element", 16 | "element-ui" 17 | ], 18 | "description": "A vue-fullstack project", 19 | "author": "erguotou ", 20 | "private": false, 21 | "scripts": { 22 | "client": "node client/build/dev-server.js", 23 | "server": "./node_modules/.bin/nodemon --watch server server/app.js", 24 | "dev": "npm run client|npm run server", 25 | "build": "node client/build/build.js", 26 | "lint": "eslint --ext .js,.vue client/src" 27 | }, 28 | "dependencies": { 29 | "body-parser": "^1.15.2", 30 | "composable-middleware": "^0.3.0", 31 | "compression": "^1.6.2", 32 | "cookie-parser": "^1.4.3", 33 | "crypto": "0.0.3", 34 | "ejs": "^2.5.2", 35 | "element-ui": "^1.0.3", 36 | "express": "^4.14.0", 37 | "express-jwt": "^3.4.0", 38 | "jsonwebtoken": "^7.1.9", 39 | "lodash": "^4.17.2", 40 | "method-override": "^2.3.7", 41 | "mongoose": "^4.7.0", 42 | "nprogress": "^0.2.0", 43 | "passport": "^0.3.2", 44 | "passport-local": "^1.0.0", 45 | "socket.io": "^1.7.1", 46 | "socket.io-client": "^1.7.1", 47 | "socketio-jwt": "^4.5.0", 48 | "vue": "^2.1.3", 49 | "vue-i18n": "^4.7.3", 50 | "vue-resource": "^1.0.3", 51 | "vue-router": "^2.0.3", 52 | "vuex": "^2.0.0", 53 | "vuex-router-sync": "^3.0.0" 54 | }, 55 | "devDependencies": { 56 | "autoprefixer": "^6.5.1", 57 | "babel-core": "^6.0.0", 58 | "babel-loader": "^6.0.0", 59 | "babel-plugin-component": "^0.4.1", 60 | "babel-plugin-transform-runtime": "^6.0.0", 61 | "babel-preset-es2015": "^6.0.0", 62 | "babel-preset-stage-2": "^6.0.0", 63 | "babel-register": "^6.18.0", 64 | "babel-runtime": "^6.0.0", 65 | "connect-history-api-fallback": "^1.1.0", 66 | "cross-spawn": "^2.1.5", 67 | "css-loader": "^0.23.0", 68 | "eslint": "^2.10.2", 69 | "eslint-config-vue": "^1.1.0", 70 | "eslint-friendly-formatter": "^2.0.5", 71 | "eslint-loader": "^1.3.0", 72 | "eslint-plugin-html": "^1.3.0", 73 | "eslint-plugin-promise": "^1.0.8", 74 | "eventsource-polyfill": "^0.9.6", 75 | "extract-text-webpack-plugin": "^1.0.1", 76 | "file-loader": "^0.8.4", 77 | "glob": "^7.1.1", 78 | "html-webpack-plugin": "^2.8.1", 79 | "http-proxy-middleware": "^0.12.0", 80 | "inject-loader": "^2.0.1", 81 | "mockjs": "^1.0.1-beta3", 82 | "ora": "^0.2.0", 83 | "os-locale": "^2.0.0", 84 | "shelljs": "^0.6.0", 85 | "stylus": "^0.54.5", 86 | "stylus-loader": "^2.3.1", 87 | "url-loader": "^0.5.7", 88 | "vue-hot-reload-api": "^1.2.0", 89 | "vue-html-loader": "^1.0.0", 90 | "vue-loader": "^9.5.1", 91 | "vue-style-loader": "^1.0.0", 92 | "vue-template-compiler": "^2.1.3", 93 | "webpack": "^1.12.2", 94 | "webpack-dev-middleware": "^1.4.0", 95 | "webpack-hot-middleware": "^2.6.0", 96 | "webpack-merge": "^0.8.3" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Backend server 2 | -------------------------------------------------------------------------------- /server/api/paging.js: -------------------------------------------------------------------------------- 1 | var async = require('async') 2 | module.exports = { 3 | listQuery: function (schema, search, selection, sort, page, callback) { 4 | for (var key in search) { 5 | if (search[key] === null || search[key] === undefined || search[key] === '') { 6 | delete search[key] 7 | } 8 | } 9 | async.parallel({ 10 | total: function (done) { 11 | schema.count(search).exec(function (err, total) { 12 | done(err, total) 13 | }) 14 | }, 15 | records: function (done) { 16 | schema.find(search, selection).sort(sort).skip((+page.current - 1) * (+page.limit)) 17 | .limit(+page.limit).exec(function (err, doc) { 18 | done(err, doc) 19 | }) 20 | } 21 | }, function functionName (err, data) { 22 | callback(err, { 23 | page: { 24 | total: data.total 25 | }, 26 | results: data.records 27 | }) 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/api/thing/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var express = require('express') 4 | var controller = require('./thing.controller') 5 | 6 | var router = express.Router() 7 | 8 | router.get('/', controller.index) 9 | router.get('/:id', controller.show) 10 | router.post('/', controller.create) 11 | router.put('/:id', controller.update) 12 | router.patch('/:id', controller.update) 13 | router.delete('/:id', controller.destroy) 14 | 15 | module.exports = router 16 | -------------------------------------------------------------------------------- /server/api/thing/thing.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Using Rails-like standard naming convention for endpoints. 3 | * GET /things -> index 4 | * POST /things -> create 5 | * GET /things/:id -> show 6 | * PUT /things/:id -> update 7 | * DELETE /things/:id -> destroy 8 | */ 9 | 10 | 'use strict' 11 | 12 | var _ = require('lodash') 13 | var Thing = require('./thing.model') 14 | 15 | // Get list of things 16 | exports.index = function (req, res) { 17 | Thing.find(function (err, things) { 18 | if (err) { 19 | return handleError(res, err) 20 | } 21 | return res.status(200).json({ results: things }) 22 | }) 23 | } 24 | 25 | // Get a single thing 26 | exports.show = function (req, res) { 27 | Thing.findById(req.params.id, function (err, thing) { 28 | if (err) { return handleError(res, err) } 29 | if (!thing) { return res.send(404) } 30 | return res.json(thing) 31 | }) 32 | } 33 | 34 | // Creates a new thing in the DB. 35 | exports.create = function (req, res) { 36 | Thing.create(req.body, function (err, thing) { 37 | if (err) { return handleError(res, err) } 38 | return res.status(200).json(thing) 39 | }) 40 | } 41 | 42 | // Updates an existing thing in the DB. 43 | exports.update = function (req, res) { 44 | if (req.body._id) { delete req.body._id } 45 | Thing.findById(req.params.id, function (err, thing) { 46 | if (err) { return handleError(res, err) } 47 | if (!thing) { return res.status(404).send() } 48 | var updated = _.merge(thing, req.body) 49 | updated.save(function (err) { 50 | if (err) { return handleError(res, err) } 51 | return res.status(200).json(thing) 52 | }) 53 | }) 54 | } 55 | 56 | // Deletes a thing from the DB. 57 | exports.destroy = function (req, res) { 58 | Thing.findById(req.params.id, function (err, thing) { 59 | if (err) { return handleError(res, err) } 60 | if (!thing) { return res.status(404).send() } 61 | thing.remove(function (err) { 62 | if (err) { return handleError(res, err) } 63 | return res.status(204).send() 64 | }) 65 | }) 66 | } 67 | 68 | function handleError (res, err) { 69 | return res.status(500).send(err) 70 | } 71 | -------------------------------------------------------------------------------- /server/api/thing/thing.model.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var mongoose = require('mongoose') 4 | var Schema = mongoose.Schema 5 | 6 | var ThingSchema = new Schema({ 7 | name: String, 8 | info: String, 9 | active: Boolean 10 | }) 11 | 12 | module.exports = mongoose.model('Thing', ThingSchema) 13 | -------------------------------------------------------------------------------- /server/api/thing/thing.socket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Broadcast updates to client when the model changes 3 | */ 4 | 5 | 'use strict' 6 | 7 | var thing = require('./thing.model') 8 | 9 | exports.register = function (socket) { 10 | thing.schema.post('save', function (doc) { 11 | onSave(socket, doc) 12 | }) 13 | thing.schema.post('remove', function (doc) { 14 | onRemove(socket, doc) 15 | }) 16 | } 17 | 18 | function onSave (socket, doc, cb) { 19 | socket.emit('thing:save', doc) 20 | } 21 | 22 | function onRemove (socket, doc, cb) { 23 | socket.emit('thing:remove', doc) 24 | } 25 | -------------------------------------------------------------------------------- /server/api/thing/thing.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // var should = require('should') 4 | var app = require('../../app') 5 | var request = require('supertest') 6 | 7 | describe('GET /api/things', function () { 8 | it('should respond with JSON array', function (done) { 9 | request(app) 10 | .get('/api/things') 11 | .expect(200) 12 | .expect('Content-Type', /json/) 13 | .end(function (err, res) { 14 | if (err) return done(err) 15 | res.body.should.be.instanceof(Array) 16 | done() 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /server/api/user/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var express = require('express') 4 | var controller = require('./user.controller') 5 | // var config = require('../../../config').backend 6 | var auth = require('../../auth/auth.service') 7 | 8 | var router = express.Router() 9 | 10 | router.get('/', auth.hasRole('admin'), controller.index) 11 | router.delete('/:id', auth.hasRole('admin'), controller.destroy) 12 | router.get('/me', auth.isAuthenticated(), controller.me) 13 | router.put('/:id/password', auth.isAuthenticated(), controller.changePassword) 14 | router.get('/:id', auth.isAuthenticated(), controller.show) 15 | router.post('/', controller.create) 16 | 17 | module.exports = router 18 | -------------------------------------------------------------------------------- /server/api/user/user.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var User = require('./user.model') 4 | // var passport = require('passport') 5 | var config = require('../../../config').backend 6 | var jwt = require('jsonwebtoken') 7 | var paging = require('../paging') 8 | var _ = require('lodash') 9 | 10 | var validationError = function (res, err) { 11 | return res.status(422).json(err) 12 | } 13 | 14 | /** 15 | * Get list of users 16 | * restriction: 'admin' 17 | */ 18 | exports.index = function (req, res) { 19 | var search = _.merge(req.query.search, { role: 'user' }) 20 | paging.listQuery(User, search, '-salt -hashedPassword', {}, req.query.page, function (err, json) { 21 | if (err) return res.status(500).send(err) 22 | res.status(200).json(json) 23 | }) 24 | } 25 | 26 | /** 27 | * Creates a new user 28 | */ 29 | exports.create = function (req, res, next) { 30 | var newUser = new User(req.body) 31 | newUser.provider = 'local' 32 | newUser.role = 'user' 33 | newUser.save(function (err, user) { 34 | if (err) return validationError(res, err) 35 | var token = jwt.sign({ _id: user._id, name: user.name, role: user.role }, config.secrets.session, { expiresIn: '7d' }) 36 | res.json({ token: token }) 37 | }) 38 | } 39 | 40 | /** 41 | * Get a single user 42 | */ 43 | exports.show = function (req, res, next) { 44 | var userId = req.params.id 45 | 46 | User.findById(userId, function (err, user) { 47 | if (err) return next(err) 48 | if (!user) return res.sendStatus(404) 49 | res.json(user.profile) 50 | }) 51 | } 52 | 53 | /** 54 | * Deletes a user 55 | * restriction: 'admin' 56 | */ 57 | exports.destroy = function (req, res) { 58 | User.findByIdAndRemove(req.params.id, function (err, user) { 59 | if (err) return res.status(500).send(err) 60 | return res.sendStatus(204) 61 | }) 62 | } 63 | 64 | /** 65 | * Change a users password 66 | */ 67 | exports.changePassword = function (req, res, next) { 68 | var userId = req.user._id 69 | var oldPass = String(req.body.oldPassword) 70 | var newPass = String(req.body.newPassword) 71 | 72 | User.findById(userId, function (err, user) { 73 | if (err) { 74 | // handler error 75 | } 76 | if (user.authenticate(oldPass)) { 77 | user.password = newPass 78 | user.save(function (err) { 79 | if (err) return validationError(res, err) 80 | res.sendStatus(200) 81 | }) 82 | } else { 83 | res.status(403).json({ message: 'Old password is not correct.' }) 84 | } 85 | }) 86 | } 87 | 88 | /** 89 | * Get my info 90 | */ 91 | exports.me = function (req, res, next) { 92 | var userId = req.user._id 93 | User.findOne({ 94 | _id: userId 95 | }, '-salt -hashedPassword', function (err, user) { // don't ever give out the password or salt 96 | if (err) return next(err) 97 | if (!user) return res.json(401) 98 | res.json(user) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /server/api/user/user.model.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var mongoose = require('mongoose') 4 | var Schema = mongoose.Schema 5 | var crypto = require('crypto') 6 | 7 | var UserSchema = new Schema({ 8 | name: String, 9 | username: { type: String, lowercase: true }, 10 | role: { 11 | type: String, 12 | default: 'user' 13 | }, 14 | hashedPassword: String, 15 | provider: String, 16 | salt: String 17 | }) 18 | 19 | /** 20 | * Virtuals 21 | */ 22 | UserSchema 23 | .virtual('password') 24 | .set(function (password) { 25 | this._password = password 26 | this.salt = this.makeSalt() 27 | this.hashedPassword = this.encryptPassword(password) 28 | }) 29 | .get(function () { 30 | return this._password 31 | }) 32 | 33 | // Public profile information 34 | UserSchema 35 | .virtual('profile') 36 | .get(function () { 37 | return { 38 | 'name': this.name, 39 | 'role': this.role 40 | } 41 | }) 42 | 43 | // Non-sensitive info we'll be putting in the token 44 | UserSchema 45 | .virtual('token') 46 | .get(function () { 47 | return { 48 | '_id': this._id, 49 | 'role': this.role 50 | } 51 | }) 52 | 53 | /** 54 | * Validations 55 | */ 56 | 57 | // Validate empty username 58 | UserSchema 59 | .path('username') 60 | .validate(function (username) { 61 | return username.length 62 | }, 'Username cannot be blank') 63 | 64 | // Validate empty password 65 | UserSchema 66 | .path('hashedPassword') 67 | .validate(function (hashedPassword) { 68 | return hashedPassword.length 69 | }, 'Password cannot be blank') 70 | 71 | // Validate username is not taken 72 | UserSchema 73 | .path('username') 74 | .validate(function (value, respond) { 75 | var self = this 76 | this.constructor.findOne({ username: value }, function (err, user) { 77 | if (err) throw err 78 | if (user) { 79 | if (self.id === user.id) return respond(true) 80 | return respond(false) 81 | } 82 | respond(true) 83 | }) 84 | }, 'The specified username is already in use.') 85 | 86 | var validatePresenceOf = function (value) { 87 | return value && value.length 88 | } 89 | 90 | /** 91 | * Pre-save hook 92 | */ 93 | UserSchema 94 | .pre('save', function (next) { 95 | if (!this.isNew) return next() 96 | 97 | if (!validatePresenceOf(this.hashedPassword)) { 98 | next(new Error('Invalid password')) 99 | } else { 100 | next() 101 | } 102 | }) 103 | 104 | /** 105 | * Methods 106 | */ 107 | UserSchema.methods = { 108 | /** 109 | * Authenticate - check if the passwords are the same 110 | * 111 | * @param {String} plainText 112 | * @return {Boolean} 113 | * @api public 114 | */ 115 | authenticate: function (plainText) { 116 | return this.encryptPassword(plainText) === this.hashedPassword 117 | }, 118 | 119 | /** 120 | * Make salt 121 | * 122 | * @return {String} 123 | * @api public 124 | */ 125 | makeSalt: function () { 126 | return crypto.randomBytes(16).toString('base64') 127 | }, 128 | 129 | /** 130 | * Encrypt password 131 | * 132 | * @param {String} password 133 | * @return {String} 134 | * @api public 135 | */ 136 | encryptPassword: function (password) { 137 | if (!password || !this.salt) return '' 138 | var salt = new Buffer(this.salt, 'base64') 139 | return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64') 140 | } 141 | } 142 | 143 | module.exports = mongoose.model('User', UserSchema) 144 | -------------------------------------------------------------------------------- /server/api/user/user.model.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var should = require('should') 4 | // var app = require('../../app') 5 | var User = require('./user.model') 6 | 7 | var user = new User({ 8 | provider: 'local', 9 | name: 'Fake User', 10 | username: 'test@test.com', 11 | password: 'password' 12 | }) 13 | 14 | describe('User Model', function () { 15 | before(function (done) { 16 | // Clear users before testing 17 | User.remove().exec().then(function () { 18 | done() 19 | }) 20 | }) 21 | 22 | afterEach(function (done) { 23 | User.remove().exec().then(function () { 24 | done() 25 | }) 26 | }) 27 | 28 | it('should begin with no users', function (done) { 29 | User.find({}, function (err, users) { 30 | users.should.have.length(0) 31 | done() 32 | }) 33 | }) 34 | 35 | it('should fail when saving a duplicate user', function (done) { 36 | user.save(function () { 37 | var userDup = new User(user) 38 | userDup.save(function (err) { 39 | should.exist(err) 40 | done() 41 | }) 42 | }) 43 | }) 44 | 45 | it('should fail when saving without an username', function (done) { 46 | user.username = '' 47 | user.save(function (err) { 48 | should.exist(err) 49 | done() 50 | }) 51 | }) 52 | 53 | it('should authenticate user if password is valid', function () { 54 | return user.authenticate('password').should.be.true 55 | }) 56 | 57 | it('should not authenticate user if password is invalid', function () { 58 | return user.authenticate('blah').should.not.be.true 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application file 3 | */ 4 | 5 | 'use strict' 6 | 7 | // Set default node environment to development 8 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 9 | 10 | var express = require('express') 11 | var mongoose = require('mongoose') 12 | var config = require('../config').backend 13 | 14 | // Connect to database 15 | mongoose.connect(config.mongo.uri, config.mongo.options) 16 | 17 | // insure DB with admin user data 18 | require('./config/seed') 19 | 20 | // Setup server 21 | var app = express() 22 | var server = require('http').createServer(app) 23 | var socketio = require('socket.io')(server) 24 | require('./config/socketio')(socketio) 25 | require('./config/express')(app) 26 | require('./routes')(app) 27 | 28 | // Start server 29 | server.listen(config.port, config.ip, function () { 30 | console.log('Express server listening on %d, in %s mode', config.port, app.get('env')) 31 | }) 32 | 33 | // Expose app 34 | exports = module.exports = app 35 | -------------------------------------------------------------------------------- /server/auth/auth.service.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // var mongoose = require('mongoose') 4 | // var passport = require('passport') 5 | var config = require('../../config').backend 6 | var jwt = require('jsonwebtoken') 7 | var expressJwt = require('express-jwt') 8 | var compose = require('composable-middleware') 9 | var User = require('../api/user/user.model') 10 | var validateJwt = expressJwt({ secret: config.secrets.session }) 11 | 12 | /** 13 | * Attaches the user object to the request if authenticated 14 | * Otherwise returns 403 15 | */ 16 | function isAuthenticated () { 17 | return compose() 18 | // Validate jwt 19 | .use(function (req, res, next) { 20 | // allow access_token to be passed through query parameter as well 21 | if (req.query && req.query.hasOwnProperty('access_token')) { 22 | req.headers.authorization = 'Bearer ' + req.query.access_token 23 | } 24 | validateJwt(req, res, next) 25 | }) 26 | // Attach user to request 27 | .use(function (req, res, next) { 28 | User.findById(req.user._id, function (err, user) { 29 | if (err) return next(err) 30 | if (!user) return res.sendStatus(401) 31 | 32 | req.user = user 33 | next() 34 | }) 35 | }) 36 | } 37 | 38 | /** 39 | * Checks if the user role meets the minimum requirements of the route 40 | */ 41 | function hasRole (roleRequired) { 42 | if (!roleRequired) throw new Error('Required role needs to be set') 43 | 44 | return compose() 45 | .use(isAuthenticated()) 46 | .use(function meetsRequirements (req, res, next) { 47 | if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) { 48 | next() 49 | } else { 50 | res.sendStatus(403) 51 | } 52 | }) 53 | } 54 | 55 | /** 56 | * Returns a jwt token signed by the app secret 57 | */ 58 | function signToken (user) { 59 | return jwt.sign({ _id: user._id, name: user.name, role: user.role }, config.secrets.session, { expiresIn: '7d' }) 60 | } 61 | 62 | /** 63 | * Set token cookie directly for oAuth strategies 64 | */ 65 | function setTokenCookie (req, res) { 66 | if (!req.user) return res.json(404, { message: 'Something went wrong, please try again.' }) 67 | var token = signToken(req.user) 68 | res.cookie('token', JSON.stringify(token)) 69 | res.redirect('/') 70 | } 71 | 72 | exports.isAuthenticated = isAuthenticated 73 | exports.hasRole = hasRole 74 | exports.signToken = signToken 75 | exports.setTokenCookie = setTokenCookie 76 | -------------------------------------------------------------------------------- /server/auth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var express = require('express') 4 | // var passport = require('passport') 5 | var config = require('../../config').backend 6 | var User = require('../api/user/user.model') 7 | 8 | // Passport Configuration 9 | require('./local/passport').setup(User, config) 10 | 11 | var router = express.Router() 12 | 13 | router.use('/local', require('./local')) 14 | 15 | module.exports = router 16 | -------------------------------------------------------------------------------- /server/auth/local/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var express = require('express') 4 | var passport = require('passport') 5 | var auth = require('../auth.service') 6 | 7 | var router = express.Router() 8 | 9 | router.post('/', function (req, res, next) { 10 | passport.authenticate('local', function (err, user, info) { 11 | var error = err || info 12 | if (error) return res.status(401).json(error) 13 | if (!user) return res.status(404).json({ message: 'Something went wrong, please try again.' }) 14 | 15 | var token = auth.signToken(user) 16 | res.json({ token: token }) 17 | })(req, res, next) 18 | }) 19 | 20 | module.exports = router 21 | -------------------------------------------------------------------------------- /server/auth/local/passport.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport') 2 | var LocalStrategy = require('passport-local').Strategy 3 | 4 | exports.setup = function (User, config) { 5 | passport.use(new LocalStrategy({ 6 | usernameField: 'username', 7 | passwordField: 'password' // this is the virtual field on the model 8 | }, 9 | function (username, password, done) { 10 | User.findOne({ 11 | username: username.toLowerCase() 12 | }, function (err, user) { 13 | if (err) return done(err) 14 | 15 | if (!user) { 16 | return done(null, false, { message: 'This username is not registered.' }) 17 | } 18 | if (!user.authenticate(password)) { 19 | return done(null, false, { message: 'This password is not correct.' }) 20 | } 21 | return done(null, user) 22 | }) 23 | })) 24 | } 25 | -------------------------------------------------------------------------------- /server/components/errors/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error responses 3 | */ 4 | 5 | 'use strict' 6 | 7 | module.exports[404] = function pageNotFound (req, res) { 8 | var viewFilePath = '404' 9 | var statusCode = 404 10 | var result = { 11 | status: statusCode 12 | } 13 | 14 | res.status(result.status) 15 | res.render(viewFilePath, function (err) { 16 | if (err) { return res.json(result.status) } 17 | 18 | res.render(viewFilePath) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /server/config/express.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Express configuration 3 | */ 4 | 5 | 'use strict' 6 | 7 | var express = require('express') 8 | var path = require('path') 9 | var compression = require('compression') 10 | var bodyParser = require('body-parser') 11 | var methodOverride = require('method-override') 12 | var cookieParser = require('cookie-parser') 13 | var config = require('../../config') 14 | var passport = require('passport') 15 | 16 | module.exports = function (app) { 17 | // render 18 | app.set('views', config.backend.root + '/server/views') 19 | app.engine('html', require('ejs').renderFile) 20 | app.set('view engine', 'html') 21 | 22 | app.use(compression()) 23 | app.use(bodyParser.urlencoded({ extended: false })) 24 | app.use(bodyParser.json()) 25 | app.use(methodOverride()) 26 | app.use(cookieParser()) 27 | app.use(passport.initialize()) 28 | 29 | if (config.backend.serverFrontend) { 30 | var staticPath = path.posix.join(config.frontend.assetsPublicPath, config.frontend.assetsSubDirectory) 31 | app.use(staticPath, express.static(path.join(config.backend.frontend, '/static'))) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/config/seed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Populate DB with admin user data on server start 3 | */ 4 | 5 | 'use strict' 6 | 7 | var User = require('../api/user/user.model') 8 | 9 | // search for admin user, if no, create one 10 | User.find({ role: 'admin' }, function (err, admin) { 11 | if (err) throw err 12 | if (!(admin && admin.length)) { 13 | User.create({ 14 | provider: 'local', 15 | role: 'admin', 16 | name: 'Admin', 17 | username: 'admin', 18 | password: 'admin' 19 | }, function () { 20 | console.log('finished populating users') 21 | }) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /server/config/socketio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Socket.io configuration 3 | */ 4 | 5 | 'use strict' 6 | var socketioJwt = require('socketio-jwt') 7 | var config = require('../../config').backend 8 | 9 | // When the user disconnects.. perform this 10 | function onDisconnect (socket) { 11 | } 12 | 13 | // When the user connects.. perform this 14 | function onConnect (socket) { 15 | // When the client emits 'info', this listens and executes 16 | socket.on('info', function (data) { 17 | console.info('[%s] %s', socket.address, JSON.stringify(data, null, 2)) 18 | }) 19 | 20 | socket.on('client:hello', function (data) { 21 | console.info('Received from client: %s', data) 22 | socket.emit('server:hello', 'server hello') 23 | }) 24 | 25 | // Insert sockets below 26 | require('../api/thing/thing.socket').register(socket) 27 | } 28 | 29 | module.exports = function (socketio) { 30 | socketio.sockets 31 | .on('connection', socketioJwt.authorize({ 32 | secret: config.secrets.session, 33 | timeout: 15000 // 15 seconds to send the authentication message 34 | })) 35 | .on('authenticated', function (socket) { 36 | // this socket is authenticated, we are good to handle more events from it. 37 | console.log('hello! ' + socket.decoded_token.name) 38 | socket.address = socket.handshake.address || 39 | socket.handshake.headers.host || process.env.DOMAIN 40 | 41 | socket.connectedAt = new Date() 42 | 43 | // Call onDisconnect. 44 | socket.on('disconnect', function () { 45 | onDisconnect(socket) 46 | console.info('[%s] DISCONNECTED', socket.address) 47 | }) 48 | 49 | // Call onConnect. 50 | onConnect(socket) 51 | console.info('[%s] CONNECTED', socket.address) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application routes 3 | */ 4 | 5 | 'use strict' 6 | 7 | var errors = require('./components/errors') 8 | var config = require('../config').backend 9 | var path = require('path') 10 | 11 | module.exports = function (app) { 12 | // Insert routes below 13 | app.use('/api/things', require('./api/thing')) 14 | app.use('/api/users', require('./api/user')) 15 | 16 | app.use('/api/auth', require('./auth')) 17 | 18 | // All undefined asset or api routes should return a 404 19 | app.route('/:url(api|auth|static)/*').get(errors[404]) 20 | 21 | // All other routes should redirect to the index.html 22 | if (config.serverFrontend) { 23 | app.route('/*').get(function (req, res) { 24 | res.sendFile(path.join(config.frontend, '/index.html')) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/views/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 |
    148 |
  • a mistyped address
  • 149 |
  • an out-of-date link
  • 150 |
151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /tasks/mock.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global') 2 | var path = require('path') 3 | console.log('mv client folder %s to root path %s', path.join(__dirname, '../client/'), path.join(__dirname, '..')) 4 | /* eslint-disable */ 5 | mv(path.join(__dirname, '../client/*'), path.join(__dirname, '../')) 6 | rm('-rf', path.join(__dirname, '../client')) 7 | rm('-rf', path.join(__dirname, '../server')) 8 | 9 | console.log('Done') 10 | --------------------------------------------------------------------------------