├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets └── images │ ├── logo.png │ └── logo.svg ├── meta.js ├── package.json ├── script └── generate.js └── template ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── client ├── README.md ├── build │ ├── build.js │ ├── dev-client.js │ ├── dev-server.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── 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 │ ├── components │ │ ├── ContentLoading.vue │ │ ├── ContentModule.vue │ │ ├── DataTable.vue │ │ ├── Header.vue │ │ ├── NProgress.vue │ │ ├── NavMenu.vue │ │ └── Pagination.vue │ ├── constants.js │ ├── element-ui.js │ ├── http │ │ └── index.js │ ├── locales │ │ ├── en.js │ │ ├── header │ │ │ ├── en.js │ │ │ ├── index.js │ │ │ └── zh-CN.js │ │ ├── index.js │ │ ├── login │ │ │ ├── en.js │ │ │ ├── index.js │ │ │ └── zh-CN.js │ │ ├── menu │ │ │ ├── en.js │ │ │ ├── index.js │ │ │ └── zh-CN.js │ │ ├── pagination │ │ │ ├── en.js │ │ │ ├── index.js │ │ │ └── zh-CN.js │ │ ├── things │ │ │ ├── en.js │ │ │ ├── index.js │ │ │ └── zh-CN.js │ │ ├── users │ │ │ ├── en.js │ │ │ ├── index.js │ │ │ └── zh-CN.js │ │ └── zh-CN.js │ ├── main.js │ ├── resources.js │ ├── router │ │ ├── index.js │ │ └── module.js │ ├── shared │ │ ├── search.js │ │ └── validators.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 ├── mock ├── README.md ├── ajax │ ├── things.js │ └── users.js ├── index.js └── socket │ └── dashboard.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 ├── remove.js └── replaceI18n.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_install: 5 | - git config --global push.default matching 6 | - git config --global user.name "erguotou" 7 | - git config --global user.email "erguotou525@gmail.com" 8 | install: 9 | - npm install -g vue-cli 10 | - npm install 11 | script: 12 | - rm -rf ../vf-backend 13 | - rm -rf ../vf-mock 14 | - node script/generate.js 15 | - echo 'shell pwd' 16 | - pwd 17 | - cd ../vf-mock 18 | - sed -i '4d' .gitignore 19 | - npm install 20 | - npm run remove:mock 21 | - npm run build 22 | - git init 23 | - git add -A 24 | - git commit -m "Auto commit" 25 | - git push -u https://$GH_TOKEN@github.com/erguotou520/vue-fullstack.git HEAD:vf-mock --force 26 | - cd ../vf-backend 27 | - sed -i '4d' .gitignore 28 | - npm install 29 | - npm run build 30 | - git init 31 | - git add -A 32 | - git commit -m "Auto commit" 33 | - git push -u https://$GH_TOKEN@github.com/erguotou520/vue-fullstack.git HEAD:vf-backend --force 34 | cache: 35 | directories: 36 | - node_modules 37 | - $(npm config get prefix)/vue-cli 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 erguotou520 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](./assets/images/logo.png) 2 | # Vue fullstack template 3 | ![](https://travis-ci.org/erguotou520/vue-fullstack.svg?branch=master) 4 | ![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103) 5 | ![vue version](https://img.shields.io/badge/vue-2.x-brightgreen.svg) 6 | 7 | This project's goal is to help people create a **reactive, realtime and user friendly** backend system.:smirk: 8 | 9 | ## Vue version 10 | This template only support vue2, because it use `ElementUI` as the default ui library and `ElementUI` just support vue2. 11 | 12 | ## Requirement 13 | - `Mongodb` This project requires `mongodb` as the database. You can follow [it's tutorial](https://docs.mongodb.com/manual/administration/install-community/) to install it if you haven't installed it already. In consideration of the recent [mongodb attack event](https://www.bleepingcomputer.com/news/security/mongodb-apocalypse-is-here-as-ransom-attacks-hit-10-000-servers/), it's highly recommend to enable auth and disable public login for your mongodb. 14 | - Basic `vue.js` skills 15 | 16 | ## About mock and i18n 17 | If you chose to use mock server or not use i18n at project initialization, you need to take a look at this description, else you can skip this step. 18 | As `vue-cli` does't support to run a task after initialization, you need to run `npm run remove:i18n`(*This script doesn't change the page view which contains `change locale` in `client/src/components/Header.vue` and `client/src/view/auth/Login.vue`, you can remove that code manually.*) and `npm run remove:mock` manually. 19 | 20 | ## Usage 21 | This is a project template for vue-cli. It is recommended to use npm 3+ for a more efficient dependency tree. 22 | ```shell 23 | # cli version must be greater than 2.8.0 24 | $ npm install -g vue-cli 25 | $ vue init erguotou520/vue-fullstack my-project ## (important hint: don't use a dot in your project name, otherwise it will cause issues with mongodb) 26 | $ cd my-project 27 | $ npm install 28 | # If you chose to use mock server, you need to run remove:mock to change the file structure, otherwise just skip this step 29 | $ npm run remove:mock 30 | # If you chose not to use vue-i18n, you need to run remove:i18n to replace the files with default locale text, otherwise just skip this step 31 | $ npm run remove:i18n 32 | # If you chose to use real backend server, run this script to start an express server 33 | $ npm run server 34 | # If you chose to use mock server, run this script to start a mock server 35 | $ npm run mock 36 | # Open another terminal and cd into my-project 37 | # This runs a frontend dev server 38 | $ npm run client 39 | ``` 40 | 41 | ## What's Included 42 | - `vue` 43 | - `vue-router` 44 | - `vuex` 45 | - `vue-i18n` 46 | - `vue-resource` 47 | - `element-ui` 48 | - `express` 49 | - `mongoose` 50 | - `socket.io` 51 | - `mock server` 52 | 53 | ## Demo app 54 | ~~* Here is a [demo](https://vue-fullstack-demo.herokuapp.com) and the demo repo is [here](https://github.com/erguotou520/vue-fullstack-demo)~~ 55 | ~~* This is [another one](http://meals.erguotou.me)~~ 56 | 57 | Now the demo apps are generated by ci automatically and pushed to `vf-backend`|`vf-mock` branches. 58 | - [Backend server demo](https://vf-backend.herokuapp.com) 59 | - [Mock server demo](https://vf-mock.herokuapp.com/) 60 | 61 | *Do not change the password please.* 62 | ``` 63 | username: admin 64 | password: admin 65 | ``` 66 | 67 | ## App structure 68 | ``` 69 | ├─client # frontend source folder 70 | │ ├─build # frontend dev scripts 71 | │ ├─src # frontend src 72 | │ │ ├─assets 73 | │ │ │ ├─css 74 | │ │ │ ├─fonts 75 | │ │ │ └─images 76 | │ │ ├─components # vue components 77 | │ │ ├─http # vue-resource configuration 78 | │ │ ├─locale # vue-i18n configuration 79 | │ │ ├─router # vue-router configuration 80 | │ │ ├─socket # socket.io configuration 81 | │ │ ├─storage # web storage api 82 | │ │ ├─store # vuex store 83 | │ │ │ └─modules 84 | │ │ └─view # app pages 85 | │ │ └─auth 86 | │ └─static # static folder 87 | ├─mock # mock server 88 | │ ├─ajax # ajax mock configs 89 | │ ├─socket # socket.io mock configs 90 | └─server # backend server folder 91 | ├─api # backend api list 92 | │ ├─thing 93 | │ └─user 94 | ├─auth # user auth logical 95 | │ └─local 96 | ├─components # server components 97 | │ └─errors 98 | ├─config # server configs, contains express socket.io, etc. 99 | └─views # server served pages 100 | ``` 101 | 102 | ## Configuration 103 | 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. 104 | 105 | Here are some important/frequently-used configurations: 106 | - `frontend.port` port that frontend server listens at 107 | - `backend.port` port that backend server listen at 108 | - `backend.secrets.session` secret for session, important when you deploy your app, make sure it's complex enough 109 | - `backend.mongo.uri` change this if your mongodb uri is not matched 110 | - `backend.serverFrontend` whether to server the frontend code. If set to `true` the express server serves the frontend code. Otherwise you may need a http server like nginx to serve frontend code and there is a nginx configuration at `nginx.example.conf` (default true) 111 | 112 | ## Environment variables 113 | When you deploy your app to you cloud server it's easy to configure youre app with `environment variables`. Following are supported: 114 | - `APP_port` or `PORT`: set to `backend.port` 115 | - `APP_HOST` or `APP_IP` or `HOST` or `IP`: set to `backend.ip` 116 | - `MONGODB_URI` or `MONGOHQ_URI`: set to `backend.mongo.uri` 117 | - `SECRET`: set to `backend.secrets.session` 118 | 119 | ## Notice 120 | 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 changes at this issue. 121 | 122 | ## License 123 | Under [MIT license](./LICENSE) 124 | 125 | ## Reference resources 126 | - [generator-angular-fullstack](https://github.com/angular-fullstack/generator-angular-fullstack) most express code are copied from this repo :kissing_heart: 127 | - [ElementUI](http://element.eleme.io/) design page fast and easy :+1: 128 | 129 | ## At the end 130 | I am very glad to receive your suggestions and pull request. 131 | -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack/3bcece5a3effc8ea96125e14c264771373a1d6f8/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 资源 1 -------------------------------------------------------------------------------- /meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "prompts": { 3 | "name": { 4 | "type": "string", 5 | "required": true, 6 | "message": "Project name", 7 | "default": "vue-fullstack" 8 | }, 9 | "description": { 10 | "type": "string", 11 | "required": false, 12 | "message": "Project description", 13 | "default": "A vue-fullstack project" 14 | }, 15 | "author": { 16 | "type": "string", 17 | "message": "Author" 18 | }, 19 | "mock": { 20 | "type": "list", 21 | "message": "Use real backend server or mock server", 22 | "choices": [{ 23 | "name": "Backend server", 24 | "value": "backend", 25 | "short": "Backend" 26 | }, { 27 | "name": "Mock server", 28 | "value": "mock", 29 | "short": "Mock" 30 | }] 31 | }, 32 | "backendPort": { 33 | "type": "string", 34 | "when": "mock==='backend'", 35 | "message": "Port that backend server listen at development environment", 36 | "default": 9000 37 | }, 38 | "frontendPort": { 39 | "type": "string", 40 | "message": "Port that frontend server listen at development environment", 41 | "default": 9001 42 | }, 43 | "mongoUri": { 44 | "type": "string", 45 | "when": "mock==='backend'", 46 | "message": "MongoDB uri, '-dev' will be appended in development, default is local uri" 47 | }, 48 | "mockPort": { 49 | "type": "string", 50 | "when": "mock==='mock'", 51 | "message": "Port that mock server listen at", 52 | "default": 7878 53 | }, 54 | "i18n": { 55 | "type": "confirm", 56 | "message": "Need vue-i18n?" 57 | } 58 | }, 59 | "filters": { 60 | "tasks/replaceI18n.js": "!i18n", 61 | "tasks/mock.js": "mock==='mock'", 62 | "tasks/remove.js": "!i18n||mock==='mock'", 63 | "mock/**/*": "mock==='mock'" 64 | }, 65 | "skipInterpolation": "{client/**/*.vue,tasks/**/*.*}", 66 | "completeMessage": "To get started:\n\n cd {{destDirName}}\n npm install\n{{#if_eq mock 'mock'}} npm run remove:mock\n{{/if_eq}}{{#unless i18n}} npm run remove:i18n\n{{/unless}}{{#if_eq mock 'backend'}} npm run server\n{{/if_eq}}{{#if_eq mock 'mock'}} npm run mock\n{{/if_eq}} npm run client" 67 | }; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-fullstack", 3 | "version": "0.2.1", 4 | "license": "MIT", 5 | "keywords": [ 6 | "vue", 7 | "vuejs", 8 | "vuex", 9 | "vue-router", 10 | "vue-i18n", 11 | "vue-resource", 12 | "fullstack", 13 | "mongodb", 14 | "mongoose", 15 | "socket.io", 16 | "element", 17 | "element-ui", 18 | "mock server" 19 | ], 20 | "description": "This project's target is to helper people to create a Reactivity, Realtime, User friendly backend system", 21 | "author": "erguotou525@gmail.com", 22 | "devDependencies": { 23 | "suppose": "^0.6.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /script/generate.js: -------------------------------------------------------------------------------- 1 | var suppose = require('suppose') 2 | var path = require('path') 3 | var fs = require('fs') 4 | 5 | var backendProject = 'vf-backend' 6 | var mockProject = 'vf-mock' 7 | 8 | // set the package.json engines 9 | var pkgPath = path.resolve(__dirname, '../template/package.json') 10 | fs.readFile(pkgPath, 'utf-8', function (err, data) { 11 | if (err) throw err 12 | data = data.replace('"node": ">= 4.0.0",', '"node": "6.4.0",') 13 | data = data.replace('"npm": ">= 3.0.0"', '"npm": "3.10.6"') 14 | fs.writeFile(pkgPath, data, function (err) { 15 | if (err) { 16 | console.error('Error when set engines.') 17 | } 18 | }) 19 | }) 20 | 21 | // cd to parent folder 22 | process.chdir(path.join(__dirname, '../../')) 23 | console.log('Current pwd: ', process.cwd()) 24 | // remove folder 25 | // fs.unlinkSync(path.join(__dirname, backendProject)) 26 | // fs.unlinkSync(path.join(__dirname, mockProject)) 27 | // console.log('Remove backend and mock folder: ', path.join(__dirname, backendProject), path.join(__dirname, mockProject)) 28 | 29 | // shell expect 30 | // backend expect 31 | suppose('vue', ['init', './vue-fullstack', backendProject]) 32 | .when(/Project name/).respond('\n') 33 | .when(/Project description/).respond('\n') 34 | .when(/Author/).respond('\n') 35 | .when(/Use real backend server or mock server/).respond('\n') 36 | .when(/Port that backend server listen at development environment/).respond("\n") 37 | .when(/Port that frontend server listen at development environment/).respond('\n') 38 | .when(/MongoDB uri/).respond("\n") 39 | .when(/Need vue-i18n/).respond('Y\n') 40 | .on('error', function(err){ 41 | console.log(err.message) 42 | }) 43 | .end(function(code){ 44 | console.log('Finish expecting backend.') 45 | fs.writeFileSync(path.join(__dirname, '../../', backendProject, 'Procfile'), 'web: node server/app.js') 46 | // add code: can't change admin's password 47 | var text = fs.readFileSync(path.join(__dirname, '../../', backendProject, 'server/api/user/user.controller.js'), { encoding: 'utf-8' }) 48 | fs.writeFileSync(path.join(__dirname, '../../', backendProject, 'server/api/user/user.controller.js'), text.replace(/exports\.changePassword = function \(req, res, next\) \{/, 'exports.changePassword = function (req, res, next) {\nif(req.user.role==="admin"){return res.sendStatus(200)}\n')) 49 | // mock expect 50 | suppose('vue', ['init', './vue-fullstack', mockProject]) 51 | .when(/Project name/).respond('\n') 52 | .when(/Project description/).respond('\n') 53 | .when(/Author/).respond('\n') 54 | .when(/Use real backend server or mock server/).respond('\033\[A\n') 55 | .when(/Port that frontend server listen at development environment/).respond('\n') 56 | .when(/Port that mock server listen at/).respond('\n') 57 | .when(/Need vue-i18n/).respond('Y\n') 58 | .on('error', function(err){ 59 | console.log(err.message) 60 | }) 61 | .end(function(code){ 62 | console.log('Finish expecting mock.') 63 | fs.writeFileSync(path.join(__dirname, '../../', mockProject, 'Procfile'), 'web: node mock/index.js') 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /template/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime", ["component", [ 7 | { 8 | "libraryName": "element-ui", 9 | "styleLibraryName": "theme-default" 10 | } 11 | ]]], 12 | "comments": false 13 | } 14 | -------------------------------------------------------------------------------- /template/.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 | -------------------------------------------------------------------------------- /template/.eslintignore: -------------------------------------------------------------------------------- 1 | {{#if_eq mock 'backend'}}client/{{/if_eq}}src/assets/fonts 2 | -------------------------------------------------------------------------------- /template/.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 | parser: 'babel-eslint', 19 | parserOptions: { 20 | sourceType: 'module', 21 | allowImportExportEverywhere: false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | {{#if_eq mock 'backend'}}client/{{/if_eq}}dist 5 | -------------------------------------------------------------------------------- /template/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 {{author}} 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 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | # {{name}} 2 | This is a NodeJs fullstack project using `express`, `mongodb`, `passport`, `vue`, `vue-router`, `vuex`, etc. 3 | 4 | ## Feature 5 | - Separate for backend and frontend when development 6 | - Configurable 7 | - Restfull api 8 | 9 | ## Before dev 10 | 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. 11 | 2. NodeJs installed. 12 | 13 | ## Dev step 14 | 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 15 | 2. Run `npm run server`, this will initial the db and `User` document if not exists 16 | 3. Open other terminal and run `npm run client`, you can combine the two command with `npm run dev` 17 | 4. Open browser and nav to `localhost:9001` (the default port is 9001, if you change this, change the port) 18 | 19 | ## Build 20 | Run `npm run build` 21 | 22 | ## App structure 23 | ``` 24 | ├─client # frontend source folder 25 | │ ├─build # frontend dev scripts 26 | │ ├─src # frontend src 27 | │ │ ├─assets 28 | │ │ │ ├─css 29 | │ │ │ ├─fonts 30 | │ │ │ └─images 31 | │ │ ├─components # vue components 32 | │ │ ├─http # vue-resource configuration 33 | │ │ ├─locale # vue-i18n configuration 34 | │ │ ├─router # vue-router configuration 35 | │ │ ├─socket # socket.io configuration 36 | │ │ ├─storage # web storage api 37 | │ │ ├─store # vuex store 38 | │ │ │ └─modules 39 | │ │ └─view # app pages 40 | │ │ └─auth 41 | │ └─static # static folder 42 | └─server # backend server folder 43 | ├─api # backend api list 44 | │ ├─thing 45 | │ └─user 46 | ├─auth # user auth logical 47 | │ └─local 48 | ├─components # server components 49 | │ └─errors 50 | ├─config # server configs, contains express socket.io, etc. 51 | └─views # server servered pages 52 | ``` 53 | 54 | ## Configuration 55 | 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. 56 | 57 | Here is some important/frequently-used configuration: 58 | - `frontend.port` port that frontend server listens at 59 | - `backend.port` port that backend server listen at 60 | - `backend.secrets.session` secret for session, important when you deploy your app, make sure it's complex enough 61 | - `backend.mongo.uri` change this if your mongodb uri is not matched 62 | - `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) 63 | 64 | ## Environment variable 65 | When you deploy your app to you cloud server, it's easy to config you app with `environment variable`, here is the supported: 66 | - `APP_port` or `PORT`: set to `backend.port` 67 | - `APP_HOST` or `APP_IP` or `HOST` or `IP`: set to `backend.ip` 68 | - `MONGODB_URI` or `MONGOHQ_URI`: set to `backend.mongo.uri` 69 | - `SECRET`: set to `backend.secrets.session` 70 | 71 | ## Notice 72 | 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. 73 | 74 | ## License 75 | Under [MIT license](./LICENSE) 76 | -------------------------------------------------------------------------------- /template/client/README.md: -------------------------------------------------------------------------------- 1 | # Frontend folder 2 | -------------------------------------------------------------------------------- /template/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({{#if_eq mock "mock"}}'../config'{{/if_eq}}{{#if_eq mock "backend"}}'../../config'{{/if_eq}}).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', {{#if_eq mock "mock"}}'static/*'{{/if_eq}}{{#if_eq mock "backend"}}'client/static/*'{{/if_eq}}, 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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/client/build/dev-server.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development' 2 | var config = require({{#if_eq mock "mock"}}'../config'{{/if_eq}}{{#if_eq mock "backend"}}'../../config'{{/if_eq}}).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 | quiet: true 21 | }) 22 | 23 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 24 | log: () => {} 25 | }) 26 | // force page reload when html-webpack-plugin template changes 27 | compiler.plugin('compilation', function (compilation) { 28 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 29 | hotMiddleware.publish({ action: 'reload' }) 30 | cb() 31 | }) 32 | }) 33 | 34 | // proxy api requests 35 | Object.keys(proxyTable).forEach(function (context) { 36 | var options = proxyTable[context] 37 | if (typeof options === 'string') { 38 | options = { target: options } 39 | } 40 | app.use(proxyMiddleware(options.filter || context, options)) 41 | }) 42 | 43 | // handle fallback for HTML5 history API 44 | app.use(require('connect-history-api-fallback')()) 45 | 46 | // serve webpack bundle output 47 | app.use(devMiddleware) 48 | 49 | // enable hot-reload and state-preserving 50 | // compilation error display 51 | app.use(hotMiddleware) 52 | 53 | // serve pure static assets 54 | var staticPath = path.posix.join(config.assetsPublicPath, config.assetsSubDirectory) 55 | app.use(staticPath, express.static(path.join(__dirname, '../static'))) 56 | 57 | module.exports = app.listen(port, function (err) { 58 | if (err) { 59 | console.log(err) 60 | return 61 | } 62 | var uri = 'http://localhost:' + port 63 | console.log('Listening at ' + uri + '\n') 64 | }) 65 | -------------------------------------------------------------------------------- /template/client/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require({{#if_eq mock "mock"}}'../config'{{/if_eq}}{{#if_eq mock "backend"}}'../../config'{{/if_eq}}).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 | 13 | var cssLoader = { 14 | loader: 'css-loader', 15 | options: { 16 | minimize: process.env.NODE_ENV === 'production', 17 | sourceMap: options.sourceMap 18 | } 19 | } 20 | 21 | // generate loader string to be used with extract text plugin 22 | function generateLoaders (loader, loaderOptions) { 23 | var loaders = [cssLoader] 24 | if (loader) { 25 | loaders.push({ 26 | loader: loader + '-loader', 27 | options: Object.assign({}, loaderOptions, { 28 | sourceMap: options.sourceMap 29 | }) 30 | }) 31 | } 32 | 33 | // Extract CSS when that option is specified 34 | // (which is the case during production build) 35 | if (options.extract) { 36 | return ExtractTextPlugin.extract({ 37 | use: loaders, 38 | fallback: 'vue-style-loader' 39 | }) 40 | } else { 41 | return ['vue-style-loader'].concat(loaders) 42 | } 43 | } 44 | 45 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 46 | return { 47 | css: generateLoaders(), 48 | postcss: generateLoaders(), 49 | less: generateLoaders('less'), 50 | sass: generateLoaders('sass', { indentedSyntax: true }), 51 | scss: generateLoaders('sass'), 52 | stylus: generateLoaders('stylus'), 53 | styl: generateLoaders('stylus') 54 | } 55 | } 56 | 57 | // Generate loaders for standalone style files (outside of .vue) 58 | exports.styleLoaders = function (options) { 59 | var output = [] 60 | var loaders = exports.cssLoaders(options) 61 | for (var extension in loaders) { 62 | var loader = loaders[extension] 63 | output.push({ 64 | test: new RegExp('\\.' + extension + '$'), 65 | use: loader 66 | }) 67 | } 68 | return output 69 | } 70 | -------------------------------------------------------------------------------- /template/client/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require({{#if_eq mock "mock"}}'../config'{{/if_eq}}{{#if_eq mock "backend"}}'../../config'{{/if_eq}}).frontend 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: config.cssSourceMap, 8 | extract: isProduction 9 | }), 10 | postcss: [ 11 | require('autoprefixer')({ 12 | browsers: ['last 2 versions'] 13 | }) 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /template/client/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require({{#if_eq mock "mock"}}'../config'{{/if_eq}}{{#if_eq mock "backend"}}'../../config'{{/if_eq}}).frontend 3 | var vueLoaderConfig = require('./vue-loader.conf') 4 | var utils = require('./utils') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: resolve('src/main.js') 13 | }, 14 | output: { 15 | path: config.assetsRoot, 16 | publicPath: config.assetsPublicPath, 17 | filename: '[name].js' 18 | }, 19 | resolve: { 20 | extensions: ['.js', '.vue', '.json'], 21 | modules: [resolve('src'), resolve({{#if_eq mock "mock"}}'node_modules'{{/if_eq}}{{#if_eq mock "backend"}}'../node_modules'{{/if_eq}})], 22 | alias: { 23 | 'vue$': 'vue/dist/vue.common.js', 24 | 'src': resolve('src'), 25 | 'assets': resolve('src/assets'), 26 | 'components': resolve('src/components'), 27 | 'locales': resolve('src/locales'), 28 | 'shared': resolve('src/shared'), 29 | 'resources': resolve('src/resources') 30 | } 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.(js|vue)$/, 36 | loader: 'eslint-loader', 37 | enforce: 'pre', 38 | include: [resolve('src'), resolve('test')], 39 | options: { 40 | formatter: require('eslint-friendly-formatter') 41 | } 42 | }, 43 | { 44 | test: /\.vue$/, 45 | loader: 'vue-loader', 46 | options: vueLoaderConfig 47 | }, 48 | { 49 | test: /\.js$/, 50 | loader: 'babel-loader', 51 | include: [resolve('src')] 52 | }, 53 | { 54 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 55 | loader: 'url-loader', 56 | query: { 57 | limit: 10000, 58 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 59 | } 60 | }, 61 | { 62 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 63 | loader: 'url-loader', 64 | query: { 65 | limit: 10000, 66 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 67 | } 68 | } 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /template/client/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var config = require({{#if_eq mock "mock"}}'../config'{{/if_eq}}{{#if_eq mock "backend"}}'../../config'{{/if_eq}}).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 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = [{{#if_eq mock "mock"}}'./build/dev-client'{{/if_eq}}{{#if_eq mock "backend"}}'./client/build/dev-client'{{/if_eq}}].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.cssSourceMap }) 17 | }, 18 | // cheap-module-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': '"development"' 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: '{{#if_eq mock "mock"}}index.html{{/if_eq}}{{#if_eq mock "backend"}}client/index.html{{/if_eq}}', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /template/client/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require({{#if_eq mock "mock"}}'../config'{{/if_eq}}{{#if_eq mock "backend"}}'../../config'{{/if_eq}}).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 HtmlWebpackPlugin = require('html-webpack-plugin') 8 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 9 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 10 | 11 | var webpackConfig = merge(baseWebpackConfig, { 12 | module: { 13 | rules: utils.styleLoaders({ sourceMap: config.cssSourceMap, extract: true }) 14 | }, 15 | devtool: config.cssSourceMap ? '#source-map' : false, 16 | output: { 17 | path: config.assetsRoot, 18 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 19 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 20 | }, 21 | plugins: [ 22 | // http://vuejs.github.io/vue-loader/workflow/production.html 23 | new webpack.DefinePlugin({ 24 | 'process.env': '"production"' 25 | }), 26 | new webpack.optimize.UglifyJsPlugin({ 27 | sourceMap: true, 28 | compress: { 29 | warnings: false 30 | } 31 | }), 32 | // extract css into its own file 33 | new ExtractTextPlugin({ 34 | filename: utils.assetsPath('css/[name].[contenthash].css') 35 | }), 36 | // Compress extracted CSS. We are using this plugin so that possible 37 | // duplicated CSS from different components can be deduped. 38 | new OptimizeCSSPlugin(), 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: '{{#if_eq mock "mock"}}index.html{{/if_eq}}{{#if_eq mock "backend"}}client/index.html{{/if_eq}}', 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, {{#if_eq mock "mock"}}'../node_modules'{{/if_eq}}{{#if_eq mock "backend"}}'../../node_modules'{{/if_eq}}) 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, {{#if_eq mock "mock"}}'../node_modules/element-ui'{{/if_eq}}{{#if_eq mock "backend"}}'../../node_modules/element-ui'{{/if_eq}}) 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 | if (config.bundleAnalyzerReport) { 121 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 122 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 123 | } 124 | 125 | module.exports = webpackConfig 126 | -------------------------------------------------------------------------------- /template/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{name}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /template/client/src/App.vue: -------------------------------------------------------------------------------- 1 | 14 | 33 | 88 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/client/src/assets/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack/3bcece5a3effc8ea96125e14c264771373a1d6f8/template/client/src/assets/fonts/iconfont.eot -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/client/src/assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack/3bcece5a3effc8ea96125e14c264771373a1d6f8/template/client/src/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /template/client/src/assets/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack/3bcece5a3effc8ea96125e14c264771373a1d6f8/template/client/src/assets/fonts/iconfont.woff -------------------------------------------------------------------------------- /template/client/src/assets/images/login-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack/3bcece5a3effc8ea96125e14c264771373a1d6f8/template/client/src/assets/images/login-bg.jpg -------------------------------------------------------------------------------- /template/client/src/components/ContentLoading.vue: -------------------------------------------------------------------------------- 1 | 11 | 21 | 55 | -------------------------------------------------------------------------------- /template/client/src/components/ContentModule.vue: -------------------------------------------------------------------------------- 1 | 26 | 52 | -------------------------------------------------------------------------------- /template/client/src/components/DataTable.vue: -------------------------------------------------------------------------------- 1 | 17 | 85 | 129 | -------------------------------------------------------------------------------- /template/client/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 69 | 174 | 202 | -------------------------------------------------------------------------------- /template/client/src/components/NProgress.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 36 | -------------------------------------------------------------------------------- /template/client/src/components/NavMenu.vue: -------------------------------------------------------------------------------- 1 | 12 | 29 | 55 | -------------------------------------------------------------------------------- /template/client/src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 21 | 55 | 88 | -------------------------------------------------------------------------------- /template/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 | {{#if i18n}}export const STORE_KEY_CONFIG_LANG = 'config.lang' 8 | {{/if}}export const STORE_KEY_CONFIG_PAGE_LIMIT = 'config.page.limit' 9 | -------------------------------------------------------------------------------- /template/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, 6 | Tag, Tree, Notification, Slider, Loading, Row, Col, Upload, Progress, Spinner, 7 | Message, Badge, Card, Steps, Step } from 'element-ui' 8 | 9 | Vue.component(Dialog.name, Dialog) 10 | Vue.component(Autocomplete.name, Autocomplete) 11 | Vue.component(Dropdown.name, Dropdown) 12 | Vue.component(DropdownMenu.name, DropdownMenu) 13 | Vue.component(DropdownItem.name, DropdownItem) 14 | Vue.component(Input.name, Input) 15 | Vue.component(InputNumber.name, InputNumber) 16 | Vue.component(Radio.name, Radio) 17 | Vue.component(RadioGroup.name, RadioGroup) 18 | Vue.component(RadioButton.name, RadioButton) 19 | Vue.component(Checkbox.name, Checkbox) 20 | Vue.component(CheckboxGroup.name, CheckboxGroup) 21 | Vue.component(Switch.name, Switch) 22 | Vue.component(Select.name, Select) 23 | Vue.component(Option.name, Option) 24 | Vue.component(OptionGroup.name, OptionGroup) 25 | Vue.component(Button.name, Button) 26 | Vue.component(ButtonGroup.name, ButtonGroup) 27 | Vue.component(Table.name, Table) 28 | Vue.component(TableColumn.name, TableColumn) 29 | Vue.component(DatePicker.name, DatePicker) 30 | Vue.component(TimeSelect.name, TimeSelect) 31 | Vue.component(TimePicker.name, TimePicker) 32 | Vue.component(Popover.name, Popover) 33 | Vue.component(Tooltip.name, Tooltip) 34 | Vue.component(Breadcrumb.name, Breadcrumb) 35 | Vue.component(BreadcrumbItem.name, BreadcrumbItem) 36 | Vue.component(Form.name, Form) 37 | Vue.component(FormItem.name, FormItem) 38 | Vue.component(Tabs.name, Tabs) 39 | Vue.component(TabPane.name, TabPane) 40 | Vue.component(Tag.name, Tag) 41 | Vue.component(Tree.name, Tree) 42 | Vue.component(Slider.name, Slider) 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 | -------------------------------------------------------------------------------- /template/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 | 10 | const requestMap = {} 11 | 12 | // response interceptor 13 | Vue.http.interceptors.push((request, next) => { 14 | let key 15 | // abort the same post request 16 | if (/POST|PUT|DELETE/.test(request.method)) { 17 | key = `${request.method}${request.url}${JSON.stringify(request.body)}` 18 | // abort the existed request 19 | if (key && requestMap[key]) { 20 | key = null 21 | setTimeout(() => { 22 | request.abort() 23 | }) 24 | } else { 25 | requestMap[key] = request 26 | } 27 | } 28 | 29 | if (store.getters.loggedIn) { 30 | // if logged in, add the token to the header 31 | request.headers.set('Authorization', `Bearer ${store.getters.accessToken}`) 32 | } 33 | next((response) => { 34 | // delete current request in the map 35 | if (key) { 36 | delete requestMap[key] 37 | } 38 | // don't handler for login page 39 | if (store.state.route.path === '/login') { 40 | return 41 | } 42 | if (response.status === 401) { 43 | Message.error(Vue.t('http.error.E401')) 44 | store.dispatch('logout').then(() => { 45 | const location = window.location 46 | const path = store.state.route.fullpath || (location.pathname + location.search) 47 | router.push({ path: '/login', query: { redirect: path }}) 48 | }) 49 | return 50 | } 51 | if (response.status === 403) { 52 | Message.error(Vue.t('http.error.E403')) 53 | return 54 | } 55 | if (response.status === 404) { 56 | Message.error(Vue.t('http.error.E404')) 57 | return 58 | } 59 | if (response.status === 500) { 60 | Message.error(Vue.t('http.error.E500')) 61 | return 62 | } 63 | // other errors 64 | if (!response.ok) { 65 | Message.error({ 66 | message: response.data || Vue.t('http.error.others') 67 | }) 68 | } 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /template/client/src/locales/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'XXX Backend', 3 | constant: { 4 | name: 'Name', 5 | desc: 'Description' 6 | }, 7 | confirm: { 8 | title: 'Warning', 9 | ok: 'save', 10 | cancel: 'cancel', 11 | prevStep: 'Previous', 12 | nextStep: 'Next', 13 | remove: 'This will remove the selected {content} forever, continue?', 14 | confirmSelected: 'You have selected the following items. Please confirm your choices as this action can\'t be recoveried' 15 | }, 16 | label: { 17 | name: 'Name', 18 | enable: 'Enable' 19 | }, 20 | status: { 21 | enabled: 'Enabled', 22 | disabled: 'Disabled' 23 | }, 24 | operation: { 25 | add: 'Add', 26 | create: 'Create', 27 | edit: 'Edit', 28 | update: 'Update', 29 | remove: 'Remove', 30 | multiRemove: 'Multi remove', 31 | operation: 'Operation', 32 | search: 'Search', 33 | enable: 'Click to enable', 34 | disable: 'Click to disable' 35 | }, 36 | message: { 37 | save: { 38 | ok: 'Saved!', 39 | err: 'Error occured when saving!' 40 | }, 41 | error: 'Error', 42 | created: 'Create successed', 43 | createFailed: 'Create failed', 44 | updated: 'Update successed', 45 | updateFailed: 'Update failed', 46 | removed: 'Delete successed', 47 | removeFailed: 'Delete failed' 48 | }, 49 | http: { 50 | error: { 51 | E401: 'Not authorized', 52 | E403: 'Permission not allowed', 53 | E404: 'Url not found', 54 | E500: 'Server error', 55 | others: 'Some error occured, please try again' 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /template/client/src/locales/header/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | header: { 3 | settings: 'User settings', 4 | password: 'Password', 5 | logout: 'Logout', 6 | langSetting: 'Lang', 7 | pageLimit: 'Data count limit per page', 8 | _password: { 9 | description: 'Change your password. It\'s strongly recommend that you should pick a complex password.', 10 | old: 'Old password', 11 | _new: 'New password', 12 | newConfirm: 'Confirm new password', 13 | rules: { 14 | old: 'Please input old password', 15 | _new: 'Please input new password', 16 | newConfirm: 'Please input new password again', 17 | notMatch: 'The two new password not matched' 18 | }, 19 | afterChange: 'Password has changed, system will logout automaticaly, please re-login with the new password.' 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /template/client/src/locales/header/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | import zhCN from './zh-CN' 3 | 4 | export default { 5 | 'zh-CN': zhCN, 6 | en 7 | } 8 | -------------------------------------------------------------------------------- /template/client/src/locales/header/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | header: { 3 | settings: '用户设置', 4 | password: '修改密码', 5 | logout: '退出', 6 | langSetting: '语言', 7 | pageLimit: '每页条目数', 8 | _password: { 9 | description: '修改你的密码。强烈建议您选择一个复杂密码。', 10 | old: '旧密码', 11 | _new: '新密码', 12 | newConfirm: '确认新密码', 13 | rules: { 14 | old: '请输入旧密码', 15 | _new: '请输入新密码', 16 | newConfirm: '请再次确认新密码', 17 | notMatch: '两次输入的新密码不一致' 18 | }, 19 | afterChange: '密码已修改,将自动退出,请使用新密码重新登录。' 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /template/client/src/locales/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import { merge } from 'lodash' 4 | import { lang } from '../stored' 5 | 6 | import zhCN from './zh-CN' 7 | import en from './en' 8 | import eleZhCN from 'element-ui/lib/locale/lang/zh-CN' 9 | import eleEn from 'element-ui/lib/locale/lang/en' 10 | 11 | const locales = { 12 | 'zh-CN': merge(zhCN, eleZhCN), 13 | en: merge(en, eleEn) 14 | } 15 | 16 | Vue.use(VueI18n) 17 | 18 | Vue.config.lang = lang 19 | Vue.config.fallbackLang = 'zh-CN' 20 | 21 | // set locales 22 | Object.keys(locales).forEach(lang => { 23 | Vue.locale(lang, locales[lang]) 24 | }) 25 | -------------------------------------------------------------------------------- /template/client/src/locales/login/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | login: { 3 | username: 'Please input the username', 4 | password: 'Please input the password', 5 | button: 'Log in', 6 | authFail: 'Username or password is not correct' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /template/client/src/locales/login/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | import zhCN from './zh-CN' 3 | 4 | export default { 5 | 'zh-CN': zhCN, 6 | en 7 | } 8 | -------------------------------------------------------------------------------- /template/client/src/locales/login/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | login: { 3 | username: '请输入用户名', 4 | password: '请输入密码', 5 | button: '登录', 6 | authFail: '用户名或密码错误' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /template/client/src/locales/menu/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | menu: { 3 | users: 'Users', 4 | things: 'Things' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /template/client/src/locales/menu/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | import zhCN from './zh-CN' 3 | 4 | export default { 5 | 'zh-CN': zhCN, 6 | en 7 | } 8 | -------------------------------------------------------------------------------- /template/client/src/locales/menu/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | menu: { 3 | users: '用户列表', 4 | things: '事件列表' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /template/client/src/locales/pagination/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | pagination: { 3 | current: 'current', 4 | currentAppend: 'page', 5 | pages: 'total', 6 | pagesAppend: 'page' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /template/client/src/locales/pagination/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | import zhCN from './zh-CN' 3 | 4 | export default { 5 | 'zh-CN': zhCN, 6 | en 7 | } 8 | -------------------------------------------------------------------------------- /template/client/src/locales/pagination/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | pagination: { 3 | current: '当前第', 4 | currentAppend: '页', 5 | pages: '共', 6 | pagesAppend: '页' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /template/client/src/locales/things/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | thing: { 3 | breadcrumb: { 4 | home: 'Home', 5 | current: 'Things' 6 | }, 7 | model: { 8 | name: 'name', 9 | description: 'description' 10 | }, 11 | rules: { 12 | name: 'Please input the name' 13 | }, 14 | edit: { 15 | create: 'Add thing', 16 | update: 'Update thing' 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /template/client/src/locales/things/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | import zhCN from './zh-CN' 3 | 4 | export default { 5 | 'zh-CN': zhCN, 6 | en 7 | } 8 | -------------------------------------------------------------------------------- /template/client/src/locales/things/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | thing: { 3 | breadcrumb: { 4 | home: '首页', 5 | current: '事件管理' 6 | }, 7 | model: { 8 | name: '名字', 9 | description: '描述' 10 | }, 11 | rules: { 12 | name: '请输入名称' 13 | }, 14 | edit: { 15 | create: '新增事件', 16 | update: '编辑事件' 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /template/client/src/locales/users/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | user: { 3 | breadcrumb: { 4 | home: 'Home', 5 | current: 'Users' 6 | }, 7 | model: { 8 | username: 'Username', 9 | role: 'Role', 10 | password: 'Password' 11 | }, 12 | create: { 13 | title: 'Create a user' 14 | }, 15 | rules: { 16 | username: 'Please input the username', 17 | password: 'Please input the password' 18 | }, 19 | action: { 20 | userExisted: 'User existed' 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /template/client/src/locales/users/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | import zhCN from './zh-CN' 3 | 4 | export default { 5 | 'zh-CN': zhCN, 6 | en 7 | } 8 | -------------------------------------------------------------------------------- /template/client/src/locales/users/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | user: { 3 | breadcrumb: { 4 | home: '首页', 5 | current: '用户管理' 6 | }, 7 | model: { 8 | username: '用户名', 9 | role: '角色', 10 | password: '密码' 11 | }, 12 | create: { 13 | title: '创建用户' 14 | }, 15 | rules: { 16 | username: '请输入用户名', 17 | password: '请输入密码' 18 | }, 19 | action: { 20 | userExisted: '用户已存在' 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /template/client/src/locales/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'XXX管理系统', 3 | constant: { 4 | name: '名称', 5 | desc: '描述' 6 | }, 7 | confirm: { 8 | title: '提示', 9 | ok: '确 定', 10 | cancel: '取 消', 11 | prevStep: '上一步', 12 | nextStep: '下一步', 13 | remove: '此操作将永久删除所选{content},是否继续?', 14 | confirmSelected: '您已经选择了下列选项。请确认您的选择。这个动作不能撤消!' 15 | }, 16 | label: { 17 | name: '名称', 18 | enable: '是否已启用' 19 | }, 20 | status: { 21 | enabled: '已启用', 22 | disabled: '已禁用' 23 | }, 24 | form: { 25 | enable: '是否启用', 26 | enabled: '启用' 27 | }, 28 | operation: { 29 | add: '添加', 30 | create: '创建', 31 | edit: '编辑', 32 | update: '更新', 33 | remove: '删除', 34 | multiRemove: '批量删除', 35 | operation: '操作', 36 | search: '搜索', 37 | enable: '点击启用', 38 | disable: '点击禁用' 39 | }, 40 | message: { 41 | save: { 42 | ok: '已保存!', 43 | err: '保存失败!' 44 | }, 45 | error: '错误', 46 | created: '新增成功', 47 | createFailed: '新增失败', 48 | updated: '已保存更改', 49 | updateFailed: '更新失败', 50 | removed: '删除成功', 51 | removeFailed: '删除失败' 52 | }, 53 | http: { 54 | error: { 55 | E401: '身份认证失败', 56 | E403: '权限不足', 57 | E404: '请求路径错误', 58 | E500: '后台错误', 59 | others: '操作失败,请重试' 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /template/client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | // read localStorage stored data 3 | import './stored' 4 | {{#if i18n}} 5 | // locale 6 | import './locales' 7 | {{/if}} 8 | 9 | // router and store 10 | import store from './store' 11 | import router, { hook as routerHook } from './router' 12 | import { sync } from 'vuex-router-sync' 13 | sync(store, router) 14 | 15 | // ui library 16 | import './element-ui' 17 | 18 | // ajax 19 | import './http' 20 | 21 | const userPromise = store.dispatch('initUserInfo') 22 | routerHook(userPromise) 23 | 24 | // main component 25 | import App from './App' 26 | 27 | import './socket' 28 | 29 | userPromise.then(() => { 30 | const app = new Vue({ 31 | router, 32 | store, 33 | ...App // Object spread copying everything from App.vue 34 | }) 35 | // actually mount to DOM 36 | app.$mount('app') 37 | }) 38 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 otherModuleRoutes from './module' 8 | Vue.use(VueRouter) 9 | 10 | const routes = [{ 11 | path: '/login', 12 | component: (resolve) => { 13 | import('../view/auth/Login.vue').then(resolve) 14 | }, 15 | meta: { 16 | skipAuth: true 17 | } 18 | }, { 19 | path: '/', 20 | component: (resolve) => { 21 | import('../view/CommonView.vue').then(resolve) 22 | }, 23 | children: [...otherModuleRoutes, { 24 | path: '/', redirect: '/dashboard' 25 | }] 26 | }, { 27 | path: '*', 28 | component: { 29 | render (h) { 30 | return h('div', { staticClass: 'flex flex-main-center', attrs: { style: 'width:100%;font-size:32px' }}, 'Page not found') 31 | } 32 | } 33 | }] 34 | 35 | const router = new VueRouter({ 36 | mode: 'history', 37 | linkActiveClass: 'active', 38 | scrollBehavior: () => ({ y: 0 }), 39 | routes 40 | }) 41 | 42 | export function hook (userPromise) { 43 | // router 44 | router.beforeEach((to, from, next) => { 45 | // 确保用户身份信息已获取 46 | userPromise.then(() => { 47 | store.dispatch('changeRouteLoading', true).then(() => { 48 | // has logged in, reject 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 | 75 | export default router 76 | -------------------------------------------------------------------------------- /template/client/src/router/module.js: -------------------------------------------------------------------------------- 1 | export default [{ 2 | path: '/dashboard', 3 | component: (resolve) => { 4 | import('../view/Dashboard.vue').then(resolve) 5 | } 6 | }, { 7 | path: '/users', 8 | component: (resolve) => { 9 | import('../view/UserList.vue').then(resolve) 10 | } 11 | }, { 12 | path: '/things', 13 | component: (resolve) => { 14 | import('../view/ThingList.vue').then(resolve) 15 | } 16 | }] 17 | -------------------------------------------------------------------------------- /template/client/src/shared/search.js: -------------------------------------------------------------------------------- 1 | // 从数组中筛选含搜索字段的新数组 2 | export default function (array, searchKey) { 3 | return array.filter(obj => { 4 | return Object.keys(obj).some(key => { 5 | return String(obj[key]).toLowerCase().indexOf(searchKey.toLowerCase()) > -1 6 | }) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /template/client/src/shared/validators.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | export const EMAIL_REGEX = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9_-]+?\.[a-zA-Z]{2,5}$/ 3 | 4 | function commonValidator (regex, defaultMessage, rule, value, callback) { 5 | if (!value || regex.test(value)) { 6 | callback() 7 | } else { 8 | callback(new Error(rule.message || defaultMessage)) 9 | } 10 | } 11 | 12 | /** 13 | * 邮箱验证 14 | */ 15 | export function email (rule, value, callback) { 16 | commonValidator(EMAIL_REGEX, Vue.t('validation.email'), rule, value, callback) 17 | } 18 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/client/src/store/modules/global-config.js: -------------------------------------------------------------------------------- 1 | {{#if i18n}}import Vue from 'vue'{{/if}} 2 | import { {{#if i18n}}lang, {{/if}}pageLimit } from '../../stored' 3 | import { save } from '../../storage' 4 | import { {{#if i18n}}STORE_KEY_CONFIG_LANG, {{/if}}STORE_KEY_CONFIG_PAGE_LIMIT } from '../../constants' 5 | 6 | const state = { 7 | {{#if i18n}}lang: lang, 8 | // value see http://stackoverflow.com/questions/5580876/navigator-language-list-of-all-languages 9 | langs: [{ 10 | label: '中文', 11 | value: 'zh-CN' 12 | }, { 13 | label: 'English', 14 | value: 'en' 15 | }], 16 | {{/if}}pageLimit: pageLimit 17 | } 18 | 19 | const mutations = { 20 | UPDATE (state, config) { 21 | {{#if i18n}}state.lang = config.lang || state.lang 22 | {{/if}}state.pageLimit = config.pageLimit || state.pageLimit 23 | }{{#if i18n}}, 24 | UPDATE_LANG (state, lang) { 25 | state.lang = lang || state.lang 26 | }{{/if}} 27 | } 28 | 29 | const actions = { 30 | {{#if i18n}}changeLang ({ commit }, lang) { 31 | Vue.config.lang = lang 32 | commit('UPDATE_LANG', lang) 33 | save(STORE_KEY_CONFIG_LANG, lang) 34 | }, 35 | {{/if}}updateGlobalConfig ({ commit, state, dispatch }, config) { 36 | {{#if i18n}}if (config.lang !== state.lang) { 37 | Vue.config.lang = config.lang 38 | save(STORE_KEY_CONFIG_LANG, config.lang) 39 | } 40 | {{/if}}commit('UPDATE', config) 41 | {{#if i18n}}save(STORE_KEY_CONFIG_LANG, state.lang) 42 | {{/if}}save(STORE_KEY_CONFIG_PAGE_LIMIT, state.pageLimit) 43 | } 44 | } 45 | 46 | const getters = { 47 | globalConfig (state) { 48 | return state 49 | } 50 | } 51 | 52 | export default { 53 | state, 54 | mutations, 55 | actions, 56 | getters 57 | } 58 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/client/src/store/modules/user.api.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { authSocket } from '../../socket' 3 | 4 | export function login (username, password) { 5 | return Vue.http.post('auth/local', { 6 | username, 7 | password 8 | }).then(res => res.json()) 9 | } 10 | 11 | export function getUserInfo (token) { 12 | return new Promise((resolve) => { 13 | Vue.http.get('users/me', { 14 | headers: { 15 | 'Authorization': `Bearer ${token}` 16 | } 17 | }).then(data => data.json()).then(data => { 18 | authSocket(token, () => { 19 | console.log('Token authenticated.') 20 | }) 21 | resolve(data) 22 | }).catch(() => { 23 | resolve({}) 24 | }) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /template/client/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash' 2 | import { saveMulti, clearMulti } from '../../storage' 3 | import { login, getUserInfo } from './user.api' 4 | // eslint-disable-next-line camelcase 5 | import { username, access_token, refresh_token } from '../../stored' 6 | import { STORE_KEY_USERNAME, STORE_KEY_ACCESS_TOKEN, STORE_KEY_REFRESH_TOKEN } from '../../constants' 7 | 8 | const state = { 9 | _id: '', 10 | role: 'guest', 11 | username: username, 12 | access_token, // eslint-disable-line 13 | refresh_token // eslint-disable-line 14 | } 15 | 16 | const mutations = { 17 | // set user info 18 | SET_USER_INFO (state, userInfo) { 19 | merge(state, userInfo) 20 | }, 21 | // after logout 22 | LOGOUT (state) { 23 | state._id = '' 24 | state.username = '' 25 | state.role = 'guest' 26 | state.access_token = '' // eslint-disable-line 27 | state.refresh_token = '' // eslint-disable-line 28 | } 29 | } 30 | 31 | const actions = { 32 | // init user info 33 | initUserInfo ({ commit, dispatch, state }) { 34 | return new Promise((resolve, reject) => { 35 | // token 36 | if (username) { 37 | getUserInfo(state.access_token).then(data => { // eslint-disable-line 38 | if (data._id) { 39 | commit('SET_USER_INFO', data) 40 | } 41 | resolve(data) 42 | }).catch(err => { reject(err) }) 43 | } else { 44 | resolve() 45 | } 46 | }) 47 | }, 48 | // login action 49 | login ({ commit, dispatch }, payload) { 50 | return new Promise((resolve, reject) => { 51 | login(payload.username, payload.password).then(data => { 52 | getUserInfo(data.token).then(user => { 53 | const userInfo = merge({}, user, { 54 | username: payload.username, 55 | access_token: data.token, // eslint-disable-line 56 | refresh_token: '' // eslint-disable-line 57 | }) 58 | commit('SET_USER_INFO', userInfo) 59 | saveMulti([{ 60 | key: STORE_KEY_USERNAME, 61 | value: userInfo.username 62 | }, { 63 | key: STORE_KEY_ACCESS_TOKEN, 64 | value: userInfo.access_token // eslint-disable-line 65 | }, { 66 | key: STORE_KEY_REFRESH_TOKEN, 67 | value: userInfo.refresh_token // eslint-disable-line 68 | }]) 69 | resolve() 70 | }).catch(() => {}) 71 | }).catch(err => { reject(err) }) 72 | }) 73 | }, 74 | // refresh token action 75 | refreToken ({ commit }, payload) { 76 | commit('REFERE_TOKEN', payload) 77 | saveMulti([{ 78 | key: STORE_KEY_ACCESS_TOKEN, 79 | value: payload.access_token // eslint-disable-line 80 | }, { 81 | key: STORE_KEY_REFRESH_TOKEN, 82 | value: payload.refresh_token // eslint-disable-line 83 | }]) 84 | }, 85 | // logout action 86 | logout ({ commit }, payload) { 87 | commit('LOGOUT') 88 | clearMulti([STORE_KEY_USERNAME, STORE_KEY_ACCESS_TOKEN, STORE_KEY_REFRESH_TOKEN]) 89 | } 90 | } 91 | 92 | const getters = { 93 | userId (state) { 94 | return state._id 95 | }, 96 | userRole (state) { 97 | return state.role 98 | }, 99 | accessToken (state) { 100 | return state.access_token // eslint-disable-line 101 | }, 102 | username (state) { 103 | return state.username 104 | }, 105 | loggedIn (state) { 106 | return !!(state.username && state.access_token) // eslint-disable-line 107 | } 108 | } 109 | 110 | export default { 111 | state, 112 | mutations, 113 | actions, 114 | getters 115 | } 116 | -------------------------------------------------------------------------------- /template/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) || '' // eslint-disable-line 8 | // eslint-disable-next-line camelcase 9 | export const refresh_token = read(STORE_KEY_REFRESH_TOKEN) || '' // eslint-disable-line 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 | -------------------------------------------------------------------------------- /template/client/src/view/CommonView.vue: -------------------------------------------------------------------------------- 1 | 8 | 20 | -------------------------------------------------------------------------------- /template/client/src/view/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 7 | 21 | -------------------------------------------------------------------------------- /template/client/src/view/ThingList.vue: -------------------------------------------------------------------------------- 1 | 38 | 120 | 135 | -------------------------------------------------------------------------------- /template/client/src/view/UserList.vue: -------------------------------------------------------------------------------- 1 | 39 | 124 | -------------------------------------------------------------------------------- /template/client/src/view/auth/Login.vue: -------------------------------------------------------------------------------- 1 | 32 | 89 | 142 | -------------------------------------------------------------------------------- /template/client/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erguotou520/vue-fullstack/3bcece5a3effc8ea96125e14c264771373a1d6f8/template/client/static/favicon.ico -------------------------------------------------------------------------------- /template/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Project config file includes dev/prod and frontend/backend 3 | */ 4 | var path = require('path') 5 | var _ = require('lodash') 6 | {{#if_eq mock 'backend'}} 7 | var backendBase = { 8 | // Root path of server 9 | root: path.normalize(__dirname), 10 | 11 | // Server port 12 | port: process.env.PORT || {{backendPort}}, 13 | 14 | // Secret for session, you will want to change this and make it an environment variable 15 | secrets: { 16 | session: process.env.SECRET || '{{name}}-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 | }{{/if_eq}} 31 | 32 | var development = { 33 | frontend: { 34 | port: {{frontendPort}}, 35 | assetsRoot: path.resolve(__dirname, './{{#if_eq mock "backend"}}client/{{/if_eq}}src'), 36 | assetsSubDirectory: 'static', 37 | assetsPublicPath: '/', 38 | proxyTable: { 39 | '/api': { target: 'http://localhost:' + {{#if_eq mock 'backend'}}backendBase.port{{/if_eq}}{{#if_eq mock 'mock'}}{{mockPort}}{{/if_eq}}, changeOrigin: true{{#if_eq mock "mock"}}, pathRewrite: { '^/api/': '' }{{/if_eq}} }, 40 | '/socket.io': { target: 'http://localhost:' + {{#if_eq mock 'backend'}}backendBase.port{{/if_eq}}{{#if_eq mock 'mock'}}{{mockPort}}{{/if_eq}}, 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 | }{{#if_eq mock 'backend'}}, 49 | backend: _.merge({}, backendBase, { 50 | mongo: { 51 | uri: {{#if mongoUri}}'{{mongoUri}}-dev'{{else}}'mongodb://localhost/{{name}}-dev'{{/if}} 52 | } 53 | }){{/if_eq}}{{#if_eq mock 'mock'}}, 54 | mock: { 55 | port: {{mockPort}} 56 | }{{/if_eq}} 57 | } 58 | var production = { 59 | frontend: { 60 | index: path.resolve(__dirname, './{{#if_eq mock "backend"}}client/{{/if_eq}}dist/index.html'), 61 | assetsRoot: path.resolve(__dirname, './{{#if_eq mock "backend"}}client/{{/if_eq}}dist'), 62 | assetsSubDirectory: 'static', 63 | assetsPublicPath: '/', 64 | cssSourceMap: true, 65 | // Gzip off by default as many popular static hosts such as 66 | // Surge or Netlify already gzip all static assets for you. 67 | // Before setting to `true`, make sure to: 68 | // npm install --save-dev compression-webpack-plugin 69 | productionGzip: false, 70 | productionGzipExtensions: ['js', 'css'] 71 | }{{#if_eq mock 'backend'}}, 72 | backend: _.merge({}, backendBase, { 73 | // whether backend servers the frontend, you can use nginx to server frontend and proxy to backend services 74 | // if set to true, you need no web services like nginx 75 | serverFrontend: true, 76 | // Server IP 77 | ip: process.env.APP_HOST || process.env.APP_IP || process.env.HOST || process.env.IP, 78 | // Server port 79 | port: process.env.APP_PORT || process.env.PORT, 80 | // MongoDB connection options 81 | mongo: { 82 | uri: process.env.MONGODB_URI || process.env.MONGOHQ_URI || 83 | {{#if mongoUri}}'{{mongoUri}}'{{else}}'mongodb://localhost/{{name}}'{{/if}} 84 | }, 85 | 86 | // frontend folder 87 | frontend: path.resolve(__dirname, './client/dist') 88 | }){{/if_eq}}{{#if_eq mock 'mock'}}, 89 | mock: { 90 | port: process.env.APP_PORT || process.env.PORT 91 | }{{/if_eq}} 92 | } 93 | 94 | var config = process.env.NODE_ENV === 'production' ? production : development 95 | 96 | module.exports = _.assign({}, config) 97 | -------------------------------------------------------------------------------- /template/mock/README.md: -------------------------------------------------------------------------------- 1 | # Mock server 2 | *This folder just show the way to integrate the mock server, you can change `fms` and `Mock.js` to any other lib that you are familiar with.* 3 | 4 | * It will include all the `.js` file, so you don't need to include them manually. 5 | * `ajax` folder for web http mock and `socket` folder for socket.io request mock. 6 | * You can follow the `ajax/things.js` and `ajax/users.js` to write your own mock server. 7 | 8 | ## Doc reference 9 | [F.M.S](http://www.fmsjs.org/) 10 | [Mock.js](https://github.com/nuysoft/Mock/wiki) 11 | -------------------------------------------------------------------------------- /template/mock/ajax/things.js: -------------------------------------------------------------------------------- 1 | var fms = require('fms') 2 | var Mock = require('mockjs') 3 | 4 | fms.get({ 5 | title: 'Get thing list', 6 | url: '/things', 7 | request: { 8 | page: { 9 | current: 2, 10 | limit: 15 11 | } 12 | }, 13 | res: { 14 | ok: function () { 15 | return Mock.mock({ 16 | page: { 17 | total: '@natural(5,20)' 18 | }, 19 | 'results|5-8': [{ 20 | _id: '@uuid', 21 | name: '@name', 22 | info: '@paragraph', 23 | active: '@boolean' 24 | }] 25 | }) 26 | }, 27 | err: { 28 | message: 'error when quering' 29 | } 30 | } 31 | }) 32 | 33 | fms.get({ 34 | title: 'Get a specified thing', 35 | url: '/things/:id', 36 | resStatus: { 37 | err: 404 38 | }, 39 | res: { 40 | ok: function () { 41 | return Mock.mock({ 42 | _id: '@uuid', 43 | name: '@name', 44 | info: '@paragraph', 45 | active: '@boolean' 46 | }) 47 | } 48 | } 49 | }) 50 | 51 | fms.post({ 52 | title: 'Create a thing', 53 | url: '/things', 54 | request: { 55 | name: 'xxx', 56 | info: 'xxx' 57 | }, 58 | res: { 59 | ok: function () { 60 | return Mock.mock({ 61 | token: '@string(32)' 62 | }) 63 | } 64 | } 65 | }) 66 | 67 | fms.put({ 68 | title: 'Update a thing info', 69 | url: '/things/:id', 70 | request: { 71 | name: 'xxx', 72 | info: 'xxx', 73 | active: 'true|false' 74 | }, 75 | resStatus: { 76 | err: 403 77 | } 78 | }) 79 | 80 | fms.ajax({ 81 | title: 'Delete a thing', 82 | type: 'delete', 83 | url: '/things/:id', 84 | resStatus: { 85 | ok: 204, 86 | err: 403 87 | }, 88 | res: { 89 | ok: {} 90 | } 91 | }) 92 | -------------------------------------------------------------------------------- /template/mock/ajax/users.js: -------------------------------------------------------------------------------- 1 | var fms = require('fms') 2 | var Mock = require('mockjs') 3 | 4 | fms.post({ 5 | title: 'Login', 6 | url: '/auth/local', 7 | request: { 8 | username: 'xxx', 9 | password: 'xxx' 10 | }, 11 | resStatus: { 12 | err: 401 13 | }, 14 | res: { 15 | ok: function () { 16 | return Mock.mock({ 17 | token: '@string(32)' 18 | }) 19 | }, 20 | err: { 21 | message: 'Username or password is not correct.' 22 | } 23 | } 24 | }) 25 | 26 | fms.get({ 27 | title: 'Get user list', 28 | url: '/users', 29 | request: { 30 | page: { 31 | current: 2, 32 | limit: 15 33 | } 34 | }, 35 | res: { 36 | ok: function () { 37 | return Mock.mock({ 38 | page: { 39 | total: '@natural(5,20)' 40 | }, 41 | 'results|5-8': [{ 42 | _id: '@uuid', 43 | name: '@name', 44 | username: '@name', 45 | role: '@pick(["user","admin"])', 46 | provider: 'local' 47 | }] 48 | }) 49 | }, 50 | err: { 51 | message: 'error when quering' 52 | } 53 | } 54 | }) 55 | 56 | fms.get({ 57 | title: 'Get a specified user', 58 | url: '/users/:id', 59 | resStatus: { 60 | err: 404 61 | }, 62 | res: { 63 | ok: function () { 64 | return Mock.mock({ 65 | _id: '@uuid', 66 | name: '@name', 67 | username: '@name', 68 | role: '@pick(["user","admin"])', 69 | provider: 'local' 70 | }) 71 | } 72 | } 73 | }) 74 | 75 | fms.get({ 76 | title: 'Get user info', 77 | url: '/users/me', 78 | resStatus: { 79 | err: 401 80 | }, 81 | res: { 82 | ok: function () { 83 | return Mock.mock({ 84 | id: '@uuid' 85 | }) 86 | } 87 | } 88 | }) 89 | 90 | fms.post({ 91 | title: 'Create a user', 92 | url: '/users', 93 | request: { 94 | name: 'xxx', 95 | username: 'xxx', 96 | password: 'xxx' 97 | }, 98 | res: { 99 | ok: function () { 100 | return Mock.mock({ 101 | token: '@string(32)' 102 | }) 103 | } 104 | } 105 | }) 106 | 107 | fms.put({ 108 | title: 'Update user\'s password', 109 | url: '/users/:id/password', 110 | resStatus: { 111 | err: 403 112 | } 113 | }) 114 | 115 | fms.ajax({ 116 | title: 'Delete a user', 117 | type: 'delete', 118 | url: '/users/:id', 119 | resStatus: { 120 | ok: 204, 121 | err: 403 122 | }, 123 | res: { 124 | ok: {} 125 | } 126 | }) 127 | -------------------------------------------------------------------------------- /template/mock/index.js: -------------------------------------------------------------------------------- 1 | var fms = require('fms') 2 | var glob = require('glob') 3 | var path = require('path') 4 | 5 | // api prefix: /api 6 | fms.run({ 7 | port: require('../config').mock.port, 8 | root: process.cwd(), 9 | static: './dist', 10 | urlRewrite: [ 11 | '/', '/index.html', 12 | /^\/api/, '' 13 | ] 14 | }) 15 | 16 | // import all .js module 17 | glob.sync('ajax/**/*.js', { 18 | cwd: path.resolve(__dirname, './') 19 | }).forEach(function (file) { 20 | require(path.resolve(__dirname, file)) 21 | }) 22 | 23 | var socketio = require('socket.io')(fms.server) 24 | socketio.sockets.on('connection', function (socket) { 25 | socket.emit('authenticated') 26 | // Call onDisconnect. 27 | socket.on('disconnect', function () { 28 | console.info('DISCONNECTED') 29 | }) 30 | 31 | // import all .js module 32 | glob.sync('socket/**/*.js', { 33 | cwd: path.resolve(__dirname, './') 34 | }).forEach(function (file) { 35 | require(path.resolve(__dirname, file))(socket) 36 | }) 37 | console.info('CONNECTED') 38 | }) 39 | -------------------------------------------------------------------------------- /template/mock/socket/dashboard.js: -------------------------------------------------------------------------------- 1 | module.exports = function (socket) { 2 | socket.on('info', function (data) { 3 | console.info('[%s] %s', socket.address, JSON.stringify(data, null, 2)) 4 | }) 5 | socket.on('client:hello', function (data) { 6 | console.info('Received from client: %s', data) 7 | socket.emit('server:hello', 'server hello') 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /template/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/{{#if_eq mock 'backend'}}client/{{/if_eq}}dist; 20 | 21 | access_log logs/{{name}}.access.log main; 22 | error_log logs/{{name}}.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:{{#if_eq mock 'backend'}}{{backendPort}}{{/if_eq}}{{#if_eq mock 'mock'}}port{{/if_eq}}; 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 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{name}}", 3 | "version": "0.0.1", 4 | "keywords": [ 5 | "vue", 6 | "vuejs", 7 | "vuex", 8 | "vue-router", 9 | "vue-i18n", 10 | "vue-resource", 11 | "fullstack",{{#if_eq mock 'backend'}} 12 | "mongodb", 13 | "mongoose",{{/if_eq}} 14 | "socket.io", 15 | "element", 16 | "element-ui" 17 | ], 18 | "description": "{{description}}", 19 | "author": "{{author}}", 20 | "private": false, 21 | "scripts": { 22 | "client": "node {{#if_eq mock 'backend'}}client/{{/if_eq}}build/dev-server.js",{{#if_eq mock 'backend'}} 23 | "server": "./node_modules/.bin/nodemon --watch server server/app.js",{{/if_eq}}{{#if_eq mock 'mock'}} 24 | "mock": "./node_modules/.bin/nodemon --watch mock mock/index.js",{{/if_eq}} 25 | "dev": "npm run client|npm run {{#if_eq mock 'backend'}}server{{/if_eq}}{{#if_eq mock 'mock'}}mock{{/if_eq}}", 26 | "build": "node {{#if_eq mock 'backend'}}client/{{/if_eq}}build/build.js", 27 | {{#unless i18n}}"remove:i18n": "node tasks/replaceI18n.js", 28 | {{/unless}}{{#if_eq mock 'mock'}}"remove:mock": "node tasks/mock.js", 29 | {{/if_eq}}"lint": "eslint --ext .js,.vue {{#if_eq mock 'backend'}}client/{{/if_eq}}src" 30 | }, 31 | "dependencies": { 32 | {{#if_eq mock "backend"}} 33 | "body-parser": "^1.15.2", 34 | "composable-middleware": "^0.3.0", 35 | "compression": "^1.6.2", 36 | "cookie-parser": "^1.4.3", 37 | "crypto": "0.0.3", 38 | "ejs": "^2.5.2", 39 | {{/if_eq}} 40 | "element-ui": "^1.1.3", 41 | {{#if_eq mock "backend"}} 42 | "express": "^4.14.0", 43 | "express-jwt": "^5.1.0", 44 | {{/if_eq}}{{#if_eq mock "mock"}} 45 | "fms": "0.2.0", 46 | {{/if_eq}}{{#if_eq mock "backend"}} 47 | "jsonwebtoken": "^7.1.9", 48 | {{/if_eq}} 49 | "lodash": "^4.17.2", 50 | {{#if_eq mock "backend"}} 51 | "method-override": "^2.3.7", 52 | {{/if_eq}}{{#if_eq mock "mock"}} 53 | "mockjs": "^1.0.1-beta3", 54 | {{/if_eq}} 55 | "nodemon": "^1.10.2", 56 | {{#if_eq mock "backend"}} 57 | "mongoose": "^4.7.0", 58 | {{/if_eq}} 59 | "nprogress": "^0.2.0", 60 | {{#if_eq mock "backend"}} 61 | "passport": "^0.3.2", 62 | "passport-local": "^1.0.0", 63 | {{/if_eq}} 64 | "socket.io": "^1.7.1", 65 | "socket.io-client": "^1.7.1", 66 | {{#if_eq mock "backend"}} 67 | "socketio-jwt": "^4.5.0", 68 | {{/if_eq}} 69 | "vue": "^2.1.3", 70 | "vue-i18n": "^5.0.3", 71 | "vue-resource": "^1.0.3", 72 | "vue-router": "^2.0.3", 73 | "vuex": "^2.0.0", 74 | "vuex-router-sync": "^4.1.2" 75 | }, 76 | "devDependencies": { 77 | "autoprefixer": "^6.7.2", 78 | "babel-core": "^6.22.1", 79 | "babel-eslint": "^7.1.1", 80 | "babel-loader": "^6.2.10", 81 | "babel-plugin-component": "^0.9.1", 82 | "babel-plugin-transform-runtime": "^6.22.0", 83 | "babel-preset-es2015": "^6.22.0", 84 | "babel-preset-stage-2": "^6.22.0", 85 | "babel-register": "^6.22.0", 86 | "connect-history-api-fallback": "^1.3.0", 87 | "cross-spawn": "^5.1.0", 88 | "css-loader": "^0.27.3", 89 | "eslint": "^3.14.1", 90 | "eslint-config-vue": "^2.0.1", 91 | "eslint-friendly-formatter": "^2.0.7", 92 | "eslint-loader": "^1.6.1", 93 | "eslint-plugin-html": "^2.0.1", 94 | "eslint-plugin-vue": "^2.0.1", 95 | "eslint-plugin-promise": "^3.4.0", 96 | "eventsource-polyfill": "^0.9.6", 97 | "express": "^4.14.1", 98 | "extract-text-webpack-plugin": "^2.0.0-rc.3", 99 | "file-loader": "^0.10.0", 100 | "friendly-errors-webpack-plugin": "^1.1.3", 101 | "glob": "^7.1.1", 102 | "html-webpack-plugin": "^2.28.0", 103 | "http-proxy-middleware": "^0.17.3", 104 | "inject-loader": "^2.0.1", 105 | "ora": "^1.1.0", 106 | "optimize-css-assets-webpack-plugin": "^1.3.0", 107 | "os-locale": "^2.0.0", 108 | "shelljs": "^0.7.7", 109 | "stylus": "^0.54.5", 110 | "stylus-loader": "^3.0.1", 111 | "url-loader": "^0.5.7", 112 | "vue-loader": "^11.0.0", 113 | "vue-style-loader": "^2.0.0", 114 | "vue-template-compiler": "^2.1.10", 115 | "webpack": "^2.2.1", 116 | "webpack-bundle-analyzer": "^2.2.1", 117 | "webpack-dev-middleware": "^1.10.0", 118 | "webpack-hot-middleware": "^2.16.1", 119 | "webpack-merge": "^4.1.0" 120 | }, 121 | "engines": { 122 | "node": "6.4.0", 123 | "npm": "3.10.6" 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /template/server/README.md: -------------------------------------------------------------------------------- 1 | # Backend server 2 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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, 'sha512').toString('base64') 140 | } 141 | } 142 | 143 | module.exports = mongoose.model('User', UserSchema) 144 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 // eslint-disable-line 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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/tasks/mock.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global') 2 | var path = require('path') 3 | var fs = require('fs') 4 | var remove = require('./remove') 5 | const YELLOW = '\x1b[33m' 6 | const BLUE = '\x1b[34m' 7 | const END = '\x1b[0m' 8 | console.log('mv client folder %s to root path %s', path.join(__dirname, '../client/'), path.join(__dirname, '..')) 9 | 10 | /* eslint-disable */ 11 | mv(path.join(__dirname, '../client/*'), path.join(__dirname, '../')) 12 | rm('-rf', path.join(__dirname, '../client')) 13 | rm('-rf', path.join(__dirname, '../server')) 14 | remove(__filename) 15 | 16 | // rewrite package.json 17 | console.log(BLUE + 'Replace package.json......' + END) 18 | var pkg = require('../package.json') 19 | delete pkg.scripts['remove:mock'] 20 | fs.writeFile(path.join(__dirname, '../package.json'), JSON.stringify(pkg, null, 2) + '\n', function (err) { 21 | if (err) { 22 | console.error(YELLOW + 'Error when replace package.json, you may remove `remove:mock` script by yourself.' + END) 23 | } 24 | }) 25 | 26 | console.log(YELLOW + 'Finished. Now you can run the following script.' + END) 27 | -------------------------------------------------------------------------------- /template/tasks/remove.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | require('shelljs/global') 3 | module.exports = function (filepath, cb) { 4 | /* eslint-disable */ 5 | fs.readdir(__dirname, function (err, files) { 6 | if (files.length > 2) { 7 | rm('-f', filepath) 8 | if (cb) { 9 | cb() 10 | } 11 | } else { 12 | rm('-rf', __dirname) 13 | if (cb) { 14 | cb() 15 | } 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /template/tasks/replaceI18n.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var osLocale = require('os-locale')() 3 | var glob = require('glob') 4 | var path = require('path') 5 | var fs = require('fs') 6 | var _ = require('lodash') 7 | var remove = require('./remove') 8 | 9 | const YELLOW = '\x1b[33m' 10 | const BLUE = '\x1b[34m' 11 | const END = '\x1b[0m' 12 | 13 | var clientPath 14 | if (fs.existsSync(path.join(__dirname, '../client'))) { 15 | clientPath = path.join(__dirname, '../client') 16 | } else { 17 | clientPath = path.join(__dirname, '../') 18 | } 19 | 20 | // :prop="$t()" 21 | var syntax1 = /:([a-z-]+=")\$t\('([a-zA-Z_\w\.]+)'\)(")/g 22 | 23 | // {{$t()}} 24 | var syntax2 = /\{\{\$t\('([a-zA-Z_\w\.]+)'\)\}\}/g 25 | 26 | // this.$t() 27 | var syntax3 = /this.\$t\('([a-zA-Z_\w\.]+)'\)/g 28 | 29 | // :prop="xxx$t('')xxx$t()" 30 | var syntax4 = /:([a-z-]+=".+)\$t\('([a-zA-Z_\w\.]+)'\)(.+)\$t\('([a-zA-Z_\.]+)'\)(")/g 31 | 32 | // Vue.t('xxx') 33 | var syntax5 = /Vue\.t\('([a-zA-Z_\w\.]+)'\)/g 34 | 35 | var _locale = 'zh-CN' 36 | // os locale not math browser language 37 | var langMap = { 38 | 'zh_CN': 'zh-CN', 39 | 'en_US': 'en' 40 | } 41 | osLocale.then(locale => { 42 | // set locale 43 | if (langMap[locale] !== _locale) { 44 | _locale = 'en' 45 | } 46 | var langConfig = require(path.join(clientPath, 'src/locale/', _locale)).default 47 | 48 | // match files 49 | console.log(BLUE + 'Replace src files......' + END) 50 | glob(clientPath + '/src/**/*.{js,vue}', function (er, files) { 51 | files.forEach(function (file) { 52 | fs.readFile(file, 'utf8', function (err, data) { 53 | if (err) { 54 | console.error(YELLOW + 'Error when read file: ' + file + ', you may check the file yourself to see if \'$t\' syntax is exist.' + END) 55 | } else { 56 | var replaced = data.replace(syntax1, function () { 57 | return arguments[1] + _.get(langConfig, arguments[2]) + arguments[3] 58 | }).replace(syntax2, function () { 59 | return _.get(langConfig, arguments[1]) 60 | }).replace(syntax3, function () { 61 | return '\'' + _.get(langConfig, arguments[1]) + '\'' 62 | }).replace(syntax4, function () { 63 | return ':' + arguments[1] + '\'' + _.get(langConfig, arguments[2]) + '\'' + 64 | arguments[3] + '\'' + _.get(langConfig, arguments[4]) + '\'' + arguments[5] 65 | }).replace(syntax5, function () { 66 | return '\'' + _.get(langConfig, arguments[1]) + '\'' 67 | }) 68 | fs.writeFile(file, replaced, function (err) { 69 | if (err) { 70 | console.error(YELLOW + 'Error when replace file: ' + file + ', you may replace this file by yourself.' + END) 71 | } 72 | }) 73 | } 74 | }) 75 | }) 76 | }) 77 | 78 | // rewrite package.json 79 | console.log(BLUE + 'Replace package.json......' + END) 80 | var pkg = require('../package.json') 81 | delete pkg.devDependencies['os-locale'] 82 | delete pkg.devDependencies['glob'] 83 | delete pkg.devDependencies['babel-register'] 84 | delete pkg.scripts['remove:i18n'] 85 | fs.writeFile(path.join(__dirname, '../package.json'), JSON.stringify(pkg, null, 2) + '\n', function (err) { 86 | if (err) { 87 | console.error(YELLOW + 'Error when replace package.json, you may remove \'babel-register\' \'glob\' \'os-locale\' dependencies and `remove:i18n` script by yourself.' + END) 88 | } 89 | }) 90 | 91 | // delete locale folder 92 | console.log(BLUE + 'Remove locale and task folder......' + END) 93 | require('shelljs/global') 94 | rm('-rf', path.join(clientPath, 'src/locale')) 95 | remove(__filename) 96 | 97 | console.log(YELLOW + 'Finished. Now you can run the following script.' + END) 98 | }) 99 | --------------------------------------------------------------------------------