├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .snyk ├── .stylelintrc ├── .travis.yml ├── LICENSE ├── README-ZH.md ├── README.md ├── cli ├── clean.js ├── compile.js ├── demo.sh └── mirror.js ├── config ├── _base.js ├── _development.js ├── _production.js ├── _test.js └── index.js ├── doc └── DESIGN.png ├── mock ├── README.md ├── db.json └── routes.json ├── nodemon.json ├── npm-shrinkwrap.json ├── package.json ├── src ├── application │ ├── README.md │ ├── bootstrap.js │ ├── components │ │ ├── navbar.vue │ │ ├── route.vue │ │ └── styles │ │ │ └── navbar.css │ ├── styles │ │ ├── app.css │ │ ├── helpers.css │ │ ├── reddot.css │ │ ├── reset.css │ │ ├── variables.json │ │ └── vue.css │ └── views │ │ └── root.vue ├── assets │ └── logo.svg ├── index.ejs ├── index.js ├── modules │ ├── about │ │ ├── create-routes.js │ │ ├── index.js │ │ ├── styles │ │ │ └── index.css │ │ └── views │ │ │ └── index.vue │ ├── config │ │ ├── create-routes.js │ │ ├── create-store.js │ │ ├── index.js │ │ └── views │ │ │ └── index.vue │ ├── core │ │ ├── create-store.js │ │ └── index.js │ ├── demo │ │ ├── components │ │ │ ├── avatar.vue │ │ │ ├── badge.vue │ │ │ ├── button.vue │ │ │ ├── form.vue │ │ │ ├── icon.vue │ │ │ ├── image.vue │ │ │ ├── link.vue │ │ │ ├── modal.vue │ │ │ ├── paginator.vue │ │ │ ├── picker.vue │ │ │ ├── progress.vue │ │ │ ├── range.vue │ │ │ ├── row.vue │ │ │ ├── scroller.vue │ │ │ ├── slider.vue │ │ │ ├── spinner.vue │ │ │ ├── swiper.vue │ │ │ ├── toast.vue │ │ │ └── uploader.vue │ │ ├── create-routes.js │ │ ├── index.js │ │ ├── styles │ │ │ ├── images │ │ │ │ ├── wx@1x.png │ │ │ │ ├── wx@2x.png │ │ │ │ └── wx@3x.png │ │ │ └── index.css │ │ └── views │ │ │ └── index.vue │ ├── faq │ │ ├── create-routes.js │ │ ├── create-store.js │ │ ├── index.js │ │ ├── styles │ │ │ ├── create.css │ │ │ └── index.css │ │ └── views │ │ │ ├── create.vue │ │ │ └── index.vue │ ├── i18n │ │ ├── README.md │ │ ├── create-store.js │ │ └── index.js │ ├── logger │ │ └── index.js │ ├── persist │ │ └── index.js │ ├── request │ │ └── index.js │ ├── user │ │ ├── create-routes.js │ │ ├── index.js │ │ ├── styles │ │ │ └── logout.css │ │ └── views │ │ │ ├── login.vue │ │ │ └── logout.vue │ └── validator │ │ └── index.js ├── polyfills │ ├── README.md │ ├── _global.js │ ├── index.js │ ├── promise.js │ └── touchable.js └── static │ ├── CNAME │ ├── README.md │ ├── db │ └── faq.json │ ├── favicon.png │ ├── i18n │ ├── ar.json │ ├── en.json │ └── zh.json │ ├── images │ ├── logo.png │ ├── qr@1x.png │ ├── qr@2x.png │ └── qr@3x.png │ └── scripts │ ├── lib.flexible.js │ └── lib.viewport.js ├── test ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ ├── reports │ │ └── CHROME_51.0.2704.84_WIN8_test.xml │ ├── runner.js │ └── specs │ │ └── test.js └── unit │ ├── .eslintrc │ ├── index.js │ ├── karma.conf.js │ ├── runner.js │ ├── specs │ └── modules │ │ └── config.spec.js │ └── utils.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | "es2015", 6 | "stage-0" 7 | ], 8 | "plugins": [ 9 | "add-module-exports", 10 | "syntax-async-functions", 11 | "transform-regenerator", 12 | "dynamic-import-webpack" 13 | ], 14 | "comments": false 15 | }, 16 | "test": { 17 | "presets": [ 18 | "es2015", 19 | "stage-0" 20 | ], 21 | "plugins": [ 22 | "add-module-exports", 23 | "syntax-async-functions", 24 | "transform-regenerator", 25 | "dynamic-import-webpack", 26 | "istanbul" 27 | ], 28 | "comments": false 29 | }, 30 | "production": { 31 | "presets": [ 32 | "es2015", 33 | "stage-0" 34 | ], 35 | "plugins": [ 36 | "add-module-exports", 37 | "syntax-async-functions", 38 | "transform-regenerator", 39 | "dynamic-import-webpack" 40 | ], 41 | "comments": false 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | src/static/scripts/lib.flexible.js 5 | src/static/scripts/lib.viewport.js 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "plato" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.bak 3 | *.log 4 | *.out 5 | 6 | node_modules 7 | 8 | coverage 9 | data 10 | dist 11 | test/e2e/reports 12 | 13 | # IntelliJ project files 14 | .idea 15 | *.iml 16 | out 17 | gen 18 | 19 | stats.json 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org 2 | chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver 3 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.13.5 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-450202: 7 | - platojs > lodash: 8 | patched: '2019-07-04T22:44:53.913Z' 9 | SNYK-JS-HTTPSPROXYAGENT-469131: 10 | - snyk > proxy-agent > https-proxy-agent: 11 | patched: '2019-10-10T22:44:41.325Z' 12 | - snyk > proxy-agent > pac-proxy-agent > https-proxy-agent: 13 | patched: '2019-10-10T22:44:41.325Z' 14 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "unit-case": null 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | addons: 4 | apt: 5 | sources: 6 | - google-chrome 7 | packages: 8 | - google-chrome-stable 9 | 10 | language: node_js 11 | 12 | node_js: 13 | - "7" 14 | 15 | before_script: 16 | - export CHROME_BIN=chromium-browser 17 | - export DISPLAY=:99.0 18 | - sh -e /etc/init.d/xvfb start 19 | - sleep 3 20 | 21 | before_install: 22 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" 23 | 24 | install: 25 | - npm run init 26 | 27 | after_success: 28 | - npm i coveralls 29 | - cat ./coverage/*/lcov.info | coveralls 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present, crossjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 |

2 | PLATO
3 | 基于 Vue 2.x 4 |

5 |

6 | Travis 7 | Coveralls 8 | dependencies Status 9 | devDependencies Status 10 |

11 |

12 | a Boilerplate for [mobile] SPAs using vue, vuex, vue-router
13 | Check out 文档, 示例 and UI 组件 14 |

15 |

16 | Framework Design 17 |

18 | 19 | ## 设计原则 20 | 21 | [Less is More](https://zh.wikipedia.org/wiki/極簡主義) 22 | 23 | [若无必要,勿增实体](https://zh.wikipedia.org/wiki/奥卡姆剃刀) 24 | 25 | ## 脚手架 26 | 27 | **注意:此仓库是实践示例,请不要直接克隆后用于项目开发,应使用如下方式初始化项目** 28 | 29 | - [Vue CLI Template](https://github.com/platojs/template) 30 | 31 | ## 版本锁 32 | 33 | **此项目尚处于 Beta 阶段,实际应用时请确保使用 `npm-shrinkwrap.json`** 34 | 35 | **`npm run mirror:` 可切换依赖项的源(registry)至 SDP、淘宝或 NPM** 36 | 37 | ## 特性 38 | 39 | - [Core](https://github.com/platojs/platojs) 40 | - [system](https://github.com/platojs/system) 41 | - [util](https://github.com/platojs/util) 42 | - [components](https://github.com/platojs/components) 43 | - [directives](https://github.com/platojs/directives) 44 | - [plugins](https://github.com/platojs/plugins) 45 | - Vue 46 | - [Vue](https://github.com/vuejs/vue) 47 | - [Vue-Router](https://github.com/vuejs/vue-router) 48 | - [Vuex](https://github.com/vuejs/vuex) 49 | - [Vuex-Actions](https://github.com/weinot/vuex-actions) (for async actions) 50 | - [Vuex-LocalStorage](https://github.com/crossjs/vuex-localstorage) (for cache and persistence) 51 | - Build 52 | - [Webpack](http://webpack.github.io/) 53 | - Linters 54 | - [ESLint](http://eslint.org/) 55 | - [stylelint](http://stylelint.io/) 56 | - Tests 57 | - [Karma](https://karma-runner.github.io/) 58 | - [Mocha](https://mochajs.org/) 59 | - [Nightwatch](http://nightwatchjs.org/) 60 | - [Selenium-Server](https://github.com/eugeneware/selenium-server) 61 | - Transformers 62 | - [PostCSS](http://postcss.org/) (for css next) 63 | - [postcss-rtl](https://github.com/vkalinichev/postcss-rtl) 64 | - [postcss-flexible](https://github.com/crossjs/postcss-flexible) (for [lib.flexible](https://github.com/amfe/lib-flexible)) 65 | - ... 66 | - [Babel](https://babeljs.io/) (for es6) 67 | - Worth Reading Modules 68 | - [Logger](src/modules/logger) 69 | - [Persist](src/modules/persist) 70 | - [I18n](src/modules/i18n) 71 | - [Validator](src/modules/validator) 72 | - [Request](src/modules/request) 73 | 74 | ## 使用方法 75 | 76 | ```bash 77 | # 切换 NPM 源 78 | npm run mirror: 79 | 80 | # 安装依赖项。通过指定镜像提高安装速度 81 | npm run init 82 | 83 | # 锁定依赖项版本。使用 npm shrinkwrap 84 | npm run lock 85 | 86 | # 启动开发服务。访问地址:localhost:3000 87 | npm run dev 88 | 89 | # eslint, stylelint, unit and e2e test 90 | npm test 91 | 92 | # test, clean, and compile 93 | npm run build 94 | 95 | # serve dist, like production 96 | npm start 97 | 98 | # generate demo site and push to gh-pages 99 | npm run demo 100 | 101 | # push modifications to github 102 | npm run push 103 | ``` 104 | 105 | ## 兼容性 106 | 107 | - IE 9+ 108 | - Chrome 109 | - Safari 110 | - Firefox 111 | - ... 112 | - Android 4+ 113 | - iOS 7+ 114 | 115 | ## 版权 116 | 117 | [MIT](http://opensource.org/licenses/MIT) 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | PLATO
3 | Based on Vue 2.x 4 |

5 |

6 | Travis 7 | Coveralls 8 | dependencies Status 9 | devDependencies Status 10 |

11 |

12 | a Boilerplate for [mobile] SPAs using vue, vuex, vue-router
13 | Check out Documentation, Demonstrations and UI Components 14 |

15 | 16 | [中文说明](README-ZH.md) 17 | 18 | ## Principles 19 | 20 | [Less is More](https://en.wikipedia.org/wiki/Minimalism) 21 | 22 | [Entities must not be multiplied beyond necessity](https://en.wikipedia.org/wiki/Occam%27s_razor) 23 | 24 | ## Scaffolding 25 | 26 | [Vue CLI Template](https://github.com/platojs/template) 27 | 28 | ## Features 29 | 30 | - [Core](https://github.com/platojs/platojs) 31 | - [system](https://github.com/platojs/system) 32 | - [util](https://github.com/platojs/util) 33 | - [components](https://github.com/platojs/components) 34 | - [directives](https://github.com/platojs/directives) 35 | - [plugins](https://github.com/platojs/plugins) 36 | - Vue 37 | - [Vue](https://github.com/vuejs/vue) 38 | - [Vue-Router](https://github.com/vuejs/vue-router) 39 | - [Vuex](https://github.com/vuejs/vuex) 40 | - [Vuex-Actions](https://github.com/weinot/vuex-actions) (for async actions) 41 | - [Vuex-LocalStorage](https://github.com/crossjs/vuex-localstorage) (for cache and persistence) 42 | - Build 43 | - [Webpack](http://webpack.github.io/) 44 | - Linters 45 | - [ESLint](http://eslint.org/) 46 | - [stylelint](http://stylelint.io/) 47 | - Tests 48 | - [Karma](https://karma-runner.github.io/) 49 | - [Mocha](https://mochajs.org/) 50 | - [Nightwatch](http://nightwatchjs.org/) 51 | - [Selenium-Server](https://github.com/eugeneware/selenium-server) 52 | - Transformers 53 | - [PostCSS](http://postcss.org/) (for css next) 54 | - [postcss-rtl](https://github.com/vkalinichev/postcss-rtl) 55 | - [postcss-flexible](https://github.com/crossjs/postcss-flexible) (for [lib.flexible](https://github.com/amfe/lib-flexible)) 56 | - ... 57 | - [Babel](https://babeljs.io/) (for es6) 58 | - Worth Reading Modules 59 | - [Logger](src/modules/logger) 60 | - [Persist](src/modules/persist) 61 | - [I18n](src/modules/i18n) 62 | - [Validator](src/modules/validator) 63 | - [Request](src/modules/request) 64 | 65 | ## Usage 66 | 67 | ```bash 68 | # switch npm registry mirror 69 | npm run mirror: 70 | 71 | # install dependencies with mirrors 72 | npm run init 73 | 74 | # lock dependencies version 75 | npm run lock 76 | 77 | # serve with hot reload at localhost:3000 78 | npm run dev 79 | 80 | # eslint, stylelint, unit and e2e test 81 | npm test 82 | 83 | # test, clean, and compile 84 | npm run build 85 | 86 | # serve dist, like production 87 | npm start 88 | 89 | # generate demo site and push to gh-pages 90 | npm run demo 91 | 92 | # push modifications to github 93 | npm run push 94 | ``` 95 | 96 | ## Browser Support 97 | 98 | - IE 9+ 99 | - Chrome 100 | - Safari 101 | - Firefox 102 | - ... 103 | - Android 4+ 104 | - iOS 7+ 105 | 106 | ## License 107 | 108 | [MIT](http://opensource.org/licenses/MIT) 109 | -------------------------------------------------------------------------------- /cli/clean.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | 3 | console.log('Clean dist files...') 4 | 5 | require('rimraf')(require('../config').paths.dist('**'), err => { 6 | if (err) { 7 | console.log(err) 8 | } else { 9 | console.log('Files cleaned.') 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /cli/compile.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | 3 | const debug = require('debug')('PLATO:compile') 4 | 5 | debug('Create webpack compiler.') 6 | 7 | require('webpack')(require('../webpack.config.babel.js')).run((err, stats) => { 8 | const jsonStats = stats.toJson() 9 | 10 | debug('Webpack compile completed.') 11 | console.log(stats.toString({ 12 | modules: false, 13 | children: false, 14 | chunks: false, 15 | chunkModules: false, 16 | colors: true 17 | })) 18 | 19 | if (err) { 20 | debug('Webpack compiler encountered a fatal error.', err) 21 | process.exit(1) 22 | } else if (jsonStats.errors.length > 0) { 23 | debug('Webpack compiler encountered errors.') 24 | console.log(jsonStats.errors) 25 | process.exit(1) 26 | } else if (jsonStats.warnings.length > 0) { 27 | debug('Webpack compiler encountered warnings.') 28 | } else { 29 | debug('No errors or warnings encountered.') 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /cli/demo.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | echo "Enter message: " 3 | read MESSAGE 4 | 5 | echo "Deploying $MESSAGE ..." 6 | 7 | # build 8 | npm run build 9 | 10 | # commit 11 | cd dist 12 | git init 13 | git add -A 14 | git commit -m "$MESSAGE" 15 | git push -f https://github.com/crossjs/plato.git master:gh-pages 16 | 17 | # back to root 18 | cd .. 19 | -------------------------------------------------------------------------------- /cli/mirror.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const resolve = require('path').resolve 3 | 4 | const registry = process.argv[2] 5 | const re = /https?:\/\/registry\.[a-z.]+/g 6 | 7 | function replaceLock (cb) { 8 | const lock = resolve(__dirname, '../npm-shrinkwrap.json') 9 | fs.readFile(lock, (err, buf) => { 10 | if (err) { 11 | console.error(err) 12 | } else { 13 | let content = buf.toString() 14 | content = content.replace(re, registry) 15 | fs.writeFile(lock, content, err => { 16 | if (err) { 17 | console.error(err) 18 | } else { 19 | console.log(`registries in "npm-shrinkwrap.json" have been replaced with "${registry}"`) 20 | cb() 21 | } 22 | }) 23 | } 24 | }) 25 | } 26 | 27 | function replaceRc () { 28 | const rc = resolve(__dirname, '../.npmrc') 29 | fs.readFile(rc, (err, buf) => { 30 | if (err) { 31 | console.error(err) 32 | } else { 33 | let content = buf.toString() 34 | content = content.replace(re, registry) 35 | fs.writeFile(rc, content, err => { 36 | if (err) { 37 | console.error(err) 38 | } else { 39 | console.log(`registries in ".npmrc" have been replaced with "${registry}"`) 40 | } 41 | }) 42 | } 43 | }) 44 | } 45 | 46 | if (registry) { 47 | replaceLock(replaceRc) 48 | } else { 49 | console.error('registry is required') 50 | } 51 | -------------------------------------------------------------------------------- /config/_base.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | 3 | const NODE_ENV = process.env.NODE_ENV || 'development' 4 | 5 | const config = { 6 | env: NODE_ENV, 7 | 8 | // ---------------------------------- 9 | // Project Structure 10 | // ---------------------------------- 11 | path_base: resolve(__dirname, '../'), 12 | dir_src: 'src', 13 | dir_dist: 'dist', 14 | dir_test: 'test', 15 | 16 | // ---------------------------------- 17 | // Server Configuration 18 | // ---------------------------------- 19 | server_host: '0.0.0.0', // binds to all hosts 20 | server_port: process.env.PORT || 3000, 21 | 22 | // ---------------------------------- 23 | // Compiler Configuration 24 | // ---------------------------------- 25 | compiler_devtool: 'source-map', 26 | compiler_hash_type: 'hash', 27 | compiler_html_minify: false, 28 | compiler_public_path: '', 29 | 30 | // ------------------------------------ 31 | // Environment 32 | // ------------------------------------ 33 | globals: { 34 | 'process.env.NODE_ENV': JSON.stringify(NODE_ENV), 35 | __DEV__: NODE_ENV === 'development', 36 | __PROD__: NODE_ENV === 'production', 37 | __TEST__: NODE_ENV === 'test' 38 | } 39 | } 40 | 41 | // ------------------------------------ 42 | // Utilities 43 | // ------------------------------------ 44 | config.paths = (() => { 45 | const base = (...args) => 46 | resolve.apply(resolve, [config.path_base, ...args]) 47 | 48 | return { 49 | base, 50 | src: base.bind(null, config.dir_src), 51 | dist: base.bind(null, config.dir_dist), 52 | test: base.bind(null, config.dir_test) 53 | } 54 | })() 55 | 56 | export default config 57 | -------------------------------------------------------------------------------- /config/_development.js: -------------------------------------------------------------------------------- 1 | export default config => ({ 2 | compiler_devtool: 'cheap-module-source-map' 3 | }) 4 | -------------------------------------------------------------------------------- /config/_production.js: -------------------------------------------------------------------------------- 1 | export default config => ({ 2 | compiler_hash_type: 'chunkhash', 3 | compiler_html_minify: true 4 | }) 5 | -------------------------------------------------------------------------------- /config/_test.js: -------------------------------------------------------------------------------- 1 | import { argv } from 'yargs' 2 | 3 | const coverage_enabled = !argv.watch 4 | 5 | const coverage_reporters = [ 6 | { type: 'lcov' } 7 | ] 8 | 9 | if (coverage_enabled) { 10 | coverage_reporters.push( 11 | { type: 'json-summary', file: 'lcov.json' } 12 | ) 13 | } else { 14 | coverage_reporters.push( 15 | { type: 'text-summary' } 16 | ) 17 | } 18 | 19 | export default config => ({ 20 | compiler_devtool: 'inline-source-map', 21 | coverage_enabled, 22 | coverage_reporters 23 | }) 24 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import _debug from 'debug' 3 | import config, { env } from './_base' 4 | 5 | const debug = _debug('PLATO:config') 6 | debug('Create configuration.') 7 | debug(`Apply environment overrides for NODE_ENV "${env}".`) 8 | 9 | // Check if the file exists before attempting to require it, this 10 | // way we can provide better error reporting that overrides 11 | // weren't applied simply because the file didn't exist. 12 | const overridesFilename = `_${env}` 13 | let hasOverridesFile 14 | try { 15 | fs.lstatSync(`${__dirname}/${overridesFilename}.js`) 16 | hasOverridesFile = true 17 | } catch (e) { 18 | // debug(e) 19 | } 20 | 21 | // Overrides file exists, so we can attempt to require it. 22 | // We intentionally don't wrap this in a try/catch as we want 23 | // the Node process to exit if an error occurs. 24 | let overrides = {} 25 | if (hasOverridesFile) { 26 | overrides = require(`./${overridesFilename}`)(config) 27 | } else { 28 | debug(`No configuration overrides found for NODE_ENV "${env}"`) 29 | } 30 | 31 | export default { ...config, ...overrides } 32 | -------------------------------------------------------------------------------- /doc/DESIGN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/doc/DESIGN.png -------------------------------------------------------------------------------- /mock/README.md: -------------------------------------------------------------------------------- 1 | files for json-server 2 | -------------------------------------------------------------------------------- /mock/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "faq": [ 3 | { 4 | "id": "hrpkt5v3grg", 5 | "title": "from 🇨🇳", 6 | "content": "for the world" 7 | }, 8 | { 9 | "id": "n45om0s6b78", 10 | "title": "could be used in production?", 11 | "content": "though still working in progress" 12 | }, 13 | { 14 | "id": "rc84eok8ul", 15 | "title": "得到", 16 | "content": "点点滴滴" 17 | }, 18 | { 19 | "id": "00psciss0lo", 20 | "title": "saasfas", 21 | "content": "asddasds" 22 | }, 23 | { 24 | "id": "ar17bbqbthg", 25 | "title": "poop", 26 | "content": "monkeys" 27 | }, 28 | { 29 | "id": "bf5705249k", 30 | "title": "sfdsfsfdfs", 31 | "content": "fsdafsdfsdf" 32 | }, 33 | { 34 | "id": "sjvk7gv5jc8", 35 | "title": "wwww", 36 | "content": "wwww" 37 | }, 38 | { 39 | "id": "0s8ri08ogf8", 40 | "title": "Vue 2.0 is Here!", 41 | "content": "https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.zi0jbpmom" 42 | }, 43 | { 44 | "id": "hr3g287ns1", 45 | "title": "dfgf", 46 | "content": "fddgd" 47 | }, 48 | { 49 | "id": "f8dcvqeukq", 50 | "title": "测试一下 just a test", 51 | "content": "看上去不错哦" 52 | }, 53 | { 54 | "id": "it3fp87k708", 55 | "title": "jjj", 56 | "content": "jjjj" 57 | }, 58 | { 59 | "id": "q99hus0bk", 60 | "title": "使用了自定义的 tap 事件", 61 | "content": "解决 300 毫秒延迟,还在进行更多的测试……" 62 | }, 63 | { 64 | "id": "9t6a3odc0io", 65 | "title": "4344", 66 | "content": "444" 67 | }, 68 | { 69 | "id": "fv6dddg64m8", 70 | "title": "ttt", 71 | "content": "tttt" 72 | }, 73 | { 74 | "id": "hn5qqh7adv", 75 | "title": "1212", 76 | "content": "21242" 77 | }, 78 | { 79 | "id": "jkgcu8rt28", 80 | "title": "d", 81 | "content": "dd" 82 | }, 83 | { 84 | "id": "2n730tvn9fg", 85 | "title": "13213", 86 | "content": "12313" 87 | }, 88 | { 89 | "id": "t5qr27tf30o", 90 | "title": "sss", 91 | "content": "ddd" 92 | }, 93 | { 94 | "id": "tp06rfscto", 95 | "title": "123", 96 | "content": "456" 97 | }, 98 | { 99 | "id": "c97duh41e9", 100 | "title": "fffff", 101 | "content": "ffff" 102 | }, 103 | { 104 | "id": "316lmeshcj8", 105 | "title": "123123", 106 | "content": "123123123" 107 | }, 108 | { 109 | "id": "rfd2nhvnnbg", 110 | "title": "sdfsdfasdfas", 111 | "content": "dfsdfasd" 112 | }, 113 | { 114 | "id": "rlc6teb7f8", 115 | "title": "test for normalizer", 116 | "content": "test for normalizer" 117 | }, 118 | { 119 | "title": "fsdfsdf", 120 | "content": "dsfdsf", 121 | "id": "SJEA96oHg" 122 | }, 123 | { 124 | "title": "eqweqwe", 125 | "content": "qwewqewqe", 126 | "id": "ry1Gopjrl" 127 | }, 128 | { 129 | "title": "werewr", 130 | "content": "werwerewr", 131 | "id": "ry4UjajBl" 132 | }, 133 | { 134 | "title": "qweqwe", 135 | "content": "wqeqwe", 136 | "id": "SkrU06jrx" 137 | }, 138 | { 139 | "title": "11111111", 140 | "content": "111111111111111111111111111111111111111111111111", 141 | "id": "ryKjmRiHl" 142 | } 143 | ] 144 | } 145 | -------------------------------------------------------------------------------- /mock/routes.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "execMap": { 4 | "js": "node --harmony" 5 | }, 6 | "ignore": [ 7 | "coverage", 8 | "dist", 9 | "node_modules", 10 | "src", 11 | "test/unit", 12 | "package.json" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PLATO", 3 | "version": "3.0.0-beta.16", 4 | "description": "a Boilerplate for [mobile] SPAs use vue, vuex, vue-router", 5 | "license": "MIT", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "mirror:sdp": "node ./cli/mirror http://registry.npm.sdp.nd", 9 | "mirror:tb": "node ./cli/mirror https://registry.npm.taobao.org", 10 | "mirror:npm": "node ./cli/mirror https://registry.npmjs.org", 11 | "init": "npm install", 12 | "lock": "npm shrinkwrap", 13 | "unlock": "rm -rf npm-shrinkwrap.json", 14 | "start": "cross-env SCRIPT=start npm run build && serve ./dist", 15 | "mock": "cross-env NODE_ENV=development DEBUG=PLATO:* json-server ./mock/db.json -w -p 3001 -r ./mock/routes.json", 16 | "wds": "cross-env NODE_ENV=development DEBUG=PLATO:* webpack-dev-server --progress", 17 | "dev": "concurrently -r -k \"npm run mock\" \"npm run wds\"", 18 | "e2e": "cross-env NODE_ENV=test DEBUG=PLATO:* node ./test/e2e/runner.js", 19 | "e2e:dev": "cross-env NODE_ENV=test DEBUG=PLATO:* nodemon ./test/e2e/runner.js", 20 | "unit": "cross-env NODE_ENV=test DEBUG=PLATO:* karma start ./test/unit/runner.js", 21 | "unit:dev": "npm run unit -- --watch", 22 | "lint": "eslint --max-warnings 10 .", 23 | "lint:fix": "npm run lint -- --fix", 24 | "lint:css": "stylelint src/**/*.css", 25 | "test": "npm run lint && npm run lint:css && npm run unit && npm run e2e", 26 | "clean": "node ./cli/clean", 27 | "compile": "cross-env NODE_ENV=production DEBUG=PLATO:* node ./cli/compile", 28 | "build": "npm run test && npm run clean && npm run compile", 29 | "demo": "cross-env SCRIPT=demo bash ./cli/demo.sh", 30 | "snyk-protect": "snyk protect", 31 | "prepublish": "npm run snyk-protect" 32 | }, 33 | "dependencies": { 34 | "core-js": "^2.4.1", 35 | "nuo": "^1.0.0", 36 | "platojs": "latest", 37 | "string-template": "^1.0.0", 38 | "vuex-localstorage": "^1.0.0", 39 | "whatwg-fetch": "^2.0.3", 40 | "snyk": "^1.234.0" 41 | }, 42 | "devDependencies": { 43 | "plato-dev-dependencies": "latest" 44 | }, 45 | "engines": { 46 | "node": ">=7.7.4", 47 | "npm": ">=4.1.2" 48 | }, 49 | "snyk": true 50 | } 51 | -------------------------------------------------------------------------------- /src/application/README.md: -------------------------------------------------------------------------------- 1 | ## views 与 components 的区别 2 | 3 | ### views 下的 .vue 文件一般与路由对应,同时也有 Smart Components 的特征 4 | 5 | 如下路由配置文件中的 component 指向 views 目录下的 .vue 文件 6 | 7 | ```js 8 | return [ 9 | { 10 | path: '/', 11 | exact: true, 12 | component: () => import('./views/index') 13 | }, 14 | { 15 | path: '/create', 16 | meta: { 17 | auth: true, 18 | icon: 'plus' 19 | }, 20 | component: () => import('./views/create') 21 | } 22 | ] 23 | ``` 24 | 25 | ### components 下的 .vue 文件,则可认为是 Dumb Components 26 | 27 | ### 附录 28 | 29 | [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.clxt1j7m1) 30 | -------------------------------------------------------------------------------- /src/application/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { configure, use, run } from 'platojs/system' 2 | 3 | import logger from 'modules/logger' 4 | import persist from 'modules/persist' 5 | import request from 'modules/request' 6 | import i18n from 'modules/i18n' 7 | import validator from 'modules/validator' 8 | import config from 'modules/config' 9 | import faq from 'modules/faq' 10 | import demo from 'modules/demo' 11 | import about from 'modules/about' 12 | import user from 'modules/user' 13 | import core from 'modules/core' 14 | 15 | import Root from './views/root' 16 | import translations from 'static/i18n/zh.json' 17 | 18 | /** 19 | * 全局配置 20 | */ 21 | configure({ 22 | // 项目名称 23 | name: 'PLATO', 24 | // 项目版本号 25 | version: '1.0', 26 | // 系统自动将 component 挂载到 element 27 | element: '#app', 28 | component: Root 29 | }) 30 | 31 | /** 32 | * Use Modules 33 | */ 34 | 35 | /** 36 | * 调试相关 37 | */ 38 | __DEV__ && use(logger) 39 | 40 | /** 41 | * 被依赖的模块 42 | * 移除可能会影响部分功能 43 | */ 44 | use(request) 45 | use(i18n, { translations }) 46 | use(validator) 47 | 48 | /** 49 | * 普通模块 50 | */ 51 | use(config) 52 | use(user, { prefix: '/' }) 53 | use(faq, { prefix: '/' }) 54 | use(demo) 55 | use(about) 56 | 57 | /** 58 | * 核心模块 59 | * 路由与鉴权,以及持久化 60 | */ 61 | use(core) 62 | use(persist) 63 | 64 | /** 65 | * Run Modules 66 | */ 67 | 68 | run(({ router }) => { 69 | __PROD__ || console.log('%c[PLATO] %cLet\'s go!', 70 | 'font-weight: bold', 'color: green; font-weight: bold') 71 | 72 | /** 73 | * Let's go! 74 | */ 75 | router.afterEach(() => { 76 | // 解决 iOS 焦点 BUG 77 | const activeElement = document.activeElement 78 | if (activeElement && activeElement.nodeName !== 'BODY') { 79 | activeElement.blur() 80 | } 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/application/components/navbar.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/application/components/route.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 41 | -------------------------------------------------------------------------------- /src/application/components/styles/navbar.css: -------------------------------------------------------------------------------- 1 | .c-navbar { 2 | position: absolute; 3 | z-index: 11; 4 | width: 100%; 5 | } 6 | 7 | .c-navbar-toggle { 8 | position: absolute; 9 | z-index: 102; 10 | right: 0; 11 | width: 88px; 12 | font-size: 32px; 13 | line-height: 88px; 14 | text-align: center; 15 | background: transparent; 16 | border: none; 17 | border-radius: 0; 18 | 19 | &:active { 20 | background-color: transparent; 21 | } 22 | 23 | &.active { 24 | color: var(--primary); 25 | } 26 | } 27 | 28 | .c-navbar-menu { 29 | position: fixed; 30 | z-index: 101; 31 | top: 0; 32 | right: 0; 33 | width: 88px; 34 | padding-top: 88px; 35 | height: 100%; 36 | background-color: white; 37 | box-shadow: 0 1PX 3PX color(white lightness(-20%)); 38 | transform: translate3d(88px, 0, 0); 39 | transition: transform 0.3s ease-in-out; 40 | 41 | &.opened { 42 | transform: translate3d(0, 0, 0); 43 | } 44 | 45 | & li { 46 | & ul { 47 | display: none; 48 | } 49 | 50 | & li { 51 | display: block; 52 | 53 | & a { 54 | display: block; 55 | } 56 | } 57 | 58 | &:active { 59 | & ul { 60 | display: block; 61 | background-color: color(white lightness(-10%)); 62 | } 63 | } 64 | } 65 | 66 | & a { 67 | display: block; 68 | line-height: 88px; 69 | text-decoration: none; 70 | text-align: center; 71 | 72 | &:active { 73 | background-color: color(white lightness(-5%)); 74 | } 75 | 76 | &.router-link-active { 77 | color: var(--secondary); 78 | background-color: color(var(--secondary) lightness(95%)); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/application/styles/app.css: -------------------------------------------------------------------------------- 1 | @import "reset"; 2 | @import "vue"; 3 | @import "helpers"; 4 | 5 | #container { 6 | padding: 88px 0 0; 7 | box-sizing: border-box; 8 | height: 100%; 9 | } 10 | 11 | #progress { 12 | position: fixed; 13 | z-index: 100001; 14 | top: 0; 15 | left: 0; 16 | } 17 | 18 | #header { 19 | position: absolute; 20 | z-index: 11; 21 | top: 0; 22 | left: 0; 23 | width: 100%; 24 | height: 88px; 25 | background-color: color(white blackness(+5%)); 26 | } 27 | 28 | #history { 29 | & .c-link { 30 | position: absolute; 31 | z-index: 1; 32 | width: 88px; 33 | height: 88px; 34 | line-height: 88px; 35 | text-align: center; 36 | 37 | &:active { 38 | background-color: transparent; 39 | } 40 | } 41 | } 42 | 43 | #logo { 44 | position: absolute; 45 | z-index: 1; 46 | left: 132px; 47 | right: 132px; 48 | 49 | & a { 50 | display: block; 51 | height: 88px; 52 | color: var(--text); 53 | font-size: 32px; 54 | line-height: 88px; 55 | text-align: center; 56 | text-decoration: none; 57 | text-transform: uppercase; 58 | font-weight: bold; 59 | } 60 | 61 | & sub { 62 | font-size: 12px; 63 | } 64 | } 65 | 66 | #navbar { 67 | & .c-reddot { 68 | line-height: 1.5; 69 | } 70 | } 71 | 72 | #content { 73 | height: 100%; 74 | overflow-y: scroll; 75 | -webkit-overflow-scrolling: touch; 76 | } 77 | -------------------------------------------------------------------------------- /src/application/styles/helpers.css: -------------------------------------------------------------------------------- 1 | /* paddings */ 2 | 3 | .padding { 4 | padding: 20px; 5 | } 6 | 7 | /* font-sizes */ 8 | 9 | .fs-64 { 10 | font-size: 64px; 11 | } 12 | 13 | .fs-48 { 14 | font-size: 48px; 15 | } 16 | 17 | .fs-32 { 18 | font-size: 32px; 19 | } 20 | 21 | .fs-24 { 22 | font-size: 24px; 23 | } 24 | 25 | .fs-16 { 26 | font-size: 16px; 27 | } 28 | 29 | /* alignments */ 30 | 31 | .left { 32 | text-align: left; 33 | } 34 | 35 | .center { 36 | text-align: center; 37 | align-self: center; 38 | } 39 | 40 | .right { 41 | text-align: right; 42 | } 43 | 44 | .rotate90 { 45 | transform: rotate3d(0, 0, 1, 90deg); 46 | } 47 | 48 | .rotate90n { 49 | transform: rotate3d(0, 0, 1, -90deg); 50 | } 51 | -------------------------------------------------------------------------------- /src/application/styles/reddot.css: -------------------------------------------------------------------------------- 1 | .c-reddot { 2 | position: relative; 3 | 4 | &::after { 5 | content: ' '; 6 | position: absolute; 7 | right: -6px; 8 | top: -6px; 9 | width: 12px; 10 | height: 12px; 11 | background-color: var(--secondary); 12 | border-radius: 50%; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/application/styles/reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: 'Helvetica Neue', Helvetica, STHeiTi, Arial, sans-serif; 3 | line-height: 1.5; 4 | -webkit-text-size-adjust: 100%; 5 | -webkit-tap-highlight-color: transparent; 6 | height: 100%; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | color: var(--text); 12 | background-color: white; 13 | height: 100%; 14 | font-size: 32px; 15 | } 16 | 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6, 23 | p, 24 | dl, 25 | dd, 26 | form, 27 | ul, 28 | ol, 29 | pre, 30 | blockquote, 31 | textarea, 32 | input, 33 | figure, 34 | button { 35 | margin: 0; 36 | font-size: 32px; 37 | } 38 | 39 | ul, 40 | ol, 41 | th, 42 | td, 43 | button, 44 | textarea, 45 | input { 46 | padding: 0; 47 | } 48 | 49 | input, 50 | textarea, 51 | keygen, 52 | select, 53 | button { 54 | font-family: Helvetica Neue, Helvetica, STHeiTi, Arial, sans-serif; 55 | font-size: 32px; 56 | line-height: 1.5; 57 | color: var(--text); 58 | } 59 | 60 | button, 61 | input, 62 | textarea { 63 | outline: none; 64 | -webkit-tap-highlight-color: transparent; 65 | } 66 | 67 | button { 68 | background: none; 69 | border: none; 70 | cursor: pointer; 71 | } 72 | 73 | input, 74 | textarea { 75 | &::placeholder { 76 | color: var(--placeholder); 77 | } 78 | } 79 | 80 | ul, 81 | ol { 82 | list-style: none; 83 | } 84 | 85 | code, 86 | kbd, 87 | pre, 88 | samp { 89 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 90 | } 91 | 92 | pre { 93 | & code { 94 | padding: 0; 95 | font-size: inherit; 96 | color: inherit; 97 | white-space: pre-wrap; 98 | background-color: transparent; 99 | border-radius: 0; 100 | } 101 | } 102 | 103 | i, 104 | em { 105 | font-style: normal; 106 | } 107 | 108 | a { 109 | color: var(--primary); 110 | text-decoration: none; 111 | 112 | &:hover { 113 | text-decoration: none; 114 | } 115 | 116 | &:active { 117 | color: var(--secondary); 118 | } 119 | } 120 | 121 | article, 122 | footer, 123 | header, 124 | nav, 125 | section { 126 | display: block; 127 | } 128 | -------------------------------------------------------------------------------- /src/application/styles/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "text": "#333", 3 | "disabled": "#CCC", 4 | "primary": "#38ADFF", 5 | "secondary": "#F43531", 6 | "warning": "#FF6F6F", 7 | "placeholder": "#CCC" 8 | } 9 | -------------------------------------------------------------------------------- /src/application/styles/vue.css: -------------------------------------------------------------------------------- 1 | [v-cloak] { 2 | display: none; 3 | } 4 | 5 | .router-link-active { 6 | color: var(--primary); 7 | } 8 | 9 | .slide-enter-active, 10 | .slide-leave-active { 11 | transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); 12 | } 13 | 14 | .slide-enter, 15 | .slide-leave-active { 16 | opacity: 0; 17 | transform: translate3d(0, -1rem, 0); 18 | } 19 | 20 | .slide-up-enter-active, 21 | .slide-up-leave-active { 22 | transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); 23 | } 24 | 25 | .slide-up-enter, 26 | .slide-up-leave-active { 27 | opacity: 0; 28 | transform: translate3d(0, 1rem, 0); 29 | } 30 | 31 | .slide-left-enter-active, 32 | .slide-left-leave-active { 33 | transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1); 34 | } 35 | 36 | .slide-left-enter, 37 | .slide-left-leave-active { 38 | opacity: 0; 39 | transform: translate3d(1rem, 0, 0); 40 | } 41 | 42 | .fade-enter-active, 43 | .fade-leave-active { 44 | transition: opacity 0.5s cubic-bezier(0.55, 0, 0.1, 1); 45 | } 46 | 47 | .fade-enter, 48 | .fade-leave-active { 49 | opacity: 0; 50 | } 51 | -------------------------------------------------------------------------------- /src/application/views/root.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'application/bootstrap' 2 | 3 | /* 这里是入口,只需要引入 bootstrap 即可 */ 4 | -------------------------------------------------------------------------------- /src/modules/about/create-routes.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return [ 3 | { 4 | path: '/', 5 | meta: { 6 | icon: 'question' 7 | }, 8 | component: () => import('./views/index') 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/about/index.js: -------------------------------------------------------------------------------- 1 | import createRoutes from './create-routes' 2 | 3 | export default (context, options = {}) => { 4 | options = { scope: 'about', prefix: 'about', ...options } 5 | 6 | // only data 7 | return { 8 | routes: createRoutes(context, options), 9 | options 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/about/styles/index.css: -------------------------------------------------------------------------------- 1 | a { 2 | & img { 3 | height: 40px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/about/views/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/modules/config/create-routes.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return [ 3 | { 4 | path: '/', 5 | meta: { 6 | icon: 'globe' 7 | }, 8 | component: () => import('./views/index') 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/config/create-store.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const SET_PROGRESS = 'SET_PROGRESS' 3 | const ADD_TOAST = 'ADD_TOAST' 4 | const DELETE_TOAST = 'DELETE_TOAST' 5 | const SET_TRANSITION = 'SET_TRANSITION' 6 | 7 | const state = { 8 | transition: true, // 默认开启动画效果 9 | progress: 0, 10 | toast: null 11 | } 12 | 13 | const getters = { 14 | transition: state => state.transition, 15 | progress: state => state.progress, 16 | toast: state => state.toast 17 | } 18 | 19 | let timeoutId 20 | 21 | const actions = { 22 | setProgress ({ commit }, progress) { 23 | commit(SET_PROGRESS, progress) 24 | if (progress === 100) { 25 | setTimeout(() => { 26 | commit(SET_PROGRESS, 0) 27 | }, 500) 28 | } 29 | }, 30 | 31 | setTransition ({ commit }, payload) { 32 | commit(SET_TRANSITION, payload) 33 | }, 34 | 35 | addToast ({ state, commit }, payload) { 36 | function doAddToast () { 37 | if (timeoutId) { 38 | clearTimeout(timeoutId) 39 | } 40 | commit(ADD_TOAST, payload) 41 | timeoutId = setTimeout(() => { 42 | commit(DELETE_TOAST) 43 | }, 3000) 44 | } 45 | if (state.toast) { 46 | setTimeout(doAddToast, 3000) 47 | } else { 48 | doAddToast() 49 | } 50 | } 51 | } 52 | 53 | const mutations = { 54 | [SET_PROGRESS] (state, payload) { 55 | state.progress = payload 56 | }, 57 | [ADD_TOAST] (state, payload) { 58 | state.toast = payload 59 | }, 60 | [SET_TRANSITION] (state, payload) { 61 | state.transition = payload 62 | }, 63 | [DELETE_TOAST] (state) { 64 | state.toast = null 65 | } 66 | } 67 | 68 | return { 69 | state, 70 | getters, 71 | actions, 72 | mutations 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/modules/config/index.js: -------------------------------------------------------------------------------- 1 | import createStore from './create-store' 2 | import createRoutes from './create-routes' 3 | 4 | export default (context, options = {}) => { 5 | options = { scope: 'config', prefix: 'config', ...options } 6 | 7 | // data, and callback 8 | return [{ 9 | store: createStore(options), 10 | routes: createRoutes(options), 11 | options 12 | }, ({ store, router, dispatch }) => { 13 | // 实现进度条、错误提示 14 | // Vuex 插件的另一种实现方式,参见 src/modules/persist/index.js 15 | store.subscribe(({ payload }) => { 16 | if (!payload || !payload.__status__) { 17 | return 18 | } 19 | 20 | switch (payload.__status__) { 21 | case 'pending': 22 | dispatch('setProgress', 60) 23 | break 24 | case 'success': 25 | dispatch('setProgress', 100) 26 | break 27 | case 'error': 28 | dispatch('setProgress', 100) 29 | dispatch('addToast', payload.__payload__) 30 | break 31 | default: 32 | // setProgress(0) 33 | } 34 | }) 35 | 36 | // router hooks 37 | router.beforeEach((to, from, next) => { 38 | dispatch('setProgress', 80) 39 | next() 40 | }) 41 | router.afterEach(() => { 42 | dispatch('setProgress', 100) 43 | }) 44 | }] 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/config/views/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 92 | -------------------------------------------------------------------------------- /src/modules/core/create-store.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleAction } from 'vuex-actions' 2 | 3 | export default ({ routes }, { scope }) => { 4 | const SET_CORE = 'SET_CORE' 5 | 6 | const state = { 7 | authorized: false, 8 | routes 9 | } 10 | 11 | const getters = { 12 | authorized: state => state.authorized, 13 | routes: ({ routes, authorized }) => state.routes.filter(({ path, meta }) => path !== '/' && (!meta || (!meta.hidden && (meta.auth === undefined || meta.auth === authorized)))) 14 | } 15 | 16 | const actions = { 17 | setCore: createAction(SET_CORE) 18 | } 19 | 20 | const mutations = { 21 | [SET_CORE]: handleAction((state, mutation) => { 22 | Object.assign(state, mutation) 23 | }) 24 | } 25 | 26 | return { 27 | state, 28 | getters, 29 | actions, 30 | mutations 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/core/index.js: -------------------------------------------------------------------------------- 1 | import createStore from './create-store' 2 | 3 | export default (context, options = {}, register) => { 4 | // 合并配置项 5 | options = { scope: 'core', ...options } 6 | 7 | // 注册 store 8 | // 同时传入配置项 9 | return [{ 10 | // 为统一标准,将 context 与 options 做为数据传入 11 | store: createStore(context, options), 12 | options 13 | }, ({ router, subscribe }) => { 14 | let authorized 15 | router.beforeEach((to, from, next) => { 16 | if (to.matched.some(m => m.meta.auth) && !authorized) { 17 | next('/') 18 | } else { 19 | next() 20 | } 21 | }) 22 | // 监听变化 23 | subscribe('authorized', value => { 24 | authorized = value 25 | }) 26 | }] 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/demo/components/avatar.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/modules/demo/components/badge.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | -------------------------------------------------------------------------------- /src/modules/demo/components/button.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /src/modules/demo/components/form.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 95 | -------------------------------------------------------------------------------- /src/modules/demo/components/icon.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | 29 | 48 | -------------------------------------------------------------------------------- /src/modules/demo/components/image.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 50 | 51 | 60 | -------------------------------------------------------------------------------- /src/modules/demo/components/link.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /src/modules/demo/components/modal.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 67 | 68 | 77 | -------------------------------------------------------------------------------- /src/modules/demo/components/paginator.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 59 | -------------------------------------------------------------------------------- /src/modules/demo/components/picker.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 121 | 122 | 128 | -------------------------------------------------------------------------------- /src/modules/demo/components/progress.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 69 | -------------------------------------------------------------------------------- /src/modules/demo/components/range.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 86 | -------------------------------------------------------------------------------- /src/modules/demo/components/row.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 54 | 55 | 67 | -------------------------------------------------------------------------------- /src/modules/demo/components/scroller.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 96 | 97 | 102 | -------------------------------------------------------------------------------- /src/modules/demo/components/slider.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 81 | 82 | 118 | -------------------------------------------------------------------------------- /src/modules/demo/components/spinner.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /src/modules/demo/components/swiper.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 51 | 52 | 57 | -------------------------------------------------------------------------------- /src/modules/demo/components/toast.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | -------------------------------------------------------------------------------- /src/modules/demo/components/uploader.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 69 | 70 | 133 | -------------------------------------------------------------------------------- /src/modules/demo/create-routes.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return [ 3 | { 4 | path: '/', 5 | meta: { 6 | icon: 'eye' 7 | }, 8 | component: () => import('./views/index'), 9 | children: [{ 10 | path: ':component', 11 | meta: { 12 | hidden: true 13 | }, 14 | component: () => import('./views/index') 15 | }] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/demo/index.js: -------------------------------------------------------------------------------- 1 | import createRoutes from './create-routes' 2 | 3 | export default (context, options = {}) => { 4 | options = { scope: 'demo', prefix: 'demo', ...options } 5 | 6 | return { 7 | routes: createRoutes(context, options), 8 | options 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/demo/styles/images/wx@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/src/modules/demo/styles/images/wx@1x.png -------------------------------------------------------------------------------- /src/modules/demo/styles/images/wx@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/src/modules/demo/styles/images/wx@2x.png -------------------------------------------------------------------------------- /src/modules/demo/styles/images/wx@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/src/modules/demo/styles/images/wx@3x.png -------------------------------------------------------------------------------- /src/modules/demo/styles/index.css: -------------------------------------------------------------------------------- 1 | .main { 2 | & h3 { 3 | padding: 20px; 4 | background-color: color(white lightness(-2%)); 5 | } 6 | 7 | & .c-col { 8 | & a { 9 | display: block; 10 | padding: 20px; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/demo/views/index.vue: -------------------------------------------------------------------------------- 1 | 130 | 131 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /src/modules/faq/create-routes.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return [ 3 | { 4 | path: '/', 5 | exact: true, 6 | // 异步 7 | component: () => import('./views/index') 8 | // 同步 9 | // component: require('./views/index') 10 | }, 11 | { 12 | path: '/create', 13 | meta: { 14 | auth: true, 15 | icon: 'plus' 16 | }, 17 | component: () => import('./views/create') 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/faq/create-store.js: -------------------------------------------------------------------------------- 1 | import template from 'platojs/vuex-templates/rest' 2 | 3 | export default () => template({ 4 | source: __DEV__ ? '/api/faq' : '/db/faq.json' 5 | }) 6 | -------------------------------------------------------------------------------- /src/modules/faq/index.js: -------------------------------------------------------------------------------- 1 | import createStore from './create-store' 2 | import createRoutes from './create-routes' 3 | 4 | export default (context, options = {}) => { 5 | options = { scope: 'faq', prefix: 'faq', ...options } 6 | 7 | return { 8 | store: createStore(context, options), 9 | routes: createRoutes(context, options), 10 | options 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/faq/styles/create.css: -------------------------------------------------------------------------------- 1 | .c-button { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/faq/styles/index.css: -------------------------------------------------------------------------------- 1 | .c-button { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/faq/views/create.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /src/modules/faq/views/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/modules/i18n/README.md: -------------------------------------------------------------------------------- 1 | # 国际化问题 2 | 3 | ## 需求 4 | 5 | ### 原始需求 6 | 7 | - 默认从本地读取翻译资源 8 | - 支持从线上读取翻译资源 9 | 10 | ### 扩展需求 11 | 12 | - 各模块分别提供自己的翻译资源 13 | - 线上资源一个接口返回多个模块的翻译资源 14 | - 支持图片的国际化? 15 | 16 | ## 方案 17 | 18 | - 本地翻译资源存放在各模块的 state 19 | - 获取线上翻译资源并更新个模块的 state 20 | 21 | ### 约束 22 | 23 | - 模块必须有明确的 scope 24 | - 25 | -------------------------------------------------------------------------------- /src/modules/i18n/create-store.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleAction } from 'vuex-actions' 2 | 3 | export default ({ lang, translations }) => { 4 | const SET_I18N = 'SET_I18N' 5 | 6 | const state = { lang, translations } 7 | 8 | const getters = { 9 | lang: state => state.lang, 10 | translations: state => state.translations 11 | } 12 | 13 | const actions = { 14 | setI18n: createAction(SET_I18N) 15 | } 16 | 17 | const mutations = { 18 | [SET_I18N]: handleAction((state, mutation) => { 19 | Object.assign(state, mutation) 20 | }) 21 | } 22 | 23 | return { 24 | state, 25 | getters, 26 | actions, 27 | mutations 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/i18n/index.js: -------------------------------------------------------------------------------- 1 | import request from 'platojs/request' 2 | import template from 'string-template' 3 | import createStore from './create-store' 4 | 5 | export default ({ Vue }, options = {}) => { 6 | options = { 7 | scope: 'i18n', 8 | lang: (navigator.language || navigator.browserLanguage).toLowerCase().split('-')[0], 9 | translations: {}, 10 | ...options 11 | } 12 | 13 | const { 14 | scope, 15 | fallbackLang = 'zh', 16 | urlPattern = './i18n/{lang}.json' 17 | } = options 18 | 19 | function parseKeys (keys, scope) { 20 | switch (keys.indexOf('/')) { 21 | case 0: // 以 `/` 开头,说明是从全局里查找匹配 22 | console.warn('[I18N] 斜杠开头的规则已废弃,请直接使用`scope/k.e.y.s`') 23 | const arr1 = keys.split('.') 24 | return { 25 | scope: arr1[0].slice(1), 26 | keyArray: arr1.slice(1) 27 | } 28 | case -1: 29 | return { 30 | scope, 31 | keyArray: keys.split('.') 32 | } 33 | default: 34 | const arr2 = keys.match(/(^\w+)\/(.+$)/) 35 | return { 36 | scope: arr2[1], 37 | keyArray: arr2[2].split('.') 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * I18n 44 | */ 45 | Vue.prototype.__ = Vue.prototype.$translate = function (keys, ...args) { 46 | if (!keys) { 47 | return keys 48 | } 49 | 50 | const parsed = parseKeys(keys, this.$scope) 51 | 52 | const { translations } = this.$store.state[scope] 53 | 54 | // keys 以 `.` 作为分隔符 55 | return template(parsed.keyArray.reduce((res, key) => { 56 | if (res && typeof res === 'object' && res.hasOwnProperty(key)) { 57 | return res[key] 58 | } 59 | return keys 60 | }, parsed.scope ? translations[parsed.scope] : translations), ...args) 61 | } 62 | 63 | return [{ 64 | store: createStore(options), 65 | options 66 | }, ({ dispatch, subscribe }) => { 67 | let fallbackEnabled = false 68 | 69 | function fetchTranslations (lang) { 70 | // add `dir="..."` to `` 71 | document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr' 72 | // request json data 73 | request(template(urlPattern, { lang })) 74 | .then(translations => { 75 | dispatch('setI18n', { translations }) 76 | }) 77 | .catch(() => { 78 | if (fallbackEnabled) { 79 | // 确保只执行一次,避免无限循环 80 | fallbackEnabled = false 81 | fetchTranslations(fallbackLang) 82 | } 83 | }) 84 | } 85 | 86 | // vm for watching i18n 87 | subscribe('lang', lang => { 88 | fallbackEnabled = true 89 | fetchTranslations(lang) 90 | }) 91 | }] 92 | } 93 | -------------------------------------------------------------------------------- /src/modules/logger/index.js: -------------------------------------------------------------------------------- 1 | import createLogger from 'vuex/dist/logger' 2 | 3 | export default ({ store }) => { 4 | createLogger()(store) 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/persist/index.js: -------------------------------------------------------------------------------- 1 | import createPersist from 'vuex-localstorage' 2 | 3 | /** 4 | * 数据持久化 5 | */ 6 | 7 | export default ({ name, version }) => { 8 | // 只注册数据,不注册回调 9 | return { 10 | // Vuex 只支持全局 plugins 11 | plugins: [createPersist({ 12 | // 使用 name 与 version 做 key 13 | // 避免可能的新旧版本间的数据冲突 14 | namespace: `${name}@${version}`, 15 | expires: 7 * 24 * 60 * 60 * 1e3 16 | })] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/request/index.js: -------------------------------------------------------------------------------- 1 | import { configure, intercept } from 'platojs/request' 2 | 3 | /** 4 | * 修改 request 方法的全局配置 5 | */ 6 | 7 | export default ({ Vue, store, name, version }, options = {}) => { 8 | options = { scope: 'request', ...options } 9 | 10 | // 全局,在请求头部加入自定义字段 11 | configure({ 12 | headers: { 13 | 'Who-Am-I': `${name}@${version}` 14 | } 15 | }) 16 | 17 | // 如果当前浏览器不支持 CORS,则使用代理 18 | if (!('withCredentials' in new XMLHttpRequest())) { 19 | intercept({ 20 | request: [({ req }) => { 21 | // 如果请求是跨域,则使用本地代理 22 | // blablabla... 23 | return { req } 24 | }] 25 | }) 26 | } 27 | 28 | return ({ subscribe }) => { 29 | // 修改全局 Accept-Language 30 | subscribe('i18n/lang', value => configure({ 31 | headers: { 32 | 'Accept-Language': value 33 | } 34 | })) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/user/create-routes.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return [ 3 | { 4 | path: '/login', 5 | meta: { 6 | icon: 'lock', 7 | auth: false 8 | }, 9 | component: () => import('./views/login') 10 | }, 11 | { 12 | path: '/logout', 13 | meta: { 14 | icon: 'lock', 15 | auth: true 16 | }, 17 | component: () => import('./views/logout') 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/user/index.js: -------------------------------------------------------------------------------- 1 | import createRoutes from './create-routes' 2 | 3 | export default (context, options = {}) => { 4 | options = { scope: 'user', prefix: 'user', ...options } 5 | 6 | return { 7 | routes: createRoutes(context, options), 8 | options 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/user/styles/logout.css: -------------------------------------------------------------------------------- 1 | .main { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | 9 | .c-image { 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/user/views/login.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 160 | -------------------------------------------------------------------------------- /src/modules/user/views/logout.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/modules/validator/index.js: -------------------------------------------------------------------------------- 1 | import Validator from 'platojs/plugins/validator' 2 | 3 | export default ({ Vue }) => { 4 | Vue.use(Validator) 5 | 6 | // 不注册任何数据或回调 7 | } 8 | -------------------------------------------------------------------------------- /src/polyfills/README.md: -------------------------------------------------------------------------------- 1 | polyfills for compatibility 2 | -------------------------------------------------------------------------------- /src/polyfills/_global.js: -------------------------------------------------------------------------------- 1 | /* eslint no-new-func: 0 */ 2 | export default typeof window !== 'undefined' 3 | ? window : typeof self !== 'undefined' 4 | ? self : Function('return this')() 5 | -------------------------------------------------------------------------------- /src/polyfills/index.js: -------------------------------------------------------------------------------- 1 | import './touchable' 2 | import './promise' 3 | import 'core-js/fn/array/find' 4 | import 'core-js/fn/array/find-index' 5 | import 'core-js/fn/object/assign' 6 | import 'regenerator-runtime/runtime' 7 | import 'whatwg-fetch' 8 | -------------------------------------------------------------------------------- /src/polyfills/promise.js: -------------------------------------------------------------------------------- 1 | import Promise from 'nuo' 2 | import global from './_global' 3 | 4 | global.Promise = Promise 5 | -------------------------------------------------------------------------------- /src/polyfills/touchable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 在 PC 端模拟触摸事件 3 | */ 4 | (function (doc) { 5 | if ('ontouchstart' in doc) { 6 | return 7 | } 8 | 9 | function dispatchTouchEvent (name, originalEvent) { 10 | let event 11 | 12 | try { 13 | // Not supported in some versions of Android's old WebKit-based WebView 14 | // use document.createEvent() instead 15 | event = new Event(name, originalEvent) 16 | } catch (e) { 17 | event = doc.createEvent('HTMLEvents') 18 | event.initEvent(name, !!originalEvent.bubbles, !!originalEvent.cancelable) 19 | } 20 | 21 | Object.assign(event, { 22 | originalEvent, 23 | touches: [{ 24 | pageX: originalEvent.pageX, 25 | pageY: originalEvent.pageY 26 | }] 27 | }) 28 | 29 | originalEvent.target.dispatchEvent(event) 30 | if (event.defaultPrevented) { 31 | originalEvent.preventDefault() 32 | // 阻止点击事件 33 | if (name === 'touchend') { 34 | originalEvent.target.addEventListener('click', e => { 35 | e.preventDefault() 36 | }) 37 | } 38 | } 39 | if (event.cancelBubble) { 40 | originalEvent.stopPropagation() 41 | } 42 | } 43 | 44 | const eventMap = { 45 | mousedown: 'touchstart', 46 | mousemove: 'touchmove', 47 | mouseout: 'touchcancel', 48 | mouseup: 'touchend' 49 | } 50 | 51 | Object.keys(eventMap).forEach(key => { 52 | doc.addEventListener(key, function (event) { 53 | dispatchTouchEvent(eventMap[key], event) 54 | }, false) 55 | }) 56 | })(document) 57 | -------------------------------------------------------------------------------- /src/static/CNAME: -------------------------------------------------------------------------------- 1 | plato.crossjs.com 2 | -------------------------------------------------------------------------------- /src/static/README.md: -------------------------------------------------------------------------------- 1 | files here should be copied to dist root with copy-webpack-plugin 2 | -------------------------------------------------------------------------------- /src/static/db/faq.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1avg9fpei0.alia0duccb", 4 | "title": "leancloud is REMOVED!", 5 | "content": "use fake data instead" 6 | }, 7 | { 8 | "id": "1avg9fpei0.hrpkt5v3grg", 9 | "title": "from 🇨🇳", 10 | "content": "for the world" 11 | }, 12 | { 13 | "id": "1avg9fpei0.n45om0s6b78", 14 | "title": "could be used in production?", 15 | "content": "though still working in progress" 16 | }, 17 | { 18 | "id": "1avg9fpei0.rc84eok8ul", 19 | "title": "得到", 20 | "content": "点点滴滴" 21 | }, 22 | { 23 | "id": "1avg9fpei0.00psciss0lo", 24 | "title": "saasfas", 25 | "content": "asddasds" 26 | }, 27 | { 28 | "id": "1avg9fpei0.ar17bbqbthg", 29 | "title": "poop", 30 | "content": "monkeys" 31 | }, 32 | { 33 | "id": "1avg9fpei0.bf5705249k", 34 | "title": "sfdsfsfdfs", 35 | "content": "fsdafsdfsdf" 36 | }, 37 | { 38 | "id": "1avg9fpei0.sjvk7gv5jc8", 39 | "title": "wwww", 40 | "content": "wwww" 41 | }, 42 | { 43 | "id": "1avg9fpei0.0s8ri08ogf8", 44 | "title": "Vue 2.0 is Here!", 45 | "content": "https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.zi0jbpmom" 46 | }, 47 | { 48 | "id": "1avg9fpei0.hr3g287ns1", 49 | "title": "dfgf", 50 | "content": "fddgd" 51 | }, 52 | { 53 | "id": "1avg9fpei0.f8dcvqeukq", 54 | "title": "测试一下 just a test", 55 | "content": "看上去不错哦" 56 | }, 57 | { 58 | "id": "1avg9fpei0.it3fp87k708", 59 | "title": "jjj", 60 | "content": "jjjj" 61 | }, 62 | { 63 | "id": "1avg9fpei0.q99hus0bk", 64 | "title": "使用了自定义的 tap 事件", 65 | "content": "解决 300 毫秒延迟,还在进行更多的测试……" 66 | }, 67 | { 68 | "id": "1avg9fpei0.9t6a3odc0io", 69 | "title": "4344", 70 | "content": "444" 71 | }, 72 | { 73 | "id": "1avg9fpei0.fv6dddg64m8", 74 | "title": "ttt", 75 | "content": "tttt" 76 | }, 77 | { 78 | "id": "1avg9fpei0.hn5qqh7adv", 79 | "title": "1212", 80 | "content": "21242" 81 | }, 82 | { 83 | "id": "1avg9fpei0.jkgcu8rt28", 84 | "title": "d", 85 | "content": "dd" 86 | }, 87 | { 88 | "id": "1avg9fpei0.2n730tvn9fg", 89 | "title": "13213", 90 | "content": "12313" 91 | }, 92 | { 93 | "id": "1avg9fpei0.t5qr27tf30o", 94 | "title": "sss", 95 | "content": "ddd" 96 | }, 97 | { 98 | "id": "1avg9fpei0.tp06rfscto", 99 | "title": "123", 100 | "content": "456" 101 | }, 102 | { 103 | "id": "1avg9fpei0.c97duh41e9", 104 | "title": "fffff", 105 | "content": "ffff" 106 | }, 107 | { 108 | "id": "1avg9fpei0.316lmeshcj8", 109 | "title": "123123", 110 | "content": "123123123" 111 | }, 112 | { 113 | "id": "1avg9fpei0.rfd2nhvnnbg", 114 | "title": "sdfsdfasdfas", 115 | "content": "dfsdfasd" 116 | }, 117 | { 118 | "id": "1avg9fpei0.rlc6teb7f8", 119 | "title": "test for normalizer", 120 | "content": "test for normalizer" 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /src/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/src/static/favicon.png -------------------------------------------------------------------------------- /src/static/i18n/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n": {}, 3 | "validator": { 4 | "required": "الرجاء إدخال {0}", 5 | "minlength": "{0} يجب أن لا يقل عن {1} حرف", 6 | "maxlength": "{0} يجب أن لا يزيد عن {1} حرف", 7 | "pattern": "{0} ليست صحيحة" 8 | }, 9 | "config": { 10 | "language": "لغة", 11 | "transition": "حيوية" 12 | }, 13 | "user": { 14 | "login": { 15 | "username": "حساب", 16 | "password": "كلمة المرور", 17 | "submit": "عرض", 18 | "confirm": "تأكد من تقديم؟" 19 | }, 20 | "logout": { 21 | "submit": "استقال", 22 | "confirm": "تأكيد الخروج؟" 23 | } 24 | }, 25 | "faq": { 26 | "index": { 27 | "nothing": "لا شى", 28 | "delete": "حذف", 29 | "confirm": "تأكيد لحذف؟" 30 | }, 31 | "create": { 32 | "title": "عنوان", 33 | "content": "محتوى", 34 | "submit": "عرض", 35 | "confirm": "إرسال؟" 36 | } 37 | }, 38 | "about": { 39 | "source": "مصدر الرمز" 40 | }, 41 | "demo": {}, 42 | "core": {} 43 | } 44 | -------------------------------------------------------------------------------- /src/static/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n": {}, 3 | "validator": { 4 | "required": "Please input {0}", 5 | "minlength": "{0} must no less then {1} chars", 6 | "maxlength": "{0} must no more then {1} chars", 7 | "pattern": "{0} is NOT valid" 8 | }, 9 | "config": { 10 | "language": "Language", 11 | "transition": "Transition" 12 | }, 13 | "user": { 14 | "login": { 15 | "username": "Username", 16 | "password": "Password", 17 | "submit": "Submit", 18 | "confirm": "Submit?" 19 | }, 20 | "logout": { 21 | "submit": "Log out", 22 | "confirm": "Log out?" 23 | } 24 | }, 25 | "faq": { 26 | "index": { 27 | "nothing": "Nothing", 28 | "delete": "Delete", 29 | "confirm": "Delete?" 30 | }, 31 | "create": { 32 | "title": "Title", 33 | "content": "Content", 34 | "submit": "Submit", 35 | "confirm": "Submit?" 36 | } 37 | }, 38 | "demo": {}, 39 | "about": { 40 | "source": "Source Code" 41 | }, 42 | "core": {} 43 | } 44 | -------------------------------------------------------------------------------- /src/static/i18n/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n": {}, 3 | "validator": { 4 | "required": "请输入 {0}", 5 | "minlength": "{0} 不能少于 {1} 个字符", 6 | "maxlength": "{0} 不能多于 {1} 个字符", 7 | "pattern": "{0} 不是合法的" 8 | }, 9 | "config": { 10 | "language": "语言", 11 | "transition": "动画效果" 12 | }, 13 | "user": { 14 | "login": { 15 | "username": "账号", 16 | "password": "密码", 17 | "submit": "提交", 18 | "confirm": "确认提交?" 19 | }, 20 | "logout": { 21 | "submit": "退出", 22 | "confirm": "确认退出?" 23 | } 24 | }, 25 | "faq": { 26 | "index": { 27 | "nothing": "没事", 28 | "delete": "删除", 29 | "confirm": "确认删除?" 30 | }, 31 | "create": { 32 | "title": "标题", 33 | "content": "内容", 34 | "submit": "提交", 35 | "confirm": "确认提交?" 36 | } 37 | }, 38 | "demo": {}, 39 | "about": { 40 | "source": "源码" 41 | }, 42 | "core": {} 43 | } 44 | -------------------------------------------------------------------------------- /src/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/src/static/images/logo.png -------------------------------------------------------------------------------- /src/static/images/qr@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/src/static/images/qr@1x.png -------------------------------------------------------------------------------- /src/static/images/qr@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/src/static/images/qr@2x.png -------------------------------------------------------------------------------- /src/static/images/qr@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vvenv/plato/1b934d7fc713c559e272421a78562c333309246d/src/static/images/qr@3x.png -------------------------------------------------------------------------------- /src/static/scripts/lib.flexible.js: -------------------------------------------------------------------------------- 1 | !function(a,b){function c(){var b=f.getBoundingClientRect().width;b/i>540&&(b=540*i);var c=b/10;f.style.fontSize=c+"px",k.rem=a.rem=c}var d,e=a.document,f=e.documentElement,g=e.querySelector('meta[name="viewport"]'),h=e.querySelector('meta[name="flexible"]'),i=0,j=0,k=b.flexible||(b.flexible={});if(g){console.warn("将根据已有的meta标签来设置缩放比例");var l=g.getAttribute("content").match(/initial\-scale=([\d\.]+)/);l&&(j=parseFloat(l[1]),i=parseInt(1/j))}else if(h){var m=h.getAttribute("content");if(m){var n=m.match(/initial\-dpr=([\d\.]+)/),o=m.match(/maximum\-dpr=([\d\.]+)/);n&&(i=parseFloat(n[1]),j=parseFloat((1/i).toFixed(2))),o&&(i=parseFloat(o[1]),j=parseFloat((1/i).toFixed(2)))}}if(!i&&!j){var p=a.navigator.userAgent,q=(!!p.match(/android/gi),!!p.match(/iphone/gi)),r=q&&!!p.match(/OS 9_3/),s=a.devicePixelRatio;i=q&&!r?s>=3&&(!i||i>=3)?3:s>=2&&(!i||i>=2)?2:1:1,j=1/i}if(f.setAttribute("data-dpr",i),!g)if(g=e.createElement("meta"),g.setAttribute("name","viewport"),g.setAttribute("content","initial-scale="+j+", maximum-scale="+j+", minimum-scale="+j+", user-scalable=no"),f.firstElementChild)f.firstElementChild.appendChild(g);else{var t=e.createElement("div");t.appendChild(g),e.write(t.innerHTML)}a.addEventListener("resize",function(){clearTimeout(d),d=setTimeout(c,300)},!1),a.addEventListener("pageshow",function(a){a.persisted&&(clearTimeout(d),d=setTimeout(c,300))},!1),"complete"===e.readyState?e.body.style.fontSize=12*i+"px":e.addEventListener("DOMContentLoaded",function(){e.body.style.fontSize=12*i+"px"},!1),c(),k.dpr=a.dpr=i,k.refreshRem=c,k.rem2px=function(a){var b=parseFloat(a)*this.rem;return"string"==typeof a&&a.match(/rem$/)&&(b+="px"),b},k.px2rem=function(a){var b=parseFloat(a)/this.rem;return"string"==typeof a&&a.match(/px$/)&&(b+="rem"),b}}(window,window.lib||(window.lib={})); 2 | -------------------------------------------------------------------------------- /src/static/scripts/lib.viewport.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(a){if(i[a])return i[a].exports;var n=i[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var i={};return t.m=e,t.c=i,t.p="",t(0)}([function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=window;t["default"]=i.flex=function(e,t){var a=e||100,n=t||1,r=i.document,o=navigator.userAgent,d=o.match(/Android[\S\s]+AppleWebkit\/(\d{3})/i),l=o.match(/U3\/((\d+|\.){5,})/i),c=l&&parseInt(l[1].split(".").join(""),10)>=80,p=navigator.appVersion.match(/(iphone|ipad|ipod)/gi),s=i.devicePixelRatio||1;p||d&&d[1]>534||c||(s=1);var u=1/s,m=r.querySelector('meta[name="viewport"]');m||(m=r.createElement("meta"),m.setAttribute("name","viewport"),r.head.appendChild(m)),m.setAttribute("content","width=device-width,user-scalable=no,initial-scale="+u+",maximum-scale="+u+",minimum-scale="+u),r.documentElement.style.fontSize=a/2*s*n+"px"},e.exports=t["default"]}]); 2 | flex(100, 1); 3 | -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | const self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | // http://nightwatchjs.org/guide#settings-file 2 | 3 | const seleniumVersion = require('selenium-server/package.json').version 4 | 5 | module.exports = { 6 | 'src_folders': ['test/e2e/specs'], 7 | 'output_folder': 'test/e2e/reports', 8 | 'custom_assertions_path': ['test/e2e/custom-assertions'], 9 | 10 | 'selenium': { 11 | 'start_process': true, 12 | 'server_path': `node_modules/selenium-server/lib/runner/selenium-server-standalone-${seleniumVersion}.jar`, 13 | 'host': '127.0.0.1', 14 | 'port': 4444, 15 | 'cli_args': { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | 'test_settings': { 21 | 'default': { 22 | 'selenium_port': 4444, 23 | 'selenium_host': 'localhost', 24 | 'silent': true 25 | }, 26 | 27 | 'chrome': { 28 | 'desiredCapabilities': { 29 | 'browserName': 'chrome', 30 | 'javascriptEnabled': true, 31 | 'acceptSslCerts': true, 32 | 'chromeOptions': { 33 | 'args': ['--no-sandbox'] 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/e2e/reports/CHROME_51.0.2704.84_WIN8_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // midway for es6 style 2 | require('babel-register') 3 | 4 | // 1. start the dev server using production config 5 | const WebpackDevServer = require('webpack-dev-server') 6 | const server = new WebpackDevServer(require('webpack')(require('../../webpack.config.babel.js'))) 7 | 8 | server.listen(3000, '127.0.0.1', function () { 9 | console.log('Starting server on http://127.0.0.1:3000') 10 | }) 11 | 12 | // 2. run the nightwatch test suite against it 13 | // to run in additional browsers: 14 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 15 | // 2. add it to the --env flag below 16 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 17 | // For more information on Nightwatch's config file, see 18 | // http://nightwatchjs.org/guide#settings-file 19 | let opts = process.argv.slice(2) 20 | if (opts.indexOf('--config') === -1) { 21 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 22 | } 23 | if (opts.indexOf('--env') === -1) { 24 | opts = opts.concat(['--env', 'chrome']) 25 | } 26 | 27 | const spawn = require('cross-spawn') 28 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 29 | 30 | runner.on('exit', function (code) { 31 | server.close() 32 | process.exit(code) 33 | }) 34 | 35 | runner.on('error', function (err) { 36 | server.close() 37 | throw err 38 | }) 39 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests' (browser) { 6 | browser 7 | .url('http://localhost:3000') 8 | .waitForElementVisible('#container', 5000) 9 | .assert.elementPresent('#logo') 10 | .assert.containsText('a', 'PLATO') 11 | // chevron-down =>  12 | .assert.containsText('#history', '') 13 | // there is no router-view tag in vue 2 14 | .assert.elementCount('.router-view', 0) 15 | .end() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "globals": { 7 | "sinon": false, 8 | "assert": false, 9 | "expect": false, 10 | "triggerHTMLEvents": false, 11 | "triggerMouseEvents": false, 12 | "triggerTouchEvents": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import sinonChai from 'sinon-chai' 3 | import { triggerHTMLEvents, triggerMouseEvents, triggerTouchEvents } from './utils' 4 | 5 | localStorage.clear() 6 | 7 | chai.use(sinonChai) 8 | 9 | global.triggerHTMLEvents = triggerHTMLEvents 10 | global.triggerMouseEvents = triggerMouseEvents 11 | global.triggerTouchEvents = triggerTouchEvents 12 | global.assert = chai.assert 13 | global.expect = chai.expect 14 | 15 | // Reset styles 16 | document.body.style.margin = '0px' 17 | document.body.style.padding = '0px' 18 | 19 | // require all test files (files that ends with .spec.js) 20 | const testsContext = require.context('./', true, /^\.[/\\]((?!node_modules).)*\.spec\.js$/) 21 | testsContext.keys().forEach(testsContext) 22 | 23 | // require all src files except index.js for coverage. 24 | // you can also change this to match only the subset of files that 25 | // you want coverage for. 26 | const componentsContext = require.context('../../src/', true, /^\.\/templates\/.*\.(js|vue)$/) 27 | componentsContext.keys().forEach(componentsContext) 28 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | import config from '../../config' 2 | import webpackConfig from '../../webpack.config.babel.js' 3 | 4 | const debug = require('debug')('PLATO:karma') 5 | debug('Create configuration.') 6 | 7 | const alias = { ...webpackConfig.resolve.alias, vue: 'vue/dist/vue' } 8 | 9 | const karmaConfig = { 10 | basePath: '../../', // project root in relation to bin/karma.js 11 | files: [ 12 | `./${config.dir_src}/polyfills/index.js`, 13 | './node_modules/sinon/pkg/sinon.js', 14 | { 15 | pattern: `./${config.dir_test}/unit/index.js`, 16 | watched: false, 17 | served: true, 18 | included: true 19 | } 20 | ], 21 | proxies: { 22 | // '/api/': 'http://0.0.0.0:3000/api/' 23 | }, 24 | singleRun: config.coverage_enabled, 25 | frameworks: ['mocha', 'es6-shim'], 26 | preprocessors: { 27 | [`${config.dir_src}/polyfills/index.js`]: ['webpack'], 28 | [`${config.dir_test}/unit/index.js`]: ['webpack', 'sourcemap'] 29 | }, 30 | reporters: ['mocha', 'coverage'], 31 | coverageReporter: { 32 | reporters: config.coverage_reporters 33 | }, 34 | browsers: ['Chrome'], 35 | webpack: { 36 | devtool: webpackConfig.devtool, 37 | resolve: { ...webpackConfig.resolve, alias }, 38 | plugins: webpackConfig.plugins, 39 | module: { 40 | rules: webpackConfig.module.rules 41 | }, 42 | node: webpackConfig.node, 43 | performance: { 44 | hints: false 45 | } 46 | }, 47 | webpackMiddleware: { 48 | noInfo: true 49 | } 50 | } 51 | 52 | export default cfg => cfg.set(karmaConfig) 53 | -------------------------------------------------------------------------------- /test/unit/runner.js: -------------------------------------------------------------------------------- 1 | // midway for es6 style 2 | require('babel-register') 3 | 4 | module.exports = require('./karma.conf') 5 | -------------------------------------------------------------------------------- /test/unit/specs/modules/config.spec.js: -------------------------------------------------------------------------------- 1 | import createStore from 'modules/config/create-store' 2 | 3 | const { actions } = createStore() 4 | const { addToast } = actions 5 | 6 | const ADD_TOAST = 'ADD_TOAST' 7 | // const DELETE_TOAST = 'DELETE_TOAST' 8 | 9 | describe('config', function () { 10 | this.timeout(10000) 11 | 12 | it('should add/clear toast', done => { 13 | const state = {} 14 | const commit = (type, value) => { 15 | state.toast = type === ADD_TOAST ? value : null 16 | } 17 | addToast({ 18 | state, 19 | commit 20 | }, 'test') 21 | expect(state.toast).to.equal('test') 22 | setTimeout(() => { 23 | expect(state.toast).to.be.null 24 | done() 25 | }, 4000) 26 | }) 27 | 28 | it('should add/clear toast (2)', done => { 29 | const state = {} 30 | const commit = (type, value) => { 31 | state.toast = type === ADD_TOAST ? value : null 32 | } 33 | addToast({ 34 | state, 35 | commit 36 | }, 'test') 37 | addToast({ 38 | state, 39 | commit 40 | }, 'test1') 41 | expect(state.toast).to.equal('test') 42 | setTimeout(() => { 43 | expect(state.toast).to.equal('test1') 44 | setTimeout(() => { 45 | expect(state.toast).to.be.null 46 | done() 47 | }, 4000) 48 | }, 4000) 49 | }) 50 | 51 | it('should add/clear toast (3)', done => { 52 | const state = {} 53 | const commit = (type, value) => { 54 | state.toast = type === ADD_TOAST ? value : null 55 | } 56 | addToast({ 57 | state, 58 | commit 59 | }, 'test') 60 | expect(state.toast).to.equal('test') 61 | setTimeout(() => { 62 | addToast({ 63 | state, 64 | commit 65 | }, 'test1') 66 | expect(state.toast).to.equal('test1') 67 | setTimeout(() => { 68 | expect(state.toast).to.be.null 69 | done() 70 | }, 4000) 71 | }, 4000) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /test/unit/utils.js: -------------------------------------------------------------------------------- 1 | export function triggerHTMLEvents (target, event, process) { 2 | const e = document.createEvent('HTMLEvents') 3 | e.initEvent(event, true, true) 4 | if (process) process(e) 5 | target.dispatchEvent(e) 6 | return e 7 | } 8 | 9 | export function triggerMouseEvents (target, event, process) { 10 | const e = document.createEvent('MouseEvents') 11 | e.initMouseEvent(event, true, true) 12 | if (process) process(e) 13 | target.dispatchEvent(e) 14 | return e 15 | } 16 | 17 | export function triggerTouchEvents (target, event, process) { 18 | const e = document.createEvent('UIEvent') 19 | e.initUIEvent(event, true, true) 20 | if (process) process(e) 21 | target.dispatchEvent(e) 22 | return e 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import CopyWebpackPlugin from 'copy-webpack-plugin' 3 | import ExtractTextPlugin from 'extract-text-webpack-plugin' 4 | import OptimizeCSSPlugin from 'optimize-css-assets-webpack-plugin' 5 | import HtmlWebpackPlugin from 'html-webpack-plugin' 6 | import FaviconsWebpackPlugin from 'favicons-webpack-plugin' 7 | import FriendlyErrorsPlugin from 'friendly-errors-webpack-plugin' 8 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' 9 | import config, { paths } from './config' 10 | 11 | const pkg = require('./package.json') 12 | 13 | const { __DEV__, __PROD__, __TEST__ } = config.globals 14 | const debug = require('debug')('PLATO:webpack') 15 | 16 | debug('Create configuration.') 17 | 18 | // see: https://github.com/ai/browserslist#queries 19 | const browsers = 'Android >= 4, iOS >= 7' 20 | const postcssOptions = { 21 | plugins: [ 22 | require('postcss-import')({ 23 | path: paths.src('application/styles') 24 | }), 25 | require('postcss-url')({ 26 | basePath: paths.src('static') 27 | }), 28 | require('postcss-cssnext')({ 29 | browsers, 30 | features: { 31 | customProperties: { 32 | variables: require(paths.src('application/styles/variables')) 33 | }, 34 | // 禁用 autoprefixer,在 postcss-rtl 后单独引入 35 | // 否则会跟 postcss-rtl 冲突 36 | autoprefixer: false 37 | } 38 | }), 39 | // 如果不需要 flexible,请移除 40 | require('postcss-pxtorem')({ 41 | rootValue: 100, 42 | propWhiteList: [] 43 | }), 44 | // PostCSS plugin for RTL-optimizations 45 | require('postcss-rtl')({ 46 | // Custom function for adding prefix to selector. Optional. 47 | addPrefixToSelector (selector, prefix) { 48 | if (/^html/.test(selector)) { 49 | return selector.replace(/^html/, `html${prefix}`) 50 | } 51 | if (/:root/.test(selector)) { 52 | return selector.replace(/:root/, `${prefix}:root`) 53 | } 54 | // compliant with postcss-flexible 55 | if (/^\[data-dpr(="[1-3]")?]/.test(selector)) { 56 | return `${prefix}${selector}` 57 | } 58 | return `${prefix} ${selector}` 59 | } 60 | }), 61 | require('autoprefixer')({ 62 | browsers 63 | }), 64 | require('postcss-browser-reporter')(), 65 | require('postcss-reporter')() 66 | ] 67 | } 68 | 69 | const webpackConfig = { 70 | target: 'web', 71 | resolve: { 72 | modules: [paths.src(), 'node_modules'], 73 | extensions: ['.css', '.js', '.json', '.vue'], 74 | alias: {} 75 | }, 76 | node: { 77 | fs: 'empty', 78 | net: 'empty' 79 | }, 80 | devtool: config.compiler_devtool, 81 | devServer: { 82 | host: config.server_host, 83 | port: config.server_port, 84 | // proxy is useful for debugging 85 | proxy: [{ 86 | context: '/api', 87 | target: 'http://localhost:3001', 88 | pathRewrite: { 89 | '^/api': '' // Host path & target path conversion 90 | } 91 | }], 92 | compress: true, 93 | disableHostCheck: true, 94 | hot: true, 95 | noInfo: true 96 | }, 97 | entry: { 98 | app: [ 99 | // 加载 polyfills 100 | paths.src('polyfills/index.js'), 101 | paths.src('index.js')] 102 | }, 103 | output: { 104 | path: paths.dist(), 105 | publicPath: config.compiler_public_path, 106 | filename: `[name].[${config.compiler_hash_type}].js`, 107 | chunkFilename: `[id].[${config.compiler_hash_type}].js` 108 | }, 109 | performance: { 110 | hints: __PROD__ ? 'warning' : false 111 | }, 112 | module: { 113 | rules: [ 114 | { 115 | test: /\.(js|vue)$/, 116 | exclude: /node_modules/, 117 | loader: 'eslint-loader', 118 | options: { 119 | emitWarning: __DEV__, 120 | formatter: require('eslint-friendly-formatter') 121 | }, 122 | enforce: 'pre' 123 | }, 124 | { 125 | test: /\.vue$/, 126 | loader: 'vue-loader', 127 | options: { 128 | postcss: postcssOptions, 129 | autoprefixer: false, 130 | loaders: { 131 | js: 'babel-loader' 132 | }, 133 | // 必须为 true,否则 vue-loader@12.0.0 会导致 css 加载顺序混乱 134 | extractCSS: true 135 | } 136 | }, 137 | { 138 | test: /\.css$/, 139 | loader: 'postcss-loader', 140 | options: postcssOptions 141 | }, 142 | { 143 | test: /\.js$/, 144 | // platojs 模块需要 babel 处理 145 | exclude: /node_modules[/\\](?!(platojs|nuo))/, 146 | loader: 'babel-loader' 147 | }, 148 | { 149 | test: /\.html$/, 150 | loader: 'vue-html-loader' 151 | }, 152 | { 153 | test: /@[1-3]x\S*\.(png|jpg|gif)(\?.*)?$/, 154 | loader: 'file-loader', 155 | options: { 156 | name: '[name].[ext]?[hash:7]' 157 | } 158 | }, 159 | { 160 | test: /\.(png|jpg|gif|svg|woff2?|eot|ttf)(\?.*)?$/, 161 | exclude: /@[1-3]x/, // skip encoding @1x/@2x/@3x images with base64 162 | loader: 'url-loader', 163 | options: { 164 | limit: 10000, 165 | name: '[name].[ext]?[hash:7]' 166 | } 167 | } 168 | ] 169 | }, 170 | plugins: [ 171 | new webpack.DefinePlugin(config.globals), 172 | new HtmlWebpackPlugin({ 173 | filename: 'index.html', 174 | template: paths.src('index.ejs'), 175 | title: `${pkg.name} - ${pkg.description}`, 176 | hash: false, 177 | inject: true, 178 | minify: { 179 | collapseWhitespace: config.compiler_html_minify, 180 | minifyJS: config.compiler_html_minify 181 | } 182 | }), 183 | new CopyWebpackPlugin([{ 184 | from: paths.src('static') 185 | }], { 186 | ignore: ['README.md'] 187 | }), 188 | // extract css into its own file 189 | new ExtractTextPlugin({ 190 | filename: '[name].[contenthash].css', 191 | allChunks: true 192 | }) 193 | ] 194 | } 195 | 196 | // ------------------------------------ 197 | // Plugins 198 | // ------------------------------------ 199 | 200 | if (__PROD__) { 201 | debug('Enable plugins for production (Dedupe & UglifyJS).') 202 | webpackConfig.plugins.push( 203 | new webpack.LoaderOptionsPlugin({ 204 | minimize: true, 205 | options: { 206 | context: __dirname 207 | } 208 | }), 209 | new webpack.optimize.UglifyJsPlugin({ 210 | compress: { 211 | unused: true, 212 | dead_code: true, 213 | warnings: false 214 | }, 215 | sourceMap: true 216 | }), 217 | // Compress extracted CSS. We are using this plugin so that possible 218 | // duplicated CSS from different components can be deduped. 219 | new OptimizeCSSPlugin({ 220 | cssProcessorOptions: { 221 | safe: true 222 | } 223 | }) 224 | ) 225 | 226 | if (process.env.SCRIPT !== 'demo' && process.env.SCRIPT !== 'start') { 227 | webpackConfig.plugins.push( 228 | new BundleAnalyzerPlugin({ 229 | // Can be `server`, `static` or `disabled`. 230 | // In `server` mode analyzer will start HTTP server to show bundle report. 231 | // In `static` mode single HTML file with bundle report will be generated. 232 | // In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`. 233 | analyzerMode: 'server', 234 | // Host that will be used in `server` mode to start HTTP server. 235 | analyzerHost: '127.0.0.1', 236 | // Port that will be used in `server` mode to start HTTP server. 237 | analyzerPort: 8888, 238 | // Path to bundle report file that will be generated in `static` mode. 239 | // Relative to bundles output directory. 240 | reportFilename: 'report.html', 241 | // Automatically open report in default browser 242 | openAnalyzer: true, 243 | // If `true`, Webpack Stats JSON file will be generated in bundles output directory 244 | generateStatsFile: false, 245 | // Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`. 246 | // Relative to bundles output directory. 247 | statsFilename: 'stats.json', 248 | // Options for `stats.toJson()` method. 249 | // For example you can exclude sources of your modules from stats file with `source: false` option. 250 | // See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21 251 | statsOptions: null, 252 | // Log level. Can be 'info', 'warn', 'error' or 'silent'. 253 | logLevel: 'info' 254 | }) 255 | ) 256 | } 257 | } else { 258 | debug('Enable plugins for live development (HMR, NoErrors).') 259 | webpackConfig.plugins.push( 260 | new webpack.HotModuleReplacementPlugin(), 261 | new webpack.NoEmitOnErrorsPlugin(), 262 | new webpack.LoaderOptionsPlugin({ 263 | debug: true, 264 | options: { 265 | context: __dirname 266 | } 267 | }), 268 | new FriendlyErrorsPlugin() 269 | ) 270 | } 271 | 272 | // Don't split bundles during testing, since we only want import one bundle 273 | if (!__TEST__) { 274 | webpackConfig.plugins.push( 275 | new FaviconsWebpackPlugin({ 276 | logo: paths.src('assets/logo.svg'), 277 | prefix: 'icons-[hash:7]/', 278 | icons: { 279 | android: true, 280 | appleIcon: true, 281 | appleStartup: true, 282 | coast: false, 283 | favicons: true, 284 | firefox: false, 285 | opengraph: false, 286 | twitter: false, 287 | yandex: false, 288 | windows: false 289 | } 290 | }), 291 | new webpack.optimize.CommonsChunkPlugin({ 292 | name: 'plato', 293 | minChunks: module => /node_modules[/\\]platojs/.test(module.resource) 294 | }), 295 | new webpack.optimize.CommonsChunkPlugin({ 296 | name: 'manifest', 297 | minChunks: Infinity 298 | }) 299 | ) 300 | } 301 | 302 | export default webpackConfig 303 | --------------------------------------------------------------------------------