├── .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 |
3 | 基于 Vue 2.x
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | a Boilerplate for [mobile] SPAs using vue, vuex, vue-router
13 | Check out 文档 , 示例 and UI 组件
14 |
15 |
16 |
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 |
3 | Based on Vue 2.x
4 |
5 |
6 |
7 |
8 |
9 |
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 |
2 |
3 |
4 |
6 |
7 |
10 | three-bars
11 |
12 |
17 |
18 |
19 |
20 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/application/components/route.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 | {{route.meta.icon}}
8 | {{__(route.title)}}
9 |
10 |
13 |
14 |
15 |
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 |
2 |
3 |
6 |
7 | {{toast}}
8 |
9 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | Based on Vue 2.0
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Check out {{ __('source') }} on Github
17 |
18 |
19 |
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 |
2 |
3 |
4 |
5 | {{ __('transition') }}
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 | {{ __('language') }}
16 |
17 |
18 | {{ Object.keys(languages)[languageIndex] }}
19 |
20 |
21 |
22 |
25 | {{key}}
26 |
27 |
28 |
29 |
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 |
2 |
30 |
31 |
32 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/modules/demo/components/badge.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1
5 | 0
6 | 2
7 | 4
8 | 1024
9 |
10 |
11 | primary
12 | warning
13 |
14 |
15 | xlarge
16 | large
17 | small
18 | xsmall
19 |
20 |
21 |
22 |
23 |
34 |
--------------------------------------------------------------------------------
/src/modules/demo/components/button.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | default
4 | primary
5 | warning
6 |
7 | normal size
8 |
9 | xlarge
10 | large
11 | default
12 | small
13 | xsmall
14 | default disabled
15 | primary disabled
16 | warning disabled
17 |
18 |
19 |
20 |
33 |
--------------------------------------------------------------------------------
/src/modules/demo/components/form.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
11 |
12 |
13 |
16 |
17 |
18 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
95 |
--------------------------------------------------------------------------------
/src/modules/demo/components/icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{key}}
5 | {{key}}
6 |
7 |
8 |
9 |
10 |
28 |
29 |
48 |
--------------------------------------------------------------------------------
/src/modules/demo/components/image.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | flexible: true
5 |
6 |
7 |
8 |
9 |
10 | flexible: false
11 |
12 |
13 |
14 |
15 |
16 | resized by `data-dpr`, and replace css (background-image) with postcss-flexible
17 |
19 |
20 |
21 | no src
22 |
24 |
26 |
27 |
28 | default
29 |
30 |
31 |
32 | too wide
33 |
35 |
36 |
37 |
38 |
39 |
50 |
51 |
60 |
--------------------------------------------------------------------------------
/src/modules/demo/components/link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | default
5 | primary
6 | warning
7 |
8 | xlarge
9 | large
10 | small
11 | xsmall
12 |
13 |
14 |
15 |
16 |
29 |
--------------------------------------------------------------------------------
/src/modules/demo/components/modal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
i'm modal, i'm modal, i'm modal, i'm modal, i'm modal, i'm modal, i'm modal.
6 |
9 | i'm title
10 | i'm body, i'm body, i'm body, i'm body, i'm body, i'm body, i'm body.
11 |
12 |
show modal
13 |
show modal with title
14 |
15 |
16 |
17 |
67 |
68 |
77 |
--------------------------------------------------------------------------------
/src/modules/demo/components/paginator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
12 |
13 |
14 |
17 |
18 |
19 |
22 |
23 |
24 |
27 |
28 |
29 |
32 |
33 |
34 |
37 |
38 |
39 |
42 |
43 |
44 |
45 |
46 |
59 |
--------------------------------------------------------------------------------
/src/modules/demo/components/picker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | {{item}}
10 |
11 |
12 |
13 |
17 | {{item}}
18 |
19 |
20 |
21 |
Selected: {{picked}}
22 |
23 |
24 |
25 |
30 | {{item}}
31 |
32 |
33 | -
34 |
35 |
40 | {{item}}
41 |
42 |
43 | -
44 |
45 |
50 | {{item}}
51 |
52 |
53 |
54 |
55 |
Selected: {{yearList[year]}} - {{month + 1}} - {{date + 1}}
56 |
57 |
58 |
59 |
121 |
122 |
128 |
--------------------------------------------------------------------------------
/src/modules/demo/components/progress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | p1: {{p1}}%
8 |
9 |
10 |
11 |
12 |
13 | p2: {{p2}}%
14 |
15 |
16 |
17 |
18 |
19 | p1: {{p1}}%
20 |
21 |
22 |
23 | p2: {{p2}}%
24 |
25 |
26 | Shuffle
28 |
29 |
30 |
31 |
32 |
69 |
--------------------------------------------------------------------------------
/src/modules/demo/components/range.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 | value: {{value}}, min: 0, max: 100
8 |
9 |
10 |
14 | (disabled) value: {{value}}, min: 0, max: 100
15 |
16 |
17 |
21 | step: 10, value: {{value}}, min: 0, max: 100
22 |
23 |
24 |
28 | step: 20, value: {{value}}, min: 0, max: 100
29 |
30 |
31 |
36 | value2: {{value2}}, min: 20, max: 80
37 |
38 |
39 |
45 | step: 2, value2: {{value2}}, min: 20, max: 80
46 |
47 |
48 |
54 | step: 3, value2: {{value2}}, min: 20, max: 80
55 |
56 |
57 |
58 |
59 |
86 |
--------------------------------------------------------------------------------
/src/modules/demo/components/row.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | col
11 | col
12 |
13 |
14 | col-1
15 | col-3
16 |
17 |
18 | col-3
19 | col-1
20 |
21 |
22 |
23 |
24 | col
25 | col
26 | col
27 |
28 |
29 | col-1
30 | col-2
31 | col-1
32 |
33 |
34 |
35 | four rows in group with padding
36 | four rows in group with padding
37 | four rows in group with padding
38 | four rows in group with padding
39 |
40 |
41 |
42 |
43 |
54 |
55 |
67 |
--------------------------------------------------------------------------------
/src/modules/demo/components/scroller.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 | Release to refresh
13 | Pull down to refresh
14 | Refreshing...
15 |
16 | Pull down to refresh
17 | {{id}} : {{words[(id - 1) % words.length]}}
18 | click to toggle infinite mode
19 |
20 | Release to load
21 | Pull up to load
22 | Loading...
23 | Drained!
24 |
25 |
26 |
27 |
28 |
29 |
96 |
97 |
102 |
--------------------------------------------------------------------------------
/src/modules/demo/components/slider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Single slide
4 |
6 | Single Slide
7 |
8 |
9 | Two slides
10 |
12 | Slide First
13 | Slide Last
14 |
15 |
16 | Three slides
17 |
20 | Slide First
21 | Slide Second
22 | Slide Last
23 |
24 |
25 | Getting content async
26 |
28 | Slide {{i}}
29 |
30 |
31 | Auto sliding, and use same index in two slides
32 |
37 | Slide {{i}}
38 |
39 | Use the same index above, with transition enabled
40 |
44 | Slide {{i}}
45 |
46 |
47 |
48 |
49 |
81 |
82 |
118 |
--------------------------------------------------------------------------------
/src/modules/demo/components/spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
24 |
--------------------------------------------------------------------------------
/src/modules/demo/components/swiper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 | Left
9 | Swipe right
10 |
11 |
12 |
13 |
15 | Swipe left
16 | Right
19 |
20 |
21 |
22 |
24 | Left
27 | Swipe left/right
28 | Right
31 |
32 |
33 |
34 |
35 |
36 |
51 |
52 |
57 |
--------------------------------------------------------------------------------
/src/modules/demo/components/toast.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{toast}}
4 |
5 |
6 |
7 |
28 |
--------------------------------------------------------------------------------
/src/modules/demo/components/uploader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Uploader
4 |
5 |
6 |
7 |
8 |
file-text
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
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 |
2 |
3 |
4 |
Core
5 |
6 |
7 |
8 |
9 | flame Icon
10 |
11 |
12 |
13 |
14 |
15 | flame Badge
16 |
17 |
18 |
19 |
20 |
21 | flame Progress
22 |
23 |
24 |
25 |
26 |
27 | flame Button
28 |
29 |
30 |
31 |
32 |
33 | flame Link
34 |
35 |
36 |
37 |
38 |
39 | flame Form
40 |
41 |
42 |
43 |
44 |
45 | flame Row
46 |
47 |
48 |
49 |
50 |
51 | flame Spinner
52 |
53 |
54 |
55 |
56 |
57 | flame Modal
58 |
59 |
60 |
61 |
62 |
63 | flame Toast
64 |
65 |
66 |
67 |
68 |
69 | flame Image
70 |
71 |
72 |
73 |
74 |
75 | flame Avatar
76 |
77 |
78 |
79 |
80 |
81 | flame Scroller: pulling and infinite scroll
82 |
83 |
84 |
85 |
86 |
87 | flame Picker
88 |
89 |
90 |
91 |
92 |
93 | flame Range
94 |
95 |
96 |
97 |
98 |
99 | flame Slider
100 |
101 |
102 |
103 |
104 |
105 | flame Swiper
106 |
107 |
108 |
109 |
Misc
110 |
111 |
112 |
113 |
114 | flame Paginator
115 |
116 |
117 |
118 |
119 |
120 | flame Uploader
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
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 |
2 |
3 |
{{ __('create.confirm') }}
6 |
7 |
9 |
10 |
11 | {{__(title.label)}}
12 |
13 | {{$validation.errors.filter(function (error) { return error.field === 'title' }).map(function (error) { return error.message }).join(' ')}}
14 |
15 |
21 |
22 |
23 | {{__(content.label)}}
24 |
25 | {{$validation.errors.filter(function (error) { return error.field === 'content' }).map(function (error) { return error.message }).join(' ')}}
26 |
27 |
34 |
35 |
36 |
37 | {{ __('create.submit') }}
39 |
40 |
41 |
42 |
43 |
44 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/src/modules/faq/views/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ __('index.confirm') }}
7 |
13 |
17 |
18 | {{ __('index.nothing') }}
21 |
22 | {{ item.title }}
23 | {{ item.content }}
24 |
25 | {{ __('index.delete') }}
30 |
31 |
32 |
33 |
34 |
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 |
2 |
3 |
4 |
5 |
6 | {{__(username.label)}}
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 | {{__(password.label)}}
20 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 | {{$validation.errors.filter(function (error) { return error.field === 'username' }).map(function (error) { return error.message }).join(' ')}}
34 |
35 |
36 | {{$validation.errors.filter(function (error) { return error.field === 'password' }).map(function (error) { return error.message }).join(' ')}}
37 |
38 |
39 |
40 | {{ __('login.submit') }}
42 |
43 |
44 |
45 |
46 |
160 |
--------------------------------------------------------------------------------
/src/modules/user/views/logout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | {{ __('logout.submit') }}
9 |
10 |
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 |
--------------------------------------------------------------------------------