├── docs ├── .nojekyll ├── _navbar.md ├── vue-router.md ├── gzip.md ├── _coverpage.md ├── configure.md ├── sass-resources.md ├── svg.md ├── global-component.md ├── axios.md ├── _sidebar.md ├── plop.md ├── vuex.md ├── start.md ├── README.md ├── mobile-support.md ├── cdn.md ├── index.html ├── sprite.md ├── coding-standard.md └── mock.md ├── src ├── assets │ ├── styles │ │ ├── resources │ │ │ ├── variables.scss │ │ │ └── utils.scss │ │ └── example.scss │ ├── images │ │ └── example.png │ ├── sprites │ │ ├── example │ │ │ ├── address.png │ │ │ ├── payment.png │ │ │ └── feedback.png │ │ └── _.scss │ └── icons │ │ ├── example.svg │ │ └── example.color.svg ├── views │ ├── 404.vue │ ├── example │ │ ├── permission.router.vue │ │ ├── params.vue │ │ ├── query.vue │ │ ├── meta.vue │ │ ├── components │ │ │ └── ExampleList │ │ │ │ └── index.vue │ │ ├── component.vue │ │ ├── permission.js.vue │ │ ├── svgicon.vue │ │ ├── reload.vue │ │ ├── global.component.vue │ │ ├── axios.vue │ │ ├── cookie.vue │ │ ├── vuex.vue │ │ └── sprite.vue │ ├── index.vue │ └── login.vue ├── mock │ ├── server.js │ ├── server-modules │ │ └── news.js │ ├── modules │ │ └── news.js │ └── index.js ├── store │ ├── modules │ │ ├── global.js │ │ ├── example.js │ │ └── token.js │ └── index.js ├── router │ ├── modules │ │ ├── root.js │ │ └── example.js │ └── index.js ├── util │ └── index.js ├── components │ ├── SvgIcon │ │ └── index.vue │ ├── ExampleNotice │ │ ├── index.js │ │ └── main.vue │ └── autoRegister.js ├── main.js ├── App.vue ├── layout │ └── example.vue └── api │ └── index.js ├── .stylelintignore ├── .huskyrc ├── public ├── favicon.ico └── index.html ├── .eslintignore ├── .lintstagedrc ├── babel.config.js ├── .editorconfig ├── plop-templates ├── store │ ├── index.hbs │ └── prompt.js ├── page │ ├── index.hbs │ └── prompt.js └── component │ ├── index.hbs │ └── prompt.js ├── plopfile.js ├── .env.development ├── .env.production ├── .gitignore ├── .stylelintrc ├── README.md ├── LICENSE ├── dependencies.cdn.js ├── package.json ├── scss.template.hbs ├── .eslintrc.js └── vue.config.js /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/styles/resources/variables.scss: -------------------------------------------------------------------------------- 1 | // 全局变量 2 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | src/assets/sprites/ 4 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooray/vue-automation/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | babel.config.js 4 | vue.config.js 5 | .eslintrc.js 6 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/assets/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooray/vue-automation/HEAD/src/assets/images/example.png -------------------------------------------------------------------------------- /src/views/example/permission.router.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/sprites/example/address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooray/vue-automation/HEAD/src/assets/sprites/example/address.png -------------------------------------------------------------------------------- /src/assets/sprites/example/payment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooray/vue-automation/HEAD/src/assets/sprites/example/payment.png -------------------------------------------------------------------------------- /src/assets/sprites/example/feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hooray/vue-automation/HEAD/src/assets/sprites/example/feedback.png -------------------------------------------------------------------------------- /src/views/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{js,vue}": [ 3 | "vue-cli-service lint", 4 | "vue-cli-service lint:style" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/sprites/_.scss: -------------------------------------------------------------------------------- 1 | // 勿删 2 | // sass-resources-loader@2.2.1 版本开始,resources 如果指向的资源目录不存在文件,运行会报错 3 | // 所以当精灵图目录为空的时候,保留一个默认的 scss 文件 4 | -------------------------------------------------------------------------------- /src/views/example/params.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/example/query.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | * [Fantastic-admin](https://hooray.gitee.io/fantastic-admin) 2 | * [文档打开慢?试试 Gitee 地址](http://eoner.gitee.io/vue-automation) 3 | -------------------------------------------------------------------------------- /src/assets/styles/example.scss: -------------------------------------------------------------------------------- 1 | // 改目录下可存放第三方样式文件,或者公用样式 2 | // 该例子可在 view/example/sprite.vue 里查看 3 | .sprites { 4 | div { 5 | border: 1px solid #000; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/vue-router.md: -------------------------------------------------------------------------------- 1 | # Vue-router 2 | 3 | 路由也实现了自动注册,但因为有优先级的概念,先定义的会先匹配,所以同一模块下的路由需要放在一个路由配置文件里。 4 | 5 | 开发者只需关心 `router/modules/` 目录下的文件,一个模块对应一个 `.js` 文件,可参考 `router/modules/example.js` 文件。 -------------------------------------------------------------------------------- /docs/gzip.md: -------------------------------------------------------------------------------- 1 | # GZip 支持 2 | 3 | 除了使用 CDN 来提高加载访问速度外,如果后端服务器支持,还可以开启 gzip 进行文件压缩,这是一种更显著的减小文件体积的处理办法,通常可以减小 60% 以上的体积。 4 | 5 | 开启方式也很简单,只需在 `.env.*` 配置文件里修改为: 6 | 7 | ``` 8 | VUE_APP_GZIP = ON 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | # vue-automation 2 | 3 | > 一款开箱即用的 Vue2 项目模版 4 | 5 | [开始使用](#关于-vue-automation) 6 | [项目 Github 地址](https://github.com/hooray/vue-automation) 7 | [项目 Gitee 地址](https://gitee.com/eoner/vue-automation) 8 | -------------------------------------------------------------------------------- /docs/configure.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 3 | 默认提供开发环境和生产环境两套配置,分别在根目录下 `.env.development` 和 `.env.production` 文件里,可配置项有网站标题、接口请求地址和是否开启CDN支持。 4 | 5 | 开发者可根据实际业务需求进行扩展,如果对这块不熟悉,可阅读 Vue CLI [环境变量和模式](https://cli.vuejs.org/zh/guide/mode-and-env.html) 章节。 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | env: { 6 | development: { 7 | plugins: ['dynamic-import-node'] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /plop-templates/store/index.hbs: -------------------------------------------------------------------------------- 1 | const state = {} 2 | 3 | const getters = {} 4 | 5 | const actions = {} 6 | 7 | const mutations = {} 8 | 9 | export default { 10 | namespaced: true, 11 | state, 12 | actions, 13 | getters, 14 | mutations 15 | } 16 | -------------------------------------------------------------------------------- /src/mock/server.js: -------------------------------------------------------------------------------- 1 | let mockMap = {} 2 | 3 | const mocksContext = require.context('./server-modules/', false, /.js$/) 4 | mocksContext.keys().forEach(file_name => { 5 | mockMap = Object.assign(mockMap, mocksContext(file_name).default) 6 | }) 7 | 8 | export default mockMap 9 | -------------------------------------------------------------------------------- /docs/sass-resources.md: -------------------------------------------------------------------------------- 1 | # 全局 SCSS 资源 2 | 3 | > 全局 SCSS 资源并不是全局样式,是变量、@mixin 、@function 这些东西 4 | 5 | 在 `assets/styles/resources/` 目录下存放全局的 SCSS 资源,也就是说在这个目录里的文件,无需在页面上引用即可生效并使用。 6 | 7 | 项目中默认存放了 `utils.scss` 文件,里面有几个 `@mixin` 和 `%` ,你可以尝试在页面中使用它们看看效果。 8 | 9 | 同样,[精灵图](sprite)目录下生成的 SCSS 资源也是全局可调用的。 10 | -------------------------------------------------------------------------------- /src/store/modules/global.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 存放全局公用状态 3 | */ 4 | 5 | const state = {} 6 | 7 | const getters = {} 8 | 9 | const actions = {} 10 | 11 | const mutations = {} 12 | 13 | export default { 14 | namespaced: true, 15 | state, 16 | actions, 17 | getters, 18 | mutations 19 | } 20 | -------------------------------------------------------------------------------- /docs/svg.md: -------------------------------------------------------------------------------- 1 | # SVG 图标 2 | 3 | 现在越来越多项目开始使用 SVG 图标做为精灵图的替代品,本框架也提供了 SVG 图标支持,方便使用。推荐去[阿里巴巴矢量图标库](https://www.iconfont.cn/)下载高质量 SVG 图标 4 | 5 | 首先将 svg 文件放到 `src/assets/icons/` 目录下,然后在页面中就可以使用了,`name` 就是 svg 文件名 6 | 7 | ```html 8 | 9 | ``` 10 | 11 | > `` 组件为全局组件,所以无需注册即可使用 12 | -------------------------------------------------------------------------------- /src/router/modules/root.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | component: () => import(/* webpackChunkName: 'root' */ '@/views/index.vue') 5 | }, 6 | { 7 | path: '/login', 8 | component: () => import(/* webpackChunkName: 'root' */ '@/views/login.vue') 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(plop) { 2 | plop.setWelcomeMessage('请选择需要创建的模式:') 3 | plop.setGenerator('page', require('./plop-templates/page/prompt')) 4 | plop.setGenerator('component', require('./plop-templates/component/prompt')) 5 | plop.setGenerator('store', require('./plop-templates/store/prompt')) 6 | } 7 | -------------------------------------------------------------------------------- /docs/global-component.md: -------------------------------------------------------------------------------- 1 | # 全局组件 2 | 3 | 全局组件存放在 `components/global/` 目录下,需要注意各个组件按文件夹区分。 4 | 5 | 每个组件的文件夹内至少保留一个文件名为 `index` 的组件入口,例如 `index.vue` 。 6 | 7 | 组件必须设置 `name` 并保证其唯一,自动注册会将组件的 `name` 设为组件名,可参考 SvgIcon 组件写法。 8 | 9 | 虽然文件夹名称和 `name` 无关联,但建议与 `name` 保持一致。 10 | 11 | 如果组件是通过 js 进行调用,则确保组件入口文件为 `index.js`,可参考 ExampleNotice 组件。 -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install(Vue) { 3 | Vue.prototype.$toLogin = function() { 4 | this.$router.push({ 5 | path: '/login', 6 | query: { 7 | redirect: this.$route.fullPath 8 | } 9 | }) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 页面标题 2 | VUE_APP_TITLE = 测试环境 3 | # 接口请求地址,会设置到 axios 的 baseURL 参数上 4 | VUE_APP_API_ROOT = / 5 | # 是否开启 CDN 支持,开启设置 ON,关闭设置 OFF 6 | # 详情介绍请阅读 http://eoner.gitee.io/vue-automation/#/cdn 7 | VUE_APP_CDN = OFF 8 | # 是否开启 gzip 压缩,开启设置 ON,关闭设置 OFF 9 | VUE_APP_GZIP = OFF 10 | # 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空 11 | VUE_APP_DEBUG_TOOL = 12 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 页面标题 2 | VUE_APP_TITLE = 网站标题 3 | # 接口请求地址,会设置到 axios 的 baseURL 参数上 4 | VUE_APP_API_ROOT = / 5 | # 是否开启 CDN 支持,开启设置 ON,关闭设置 OFF 6 | # 详情介绍请阅读 http://eoner.gitee.io/vue-automation/#/cdn 7 | VUE_APP_CDN = OFF 8 | # 是否开启 gzip 压缩,开启设置 ON,关闭设置 OFF 9 | VUE_APP_GZIP = OFF 10 | # 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空 11 | VUE_APP_DEBUG_TOOL = 12 | -------------------------------------------------------------------------------- /src/views/example/meta.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /src/views/example/components/ExampleList/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /plop-templates/page/index.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /dist-dev 5 | /src/assets/sprites/*.* 6 | !/src/assets/sprites/_.scss 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw* 25 | -------------------------------------------------------------------------------- /docs/axios.md: -------------------------------------------------------------------------------- 1 | # Axios 拦截器 2 | 3 | 拦截器的用处就是拦截每一次的请求和响应,然后做一些全局的处理。 4 | 5 | 例如接口响应报错,可以在拦截器里用统一的报错提示来展示,方便业务开发。 6 | 7 | 本框架提供了一份拦截器参考代码 `src/api/index.js` ,因为每个公司提供的接口标准不同,所以该文件需要开发者根据各自公司的接口去定制对应的拦截器。 8 | 9 | 代码很简单,首先初始化 `axios` 对象,然后 `axios.interceptors.request.use()` 和 `axios.interceptors.response.use()` 就分别是请求和响应的拦截代码了。 10 | 11 | 参考代码里只做了简单的拦截处理,例如请求的时候会自动带上 `token` ,响应的时候会根据错误信息判断是登录失效还是接口报错。 12 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [使用](start) 2 | * [配置](configure) 3 | * [全局 SCSS 资源](sass-resources) 4 | * [精灵图](sprite) 5 | * [SVG 图标](svg) 6 | * [全局组件](global-component) 7 | * [Vue-router](vue-router) 8 | * [Vuex](vuex) 9 | * [Axios 拦截器](axios) 10 | * [快速创建文件](plop) 11 | * [代码规范](coding-standard.md) 12 | * 扩展 13 | * [Mock 与联调](mock.md) 14 | * [CDN 支持](cdn.md) 15 | * [GZip 支持](gzip.md) 16 | * [移动端支持](mobile-support.md) 17 | -------------------------------------------------------------------------------- /plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /docs/plop.md: -------------------------------------------------------------------------------- 1 | # 快速创建文件 2 | 3 | 开发过程中,避免不了手动去频繁创建页面、组件等文件,并且还要在文件里写一些必要的代码,是不是觉得很麻烦?现在你可以用更简洁的方式来处理这一切。 4 | 5 | ![](https://s1.ax1x.com/2020/06/30/N5jWcV.gif) 6 | 7 | > 该功能基于 [plop](https://www.npmjs.com/package/plop) 实现。 8 | 9 | 模版默认提供了 page(页面/布局) 、component(组件) 、store(全局状态) 三个模版文件,通过 `yarn new` 指令可以自行选择。 10 | 11 | 在实际项目开发中,建议根据项目定制适合项目的模版文件,可以大大提高开发效率,当多人协作开发时,也能统一部分标准。 12 | 13 | 模版目录为 `./plop-templates/` ,如果是新建模版,记得在项目根目录 `plopfile.js` 里引用一下。 -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | const modules = {} 7 | const require_module = require.context('./modules', false, /.js$/) 8 | require_module.keys().forEach(file_name => { 9 | modules[file_name.slice(2, -3)] = require_module(file_name).default 10 | }) 11 | 12 | export default new Vuex.Store({ 13 | modules: modules, 14 | strict: process.env.NODE_ENV !== 'production' 15 | }) 16 | -------------------------------------------------------------------------------- /src/mock/server-modules/news.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | export default { 4 | 'GET /mock/news/list': (req, res) => { 5 | return res.json({ 6 | error: '', 7 | status: 1, 8 | data: Mock.mock({ 9 | 'list|5-10': [ 10 | { 11 | 'title': '@ctitle' 12 | } 13 | ] 14 | }) 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/views/example/component.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /src/mock/modules/news.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | url: 'news/list', 4 | type: 'get', 5 | result: () => { 6 | return { 7 | error: '', 8 | status: 1, 9 | data: { 10 | 'list|5-10': [ 11 | { 12 | 'title': '@ctitle' 13 | } 14 | ] 15 | } 16 | } 17 | } 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /src/views/example/permission.js.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /src/views/example/svgicon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /docs/vuex.md: -------------------------------------------------------------------------------- 1 | # Vuex 2 | 3 | Vuex 同样实现了自动注册,开发只需关注 `store/modules/` 文件夹里的文件即可,同样也按照模块区分文件。 4 | 5 | 新建模版: 6 | 7 | ```js 8 | // example.js 9 | const state = {} 10 | const getters = {} 11 | const actions = {} 12 | const mutations = {} 13 | export default { 14 | namespaced: true, 15 | state, 16 | actions, 17 | getters, 18 | mutations 19 | } 20 | ``` 21 | 22 | 文件默认开启命名空间,文件名会默认注册为模块名。 23 | 24 | 使用方法: 25 | 26 | ```js 27 | this.$store.state.example.xxx; 28 | this.$store.getters['example/xxx']; 29 | this.$store.dispatch('example/xxx'); 30 | this.$store.commit('example/xxx'); 31 | ``` -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-recommended-scss" 5 | ], 6 | "plugins": [ 7 | "stylelint-scss" 8 | ], 9 | "rules": { 10 | "indentation": 4, 11 | "rule-empty-line-before": "never", 12 | "at-rule-empty-line-before": "never", 13 | "no-descending-specificity": null, 14 | "selector-pseudo-class-no-unknown": null, 15 | "selector-pseudo-element-no-unknown": [true, { "ignorePseudoElements": ["v-deep"] }], 16 | "property-no-unknown": null 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ExampleNotice/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const component = require('./main.vue').default 4 | const constructor = Vue.extend(component) 5 | 6 | const exampleNotice = options => { 7 | options = options || {} 8 | let instance = new constructor({ 9 | data: options 10 | }) 11 | instance.vm = instance.$mount() 12 | instance.dom = instance.vm.$el 13 | document.body.appendChild(instance.dom) 14 | return instance.vm 15 | } 16 | 17 | export default { 18 | install: Vue => { 19 | Vue.prototype[`$${component.name}`] = exampleNotice 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/views/example/reload.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | -------------------------------------------------------------------------------- /src/components/autoRegister.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局组件自动注册 3 | * 4 | * 全局组件各个组件按文件夹区分,文件夹名称与组件名无关联,但建议与组件名保持一致 5 | * 文件夹内至少保留一个文件名为 index 的组件入口,例如 index.vue 6 | * 普通组件必须设置 name 并保证其唯一,自动注册会将组件的 name 设为组件名,可参考 SvgIcon 组件写法 7 | * 如果组件是通过 js 进行调用,则确保组件入口文件为 index.js,可参考 ExampleNotice 组件 8 | */ 9 | 10 | import Vue from 'vue' 11 | 12 | const componentsContext = require.context('./', true, /index.(vue|js)$/) 13 | componentsContext.keys().forEach(file_name => { 14 | // 获取文件中的 default 模块 15 | const componentConfig = componentsContext(file_name).default 16 | if (/.vue$/.test(file_name)) { 17 | Vue.component(componentConfig.name, componentConfig) 18 | } else { 19 | Vue.use(componentConfig) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /plop-templates/store/prompt.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: '创建全局状态', 3 | prompts: [ 4 | { 5 | type: 'input', 6 | name: 'name', 7 | message: '请输入模块名称', 8 | validate: v => { 9 | if (!v || v.trim === '') { 10 | return '模块名称不能为空' 11 | } else { 12 | return true 13 | } 14 | } 15 | } 16 | ], 17 | actions: data => { 18 | const actions = [ 19 | { 20 | type: 'add', 21 | path: `src/store/modules/${data.name}.js`, 22 | templateFile: 'plop-templates/store/index.hbs' 23 | } 24 | ] 25 | return actions 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/icons/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/login.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /src/views/example/global.component.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /src/store/modules/example.js: -------------------------------------------------------------------------------- 1 | import api from '@/api' 2 | 3 | const state = { 4 | banner: [] 5 | } 6 | 7 | const getters = { 8 | bannerCount: state => { 9 | return state.banner.length 10 | } 11 | } 12 | 13 | const actions = { 14 | getBanner({ 15 | commit 16 | }) { 17 | api.get('banner/list', { 18 | params: { 19 | categoryid: 1 20 | } 21 | }).then(res => { 22 | commit('setBanner', res.data.banner) 23 | }) 24 | } 25 | } 26 | 27 | const mutations = { 28 | setBanner(state, banner) { 29 | state.banner = banner 30 | }, 31 | removeLast(state) { 32 | state.banner.splice(state.banner.length - 1, 1) 33 | } 34 | } 35 | 36 | export default { 37 | namespaced: true, 38 | state, 39 | actions, 40 | getters, 41 | mutations 42 | } 43 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router/index' 4 | import store from './store/index' 5 | 6 | import api from './api' 7 | Vue.prototype.$api = api 8 | 9 | import dayjs from 'dayjs' 10 | Vue.prototype.$dayjs = dayjs 11 | 12 | import util from './util/index' 13 | Vue.use(util) 14 | 15 | import Cookies from 'js-cookie' 16 | Vue.prototype.$cookies = Cookies 17 | 18 | import VueMeta from 'vue-meta' 19 | Vue.use(VueMeta) 20 | 21 | // 全局组件自动注册 22 | import '@/components/autoRegister' 23 | 24 | // 自动加载 svg 图标 25 | const req = require.context('./assets/icons', false, /\.svg$/) 26 | const requireAll = requireContext => requireContext.keys().map(requireContext) 27 | requireAll(req) 28 | 29 | import './mock' 30 | 31 | Vue.config.productionTip = false 32 | 33 | new Vue({ 34 | router, 35 | store, 36 | render: h => h(App) 37 | }).$mount('#app') 38 | -------------------------------------------------------------------------------- /src/views/example/axios.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 38 | -------------------------------------------------------------------------------- /src/views/example/cookie.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /src/components/ExampleNotice/main.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | 43 | -------------------------------------------------------------------------------- /docs/start.md: -------------------------------------------------------------------------------- 1 | # 使用 2 | 3 | 使用前确保本地环境已安装 [Vue CLI](https://cli.vuejs.org/zh/) 。 4 | 5 | ## 方法 1 6 | 7 | > 适用于初学者快速上手,项目里包含演示文件,方便学习 8 | 9 | ```bash 10 | git clone https://gitee.com/eoner/vue-automation.git 11 | cd vue-automation 12 | yarn install 13 | ``` 14 | 15 | 拉取该项目到本地,安装依赖包后即可运行。 16 | 17 | 运行后,可以看到功能演示,同时项目目录里带有 `example` 的均为演示代码。 18 | 19 | ## 方法 2 20 | 21 | > 适用于已熟练使用的老手,项目里无演代码,方便快速开展工作 22 | 23 | 安装并使用 [1one-project](https://www.npmjs.com/package/1one-project) 进行项目初始化。 24 | 25 | ## 注意事项 26 | 27 | ~~值得一提的是,如果安装过程出现 sass 相关的安装错误,请在安装 [mirror-config-china](https://www.npmjs.com/package/mirror-config-china) 后重试。~~ 28 | 29 | ~~```yarn global add mirror-config-china```~~ 30 | 31 | 大部分安装报错都是因为 `node-sass` 依赖导致,尤其是 Windows 用户,它会强制安装 `python2` 和 `Visual Studio` 才能编译成功。 32 | 33 | 目前本模版已将 `node-sass` 替换为 `sass` ,简化用户安装成本,同时 Sass 官方也已经将 `dart-sass` 作为未来主要的的开发方向了。 34 | 35 | 参考《[Node Sass to Dart Sass](https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/advanced/sass.html)》 36 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 关于 vue-automation 2 | 3 | > vue-automation 即将停止维护,欢迎访问 [Fantastic-template](https://gitee.com/hooray/fantastic-template) ,这一款开箱即用的 Vue3 项目模版,基于 Vite2.x 4 | 5 | ## 这是什么 6 | 7 | 一款开箱即用的 Vue2 项目模版,基于 Vue CLI 8 | 9 | ## 特点 10 | 11 | - 默认集成 vue-router 和 vuex 12 | - 全局 SCSS 资源自动引入 13 | - 全局组件自动注册 14 | - 支持 SVG 图标,CSS 精灵图自动合成 15 | - 支持 mock 数据,可摆脱后端束缚独立开发 16 | - 支持 GZip 和 CDN 优化项目体积/加载速度 17 | - 结合 IDE 插件、ESlint 、stylelint 、Git 钩子,轻松实现团队代码规范 18 | 19 | ## 支持 20 | 21 | 给个小 ❤️ 吧~ 22 | 23 | [![star](https://img.shields.io/github/stars/hooray/vue-automation?style=social)](https://github.com/hooray/vue-automation/stargazers) 24 | 25 | [![star](https://gitee.com/eoner/vue-automation/badge/star.svg?theme=dark)](https://gitee.com/eoner/vue-automation/stargazers) 26 | 27 | ## 生态 28 | 29 | [![](https://hooray.gitee.io/fantastic-admin/logo.png)](https://hooray.gitee.io/fantastic-admin) 30 | 31 | [Fantastic-admin](https://hooray.gitee.io/fantastic-admin) 32 | 33 | 一款开箱即用的 Vue 中后台管理系统框架 34 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue3 项目模版已经发布,[点击访问](https://gitee.com/hooray/fantastic-template) 2 | 3 | # vue-automation 4 | 5 | ## 这是什么 6 | 7 | 一款开箱即用的 Vue 项目模版,基于 Vue CLI 8 | 9 | ## 特点 10 | 11 | - 默认集成 vue-router 和 vuex 12 | - 全局 SCSS 资源自动引入 13 | - 全局组件自动注册 14 | - 支持 SVG 图标,CSS 精灵图自动合成 15 | - 支持 mock 数据,可摆脱后端束缚独立开发 16 | - 支持 GZip 和 CDN 优化项目体积/加载速度 17 | - 结合 IDE 插件、ESlint 、stylelint 、Git 钩子,轻松实现团队代码规范 18 | 19 | ## 文档 20 | 21 | [Github](https://hooray.github.io/vue-automation) 22 | 23 | [Gitee](http://eoner.gitee.io/vue-automation)(推荐国内用户访问) 24 | 25 | ## 支持 26 | 27 | 如果觉得模版不错,或者已经在使用了,希望你可以去 **Github** 或者 **Gitee(码云)** 帮我点个 ⭐ ,这将对我是极大的鼓励。 28 | 29 | [![star](https://img.shields.io/github/stars/hooray/vue-automation?style=social)](https://github.com/hooray/vue-automation/stargazers) 30 | 31 | [![star](https://gitee.com/eoner/vue-automation/badge/star.svg?theme=dark)](https://gitee.com/eoner/vue-automation/stargazers) 32 | 33 | ## 推广 34 | 35 | [![](https://hooray.gitee.io/fantastic-admin/logo.png)](https://hooray.gitee.io/fantastic-admin) 36 | 37 | [Fantastic-admin](https://hooray.gitee.io/fantastic-admin) 38 | 39 | 一款开箱即用的 Vue 中后台管理系统框架 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 vue-automation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/mobile-support.md: -------------------------------------------------------------------------------- 1 | # 移动端支持 2 | 3 | 移动端各司都有自己的解决方案,以下为我司为例,做为参考: 4 | 5 | 我司统一使用 vw/vh 做为移动端的布局单位,单位转换通过 [postcss-px-to-viewport](https://www.npmjs.com/package/postcss-px-to-viewport) 进行处理。 6 | 7 | 首先安装依赖: 8 | 9 | `yarn add -D postcss-px-to-viewport` 10 | 11 | 然后在 `vue.config.js` 里进行配置,具体配置信息可根据项目实际调整: 12 | 13 | ```js 14 | module.exports = { 15 | css: { 16 | loaderOptions: { 17 | postcss: { 18 | plugins: [ 19 | require('postcss-px-to-viewport')({ 20 | 'unitToConvert': 'px', 21 | 'viewportWidth': 750, 22 | 'unitPrecision': 3, 23 | 'viewportUnit': 'vw', 24 | 'selectorBlackList': [ 25 | 'ignore', 26 | 'van', 27 | 'mescroll' 28 | ], 29 | 'minPixelValue': 1, 30 | 'mediaQuery': false 31 | }) 32 | ] 33 | } 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | 最后在开发中就可以直接使用 px 了,最终输出就是 vw 。 -------------------------------------------------------------------------------- /docs/cdn.md: -------------------------------------------------------------------------------- 1 | # CDN 支持 2 | 3 | 开启 CDN 的好处在于,项目中引用的一些第三方库不会打包进项目内,从而减小打包出的文件体积,同时借用 CDN 的优势,大大提高项目加载速度。 4 | 5 | CDN 支持默认不开启,如果需要开启,则在 `.env.production` 生产环境配置文件中修改: 6 | 7 | ``` 8 | VUE_APP_CDN = ON 9 | ``` 10 | 11 | CDN 配置文件存放在项目根目录下的 `dependencies.cdn.js` 文件里,可按照标准格式自行扩展配置。 12 | 13 | ```js 14 | { 15 | name: '', 16 | library: '', 17 | js: '', 18 | css: '' 19 | } 20 | ``` 21 | 22 | 其中 `name` 和 `library` 最终会转成 webpack 中 externals 的配置项, `name` 是引入的包名, `library` 是全局变量。 23 | 24 | 设置好并开启后,原先文件中通过 `import` 进行引入的包,就不需要引入了,代码可以删掉,但是删掉会触发 ESLint 的错误提示,例如: 25 | 26 | ```js 27 | // import Vue from 'vue' 28 | 29 | import api from './api' 30 | Vue.prototype.$api = api // 这行代码会提示 'Vue' is not defined. 31 | ``` 32 | 33 | 解决这个问题就需要在 `.eslintrc.js` 文件中对 `globals` 对象增加 `Vue` 的属性。 34 | 35 | ```js 36 | globals: { 37 | process: true, 38 | require: true, 39 | module: true, 40 | Vue: true 41 | } 42 | ``` 43 | 44 | 这里需要注意以下两点: 45 | 46 | 1. 如果只在生产环境开启 CDN 支持,请确保第三方库的 CDN 版本与本地依赖包的版本一致,以免出现开发环境是正常的,但生产环境缺不行的情况,也就是因为版本不同造成的 bug 47 | 2. 开发环境开启 CDN 支持后会导致 [Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) 无法使用,所以不建议开发环境开启 48 | -------------------------------------------------------------------------------- /src/views/example/vuex.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 36 | 37 | 43 | -------------------------------------------------------------------------------- /src/views/example/sprite.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /src/assets/styles/resources/utils.scss: -------------------------------------------------------------------------------- 1 | // @mixin 通过 @include 调用使用 2 | // % 通过 @extend 调用使用 3 | 4 | // 文字超出隐藏,默认为单行超出隐藏,可设置多行 5 | @mixin text-overflow($line: 1, $fixed-width: true) { 6 | @if ($line==1 and $fixed-width==true) { 7 | overflow: hidden; 8 | text-overflow: ellipsis; 9 | white-space: nowrap; 10 | } 11 | @else { 12 | display: -webkit-box; 13 | -webkit-box-orient: vertical; 14 | -webkit-line-clamp: $line; 15 | overflow: hidden; 16 | } 17 | } 18 | 19 | // 定位居中,默认水平居中,可选择垂直居中,或者水平垂直都居中 20 | @mixin position-center($type: x) { 21 | position: absolute; 22 | @if ($type==x) { 23 | left: 50%; 24 | transform: translateX(-50%); 25 | } 26 | @if ($type==y) { 27 | top: 50%; 28 | transform: translateY(-50%); 29 | } 30 | @if ($type==xy) { 31 | left: 50%; 32 | top: 50%; 33 | transform: translateX(-50%) translateY(-50%); 34 | } 35 | } 36 | 37 | // 文字两端对齐 38 | %justify-align { 39 | text-align: justify; 40 | text-align-last: justify; 41 | } 42 | 43 | // 清除浮动 44 | %clearfix { 45 | zoom: 1; 46 | &::before, 47 | &::after { 48 | content: ''; 49 | display: block; 50 | clear: both; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/mock/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | export function param2Obj(url) { 4 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 5 | if (!search) { 6 | return {} 7 | } 8 | const obj = {} 9 | const searchArr = search.split('&') 10 | searchArr.forEach(v => { 11 | const index = v.indexOf('=') 12 | if (index !== -1) { 13 | const name = v.substring(0, index) 14 | const val = v.substring(index + 1, v.length) 15 | obj[name] = val 16 | } 17 | }) 18 | return obj 19 | } 20 | 21 | function XHR2ExpressReqWrap(respond) { 22 | return function(options) { 23 | let result = null 24 | if (respond instanceof Function) { 25 | const { body, type, url } = options 26 | // https://expressjs.com/en/4x/api.html#req 27 | result = respond({ 28 | method: type, 29 | body: JSON.parse(body), 30 | query: param2Obj(url) 31 | }) 32 | } else { 33 | result = respond 34 | } 35 | return Mock.mock(result) 36 | } 37 | } 38 | const mocksContext = require.context('./modules/', true, /.js$/) 39 | mocksContext.keys().forEach(file_name => { 40 | // 获取文件中的 default 模块 41 | const mocks = mocksContext(file_name) 42 | for (const mock of mocks) { 43 | Mock.mock( 44 | new RegExp(`${process.env.VUE_APP_API_ROOT}mock/${mock.url}`), 45 | mock.type || 'get', 46 | XHR2ExpressReqWrap(mock.result) 47 | ) 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-automation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
载入中...
14 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/sprite.md: -------------------------------------------------------------------------------- 1 | # 精灵图 2 | 3 | > 又称雪碧图,原理是将多张小图合并到一张大图上,以便减少 HTTP 请求,提高网站访问速度。 4 | 5 | 精灵图原始图片的存放位置位于 `assets/sprites/` 目录下,注意按文件夹区分。 6 | 7 | 项目运行前会根据文件夹生成对应的精灵图文件(精灵图图片和 `.scss` 文件),多个文件夹则会生成多个精灵图文件。需要注意的是,在项目运行时,修改文件夹里的图片,会重新生成相关精灵图文件,但如果新建文件夹,则需要重新运行项目才会生成对应精灵图文件。 8 | 9 | 在 `.vue` 文件中可通过 `@include` 直接使用精灵图,无需手动引入 `.scss` 文件: 10 | 11 | ```scss 12 | // 方法 1 13 | // @include [文件夹名称]-sprite([文件名称]); 14 | .icon { 15 | @include example-sprite(address); 16 | } 17 | 18 | // 方法 2 19 | // @include all-[文件夹名称]-sprites; 20 | @include all-example-sprites; 21 | ``` 22 | 23 | 最终输出如下: 24 | 25 | ```css 26 | /* 方法 1 */ 27 | .icon { 28 | background-image: url(img/example.326b35aec20837b9c08563c654422fe6.326b35ae.png); 29 | background-position: 0px 0px; 30 | background-size: 210px 210px; 31 | width: 100px; 32 | height: 100px; 33 | } 34 | 35 | /* 方法 2 */ 36 | .example-address-sprites { 37 | background-image: url(img/example.326b35aec20837b9c08563c654422fe6.326b35ae.png); 38 | background-position: 0 0; 39 | background-size: 210px 210px; 40 | width: 100px; 41 | height: 100px; 42 | } 43 | .example-feedback-sprites { 44 | background-image: url(img/example.326b35aec20837b9c08563c654422fe6.326b35ae.png); 45 | background-position: -110px 0; 46 | background-size: 210px 210px; 47 | width: 100px; 48 | height: 100px; 49 | } 50 | .example-payment-sprites { 51 | background-image: url(img/example.326b35aec20837b9c08563c654422fe6.326b35ae.png); 52 | background-position: 0 -110px; 53 | background-size: 210px 210px; 54 | width: 100px; 55 | height: 100px; 56 | } 57 | ``` 58 | 59 | 如果是小型项目,静态图标不多,可全部放在一个文件夹内;如果是中大型项目,文件夹可按模块来划分,这样不同的模块最终会生成各自的精灵图文件。 -------------------------------------------------------------------------------- /plop-templates/page/prompt.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | function getFolder(path) { 5 | let components = [] 6 | const files = fs.readdirSync(path) 7 | files.forEach(function(item) { 8 | let stat = fs.lstatSync(path + '/' + item) 9 | if (stat.isDirectory() === true && item != 'components') { 10 | components.push(path + '/' + item) 11 | components.push.apply(components, getFolder(path + '/' + item)) 12 | } 13 | }) 14 | return components 15 | } 16 | 17 | module.exports = { 18 | description: '创建页面', 19 | prompts: [ 20 | { 21 | type: 'list', 22 | name: 'path', 23 | message: '请选择页面创建目录', 24 | choices: getFolder('src/views') 25 | }, 26 | { 27 | type: 'input', 28 | name: 'name', 29 | message: '请输入文件名', 30 | validate: v => { 31 | if (!v || v.trim === '') { 32 | return '文件名不能为空' 33 | } else { 34 | return true 35 | } 36 | } 37 | } 38 | ], 39 | actions: data => { 40 | let relativePath = path.relative('src/views', data.path) 41 | const actions = [ 42 | { 43 | type: 'add', 44 | path: `${data.path}/{{dotCase name}}.vue`, 45 | templateFile: 'plop-templates/page/index.hbs', 46 | data: { 47 | componentName: `${relativePath} ${data.name}` 48 | } 49 | } 50 | ] 51 | return actions 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/layout/example.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 44 | -------------------------------------------------------------------------------- /src/store/modules/token.js: -------------------------------------------------------------------------------- 1 | // import api from '@/api' 2 | 3 | const state = { 4 | token: localStorage.token, 5 | failuretime: localStorage.failuretime 6 | } 7 | 8 | const getters = { 9 | isLogin: state => { 10 | let retn = false 11 | if (state.token != null) { 12 | let unix = Date.parse(new Date()) 13 | if (unix < state.failuretime * 1000) { 14 | retn = true 15 | } 16 | } 17 | return retn 18 | } 19 | } 20 | 21 | const actions = { 22 | login({ 23 | commit 24 | }) { 25 | return new Promise(resolve => { 26 | // 模拟登录成功,写入 token 信息 27 | commit('setData', { 28 | token: '1234567890', 29 | failuretime: Date.parse(new Date()) / 1000 + 24 * 60 * 60 30 | }) 31 | resolve() 32 | }) 33 | } 34 | // login({ 35 | // commit 36 | // }, data) { 37 | // return new Promise((resolve, reject) => { 38 | // api.post('member/login', data).then(res => { 39 | // commit('setData', res.data) 40 | // resolve(res) 41 | // }).catch(error => { 42 | // reject(error) 43 | // }) 44 | // }) 45 | // } 46 | } 47 | 48 | const mutations = { 49 | setData(state, data) { 50 | localStorage.setItem('token', data.token) 51 | localStorage.setItem('failuretime', data.failuretime) 52 | state.token = data.token 53 | state.failuretime = data.failuretime 54 | } 55 | } 56 | 57 | export default { 58 | namespaced: true, 59 | state, 60 | actions, 61 | getters, 62 | mutations 63 | } 64 | -------------------------------------------------------------------------------- /dependencies.cdn.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: 'vue', 4 | library: 'Vue', 5 | js: 'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js', 6 | css: '' 7 | }, 8 | { 9 | name: 'vue-router', 10 | library: 'VueRouter', 11 | js: 'https://cdn.jsdelivr.net/npm/vue-router@3.3.4/dist/vue-router.min.js', 12 | css: '' 13 | }, 14 | { 15 | name: 'vuex', 16 | library: 'Vuex', 17 | js: 'https://cdn.jsdelivr.net/npm/vuex@3.5.1/dist/vuex.min.js', 18 | css: '' 19 | }, 20 | { 21 | name: 'axios', 22 | library: 'axios', 23 | js: 'https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js', 24 | css: '' 25 | }, 26 | { 27 | name: 'qs', 28 | library: 'Qs', 29 | js: 'https://cdn.jsdelivr.net/npm/qs@6.9.3/dist/qs.js', 30 | css: '' 31 | }, 32 | { 33 | name: 'nprogress', 34 | library: 'NProgress', 35 | js: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js', 36 | css: 'https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.css' 37 | }, 38 | { 39 | name: 'vue-meta', 40 | library: 'VueMeta', 41 | js: 'https://cdn.jsdelivr.net/npm/vue-meta@2.4.0/dist/vue-meta.min.js', 42 | css: '' 43 | }, 44 | { 45 | name: 'js-cookie', 46 | library: 'Cookies', 47 | js: 'https://cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js', 48 | css: '' 49 | }, 50 | { 51 | name: 'dayjs', 52 | library: 'dayjs', 53 | js: 'https://cdn.jsdelivr.net/npm/dayjs@1.8.29/dayjs.min.js', 54 | css: '' 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %> 10 | 11 | 12 | 13 | <% } %> 14 | <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> 15 | 16 | 17 | <% } %> 18 | 19 | 20 | 23 |
24 | 25 | <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %> 26 | 27 | <% } %> 28 | <% if (htmlWebpackPlugin.options.debugTool == 'eruda') { %> 29 | 30 | 31 | <% } %> 32 | <% if (htmlWebpackPlugin.options.debugTool == 'vconsole') { %> 33 | 34 | 35 | <% } %> 36 | 37 | 38 | -------------------------------------------------------------------------------- /plop-templates/component/prompt.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | function getFolder(path) { 4 | let components = [] 5 | const files = fs.readdirSync(path) 6 | files.forEach(function(item) { 7 | let stat = fs.lstatSync(path + '/' + item) 8 | if (stat.isDirectory() === true && item != 'components') { 9 | components.push(path + '/' + item) 10 | components.push.apply(components, getFolder(path + '/' + item)) 11 | } 12 | }) 13 | return components 14 | } 15 | 16 | module.exports = { 17 | description: '创建组件', 18 | prompts: [ 19 | { 20 | type: 'confirm', 21 | name: 'isGlobal', 22 | message: '是否为全局组件', 23 | default: false 24 | }, 25 | { 26 | type: 'list', 27 | name: 'path', 28 | message: '请选择组件创建目录', 29 | choices: getFolder('src/views'), 30 | when: answers => { 31 | return !answers.isGlobal 32 | } 33 | }, 34 | { 35 | type: 'input', 36 | name: 'name', 37 | message: '请输入组件名称', 38 | validate: v => { 39 | if (!v || v.trim === '') { 40 | return '组件名称不能为空' 41 | } else { 42 | return true 43 | } 44 | } 45 | } 46 | ], 47 | actions: data => { 48 | let path = '' 49 | if (data.isGlobal) { 50 | path = 'src/components/{{properCase name}}/index.vue' 51 | } else { 52 | path = `${data.path}/components/{{properCase name}}/index.vue` 53 | } 54 | const actions = [ 55 | { 56 | type: 'add', 57 | path: path, 58 | templateFile: 'plop-templates/component/index.hbs' 59 | } 60 | ] 61 | return actions 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import store from '@/store/index' 4 | import NProgress from 'nprogress' 5 | import 'nprogress/nprogress.css' // progress bar style 6 | 7 | Vue.use(VueRouter) 8 | 9 | /** 10 | * 因为路由有优先级的概念,先定义的会先匹配,而自动注册是依据文件名的排序来遍历的 11 | * 所以下面这种情况,如果访问 /news/edit ,会指向到 info.vue 页面上 12 | * a.js /news/:id info.vue 13 | * b.js /news/edit edit.vue 14 | * 为避免这种情况发生,同一模块下的路由必须放在一个路由配置文件里 15 | * 按上面的例子,news 模块的路由,应该放到一个类似于 news.js 的文件里 16 | * 至于模块里的路由优先级,可以把 /news/edit 放在 /news/:id 前面,或者把 /news/:id 改成 /news/info/:id 均可 17 | */ 18 | const routes = [] 19 | const require_module = require.context('./modules', false, /.js$/) 20 | require_module.keys().forEach(file_name => { 21 | routes.push(require_module(file_name).default) 22 | }) 23 | 24 | routes.push({ 25 | path: '*', 26 | component: () => import('@/views/404'), 27 | meta: { 28 | title: '找不到页面' 29 | } 30 | }) 31 | 32 | const router = new VueRouter({ 33 | routes: routes.flat() 34 | }) 35 | 36 | // 解决路由在 push/replace 了相同地址报错的问题 37 | const originalPush = VueRouter.prototype.push 38 | VueRouter.prototype.push = function push(location) { 39 | return originalPush.call(this, location).catch(err => err) 40 | } 41 | const originalReplace = VueRouter.prototype.replace 42 | VueRouter.prototype.replace = function replace(location) { 43 | return originalReplace.call(this, location).catch(err => err) 44 | } 45 | 46 | router.beforeEach((to, from, next) => { 47 | NProgress.start() 48 | if (to.meta.requireLogin) { 49 | if (store.getters['token/isLogin']) { 50 | next() 51 | NProgress.done() 52 | } else { 53 | next({ 54 | path: '/login', 55 | query: { 56 | redirect: to.fullPath 57 | } 58 | }) 59 | NProgress.done() 60 | } 61 | } else { 62 | next() 63 | NProgress.done() 64 | } 65 | }) 66 | 67 | export default router 68 | -------------------------------------------------------------------------------- /docs/coding-standard.md: -------------------------------------------------------------------------------- 1 | # 代码规范 2 | 3 | ## IDE 编辑器 4 | 5 | 为保证代码风格统一,统一使用 [VS Code](https://code.visualstudio.com/) 做为开发 IDE ,并安装以下扩展: 6 | 7 | - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 8 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 9 | - [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) 10 | - [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 11 | - [stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) 12 | 13 | 安装完后在 `settings.json` 中增加如下配置: 14 | 15 | ```json 16 | "editor.codeActionsOnSave": { 17 | "source.fixAll.eslint": true, 18 | "source.fixAll.stylelint": true 19 | } 20 | ``` 21 | 22 | 最终效果为,在保存时,会自动对当前文件进行代码格式化操作。 23 | 24 | ## Git 钩子 25 | 26 | 上述操作仅对代码的写法规范进行格式化,例如缩进、空格、结尾的分号等。 27 | 28 | 而在提交代码时, Git 的钩子会检查代码中是否有错误,这些错误是 IDE 无法自动修复的,例如出现未使用过的变量。如果有错误,则会取消此次提交,直到开发者修复完所有错误后才允许提交成功,确保仓库里的代码绝对正确。 29 | 30 | > 可通过修改 `.eslintignore` 和 `.stylelintignore` 忽略无需做代码规范的文件,例如在项目中引用了一些第三方的插件或组件,我们就可以将其忽略 31 | 32 | 如果 `git init` 仓库初始化是在依赖包安装之后执行的,则无法初始化 Git 钩子,建议在 `git init` 之后再执行一遍 `yarn` 或者 `npm i` ,重新安装一遍依赖包。 33 | 34 | ## 配置代码规范 35 | 36 | 配置文件主要有 3 处,分别为 IDE 配置(`.editorconfig`)、ESLint 配置(`.eslintrc.js` 和 `.eslintignore`)、StyleLint 配置(`.stylelintrc` 和 `.stylelintignore`)。 37 | 38 | 以代码缩进举例,本模版默认是以 4 空格进行缩进,如果要调整为 2 空格,则需要在 `.editorconfig` 里修改: 39 | 40 | ``` 41 | indent_size = 2 42 | ``` 43 | 44 | 在 `.eslintrc.js` 里修改: 45 | 46 | ``` 47 | 'indent': [2, 2, { 48 | 'SwitchCase': 1 49 | }], 50 | 51 | ... 52 | 53 | 'vue/html-indent': [2, 2], 54 | 55 | ... 56 | 57 | 'vue/script-indent': [2, 2, { 58 | 'switchCase': 1 59 | }] 60 | ``` 61 | 62 | 在 `.stylelintrc` 里修改: 63 | 64 | ``` 65 | "indentation": 2 66 | ``` 67 | 68 | 修改完毕后,再分别执行下面两句命令: 69 | 70 | ```bash 71 | yarn run lint 72 | yarn run stylelint 73 | ``` 74 | 75 | 该操作会将代码进行一次格式校验,如果规则支持自动修复,则会将不符合规则的代码自动进行格式化。 76 | 77 | 以上面的例子,当缩进规则调整后,我们无需手动去每个文件调整,通过命令可以自动应用新的缩进规则。 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-automation", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build-dev": "vue-cli-service build --mode development --dest dist-dev", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint", 10 | "stylelint": "vue-cli-service lint:style", 11 | "svgo": "svgo -f src/assets/icons", 12 | "new": "plop" 13 | }, 14 | "dependencies": { 15 | "axios": "^0.21.0", 16 | "core-js": "^3.6.4", 17 | "dayjs": "^1.9.4", 18 | "js-cookie": "^2.2.1", 19 | "mockjs": "^1.1.0", 20 | "nprogress": "^0.2.0", 21 | "vue": "^2.6.12", 22 | "vue-cli-plugin-mock": "^1.0.2", 23 | "vue-meta": "^2.4.0", 24 | "vue-router": "^3.4.8", 25 | "vuex": "^3.5.1" 26 | }, 27 | "devDependencies": { 28 | "@vue/cli-plugin-babel": "^4.5.8", 29 | "@vue/cli-plugin-eslint": "^4.5.8", 30 | "@vue/cli-service": "^4.5.8", 31 | "@winner-fed/vue-cli-plugin-stylelint": "^1.0.4", 32 | "babel-eslint": "^10.1.0", 33 | "babel-plugin-dynamic-import-node": "^2.3.3", 34 | "compression-webpack-plugin": "^6.1.1", 35 | "eslint": "^7.12.1", 36 | "eslint-plugin-vue": "^7.1.0", 37 | "html-webpack-plugin": "^4.5.0", 38 | "husky": "^4.3.0", 39 | "lint-staged": "^11.0.0", 40 | "plop": "^2.7.4", 41 | "sass": "^1.28.0", 42 | "sass-loader": "^10.0.4", 43 | "sass-resources-loader": "^2.1.1", 44 | "stylelint": "^13.7.2", 45 | "stylelint-config-recommended-scss": "^4.2.0", 46 | "stylelint-config-standard": "^22.0.0", 47 | "stylelint-scss": "^3.18.0", 48 | "svg-sprite-loader": "^5.0.0", 49 | "svgo": "^2.3.0", 50 | "vue-template-compiler": "^2.6.12", 51 | "webpack-spritesmith": "^1.1.0" 52 | }, 53 | "postcss": { 54 | "plugins": { 55 | "autoprefixer": {} 56 | } 57 | }, 58 | "browserslist": [ 59 | "> 1%", 60 | "last 2 versions", 61 | "not ie <= 8" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | // import Qs from 'qs' 3 | import router from '@/router/index' 4 | import store from '@/store/index' 5 | 6 | const toLogin = () => { 7 | router.push({ 8 | path: '/login', 9 | query: { 10 | redirect: router.currentRoute.fullPath 11 | } 12 | }) 13 | } 14 | 15 | const api = axios.create({ 16 | baseURL: process.env.VUE_APP_API_ROOT, 17 | timeout: 10000, 18 | responseType: 'json' 19 | }) 20 | 21 | api.interceptors.request.use( 22 | request => { 23 | /** 24 | * 全局拦截请求发送前提交的参数 25 | * 以下代码为示例,在登录状态下,分别对 post 和 get 请求加上 token 参数 26 | */ 27 | if (request.method == 'post') { 28 | if (request.data instanceof FormData) { 29 | if (store.getters['token/isLogin']) { 30 | // 如果是 FormData 类型(上传图片) 31 | request.data.append('token', store.state.token.token) 32 | } 33 | } else { 34 | // 带上 token 35 | if (request.data == undefined) { 36 | request.data = {} 37 | } 38 | if (store.getters['token/isLogin']) { 39 | request.data.token = store.state.token.token 40 | } 41 | // request.data = Qs.stringify(request.data) 42 | } 43 | } else { 44 | // 带上 token 45 | if (request.params == undefined) { 46 | request.params = {} 47 | } 48 | if (store.getters['token/isLogin']) { 49 | request.params.token = store.state.token.token 50 | } 51 | } 52 | return request 53 | } 54 | ) 55 | 56 | api.interceptors.response.use( 57 | response => { 58 | /** 59 | * 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示 60 | * 假设返回数据格式为:{ status: 1, error: '', data: '' } 61 | * 规则是当 status 为 1 时表示请求成功,为 0 时表示接口需要登录或者登录状态失效,需要重新登录 62 | * 请求出错时 error 会返回错误信息 63 | * 则代码如下 64 | */ 65 | if (response.data.status === 1) { 66 | if (response.data.error === '') { 67 | // 请求成功并且没有报错 68 | return Promise.resolve(response.data) 69 | } else { 70 | // 这里做错误提示,如果使用了 element ui 则可以使用 Message 进行提示 71 | // Message.error(options) 72 | return Promise.reject(response.data) 73 | } 74 | } else { 75 | toLogin() 76 | } 77 | }, 78 | error => { 79 | return Promise.reject(error) 80 | } 81 | ) 82 | 83 | export default api 84 | -------------------------------------------------------------------------------- /scss.template.hbs: -------------------------------------------------------------------------------- 1 | { 2 | // Default options 3 | 'functions': true, 4 | 'variableNameTransforms': ['dasherize'] 5 | } 6 | 7 | {{#block "sprites"}} 8 | {{#each sprites}} 9 | ${{../spritesheet_info.strings.name}}-sprite-{{strings.name}}: ({{px.x}}, {{px.y}}, {{px.offset_x}}, {{px.offset_y}}, {{px.width}}, {{px.height}}, {{px.total_width}}, {{px.total_height}}, '{{{escaped_image}}}', '{{name}}'); 10 | {{/each}} 11 | 12 | ${{spritesheet_info.strings.name}}-sprites: ( 13 | {{#each sprites}} 14 | {{strings.name}}: ${{../spritesheet_info.strings.name}}-sprite-{{strings.name}}, 15 | {{/each}} 16 | ); 17 | {{/block}} 18 | 19 | {{#block "sprite-functions"}} 20 | {{#if options.functions}} 21 | @mixin {{spritesheet_info.strings.name}}-sprite-width($sprite) { 22 | width: nth($sprite, 5); 23 | } 24 | 25 | @mixin {{spritesheet_info.strings.name}}-sprite-height($sprite) { 26 | height: nth($sprite, 6); 27 | } 28 | 29 | @mixin {{spritesheet_info.strings.name}}-sprite-position($sprite) { 30 | $sprite-offset-x: nth($sprite, 3); 31 | $sprite-offset-y: nth($sprite, 4); 32 | background-position: $sprite-offset-x $sprite-offset-y; 33 | } 34 | 35 | @mixin {{spritesheet_info.strings.name}}-sprite-size($sprite) { 36 | background-size: nth($sprite, 7) nth($sprite, 8); 37 | } 38 | 39 | @mixin {{spritesheet_info.strings.name}}-sprite-image($sprite) { 40 | $sprite-image: nth($sprite, 9); 41 | background-image: url(#{$sprite-image}); 42 | } 43 | 44 | @mixin {{spritesheet_info.strings.name}}-sprite($name) { 45 | @include {{spritesheet_info.strings.name}}-sprite-image(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name})); 46 | @include {{spritesheet_info.strings.name}}-sprite-position(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name})); 47 | @include {{spritesheet_info.strings.name}}-sprite-size(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name})); 48 | @include {{spritesheet_info.strings.name}}-sprite-width(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name})); 49 | @include {{spritesheet_info.strings.name}}-sprite-height(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name})); 50 | background-repeat: no-repeat; 51 | } 52 | {{/if}} 53 | {{/block}} 54 | 55 | {{#block "spritesheet-functions"}} 56 | {{#if options.functions}} 57 | @mixin all-{{spritesheet_info.strings.name}}-sprites() { 58 | @each $key, $val in ${{spritesheet_info.strings.name}}-sprites { 59 | $sprite-name: nth($val, 10); 60 | .{{spritesheet_info.strings.name}}-#{$sprite-name}-sprites { 61 | @include {{spritesheet_info.strings.name}}-sprite($key); 62 | } 63 | } 64 | } 65 | {{/if}} 66 | {{/block}} 67 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es6: true 6 | }, 7 | globals: { 8 | process: true, 9 | require: true, 10 | module: true 11 | }, 12 | extends: [ 13 | 'plugin:vue/strongly-recommended', 14 | 'eslint:recommended' 15 | ], 16 | parserOptions: { 17 | ecmaVersion: 2015, 18 | parser: 'babel-eslint', 19 | sourceType: 'module' 20 | }, 21 | rules: { 22 | // 代码风格 23 | 'block-spacing': [2, 'always'], 24 | 'brace-style': [2, '1tbs', { 25 | 'allowSingleLine': true 26 | }], 27 | 'comma-spacing': [2, { 28 | 'before': false, 29 | 'after': true 30 | }], 31 | 'comma-dangle': [2, 'never'], 32 | 'comma-style': [2, 'last'], 33 | 'computed-property-spacing': [2, 'never'], 34 | 'indent': [2, 4, { 35 | 'SwitchCase': 1 36 | }], 37 | 'key-spacing': [2, { 38 | 'beforeColon': false, 39 | 'afterColon': true 40 | }], 41 | 'keyword-spacing': [2, { 42 | 'before': true, 43 | 'after': true 44 | }], 45 | 'linebreak-style': 0, 46 | 'multiline-ternary': [2, 'always-multiline'], 47 | 'no-multiple-empty-lines': [2, { 48 | 'max': 1 49 | }], 50 | 'no-unneeded-ternary': [2, { 51 | 'defaultAssignment': false 52 | }], 53 | 'quotes': [2, 'single'], 54 | 'semi': [2, 'never'], 55 | 'space-before-blocks': [2, 'always'], 56 | 'space-before-function-paren': [2, 'never'], 57 | 'space-in-parens': [2, 'never'], 58 | 'space-infix-ops': 2, 59 | 'space-unary-ops': [2, { 60 | 'words': true, 61 | 'nonwords': false 62 | }], 63 | 'spaced-comment': [2, 'always', { 64 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 65 | }], 66 | 'switch-colon-spacing': [2, { 67 | 'after': true, 68 | 'before': false 69 | }], 70 | // ES6 71 | 'arrow-parens': [2, 'as-needed'], 72 | 'arrow-spacing': [2, { 73 | 'before': true, 74 | 'after': true 75 | }], 76 | // Vue - https://github.com/vuejs/eslint-plugin-vue 77 | 'vue/html-indent': [2, 4], 78 | 'vue/max-attributes-per-line': 0, 79 | 'vue/require-default-prop': 0, 80 | 'vue/singleline-html-element-content-newline': 0, 81 | 'vue/attributes-order': 2, 82 | 'vue/order-in-components': 2, 83 | 'vue/this-in-template': 2, 84 | 'vue/script-indent': [2, 4, { 85 | 'switchCase': 1 86 | }] 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /src/router/modules/example.js: -------------------------------------------------------------------------------- 1 | import ExampleLayout from '@/layout/example' 2 | 3 | export default { 4 | path: '/example', 5 | redirect: '/example/sprite', 6 | component: ExampleLayout, 7 | children: [ 8 | { 9 | path: 'sprite', 10 | component: () => 11 | import(/* webpackChunkName: 'example' */ '@/views/example/sprite.vue') 12 | }, 13 | { 14 | path: 'svgicon', 15 | component: () => 16 | import(/* webpackChunkName: 'example' */ '@/views/example/svgicon.vue') 17 | }, 18 | { 19 | path: 'globalComponent', 20 | component: () => 21 | import(/* webpackChunkName: 'example' */ '@/views/example/global.component.vue') 22 | }, 23 | { 24 | path: 'axios', 25 | component: () => 26 | import(/* webpackChunkName: 'example' */ '@/views/example/axios.vue') 27 | }, 28 | { 29 | path: 'cookie', 30 | component: () => 31 | import(/* webpackChunkName: 'example' */ '@/views/example/cookie.vue') 32 | }, 33 | { 34 | path: 'meta', 35 | component: () => 36 | import(/* webpackChunkName: 'example' */ '@/views/example/meta.vue') 37 | }, 38 | { 39 | path: 'vuex', 40 | component: () => 41 | import(/* webpackChunkName: 'example' */ '@/views/example/vuex.vue') 42 | }, 43 | { 44 | path: 'component', 45 | component: () => 46 | import(/* webpackChunkName: 'example' */ '@/views/example/component.vue') 47 | }, 48 | { 49 | path: 'params/:test', 50 | name: 'exampleParams', // 设置路由的name时,建议加上模块名,避免name和其他模块重名 51 | component: () => 52 | import(/* webpackChunkName: 'example' */ '@/views/example/params.vue') 53 | }, 54 | { 55 | path: 'query', 56 | component: () => 57 | import(/* webpackChunkName: 'example' */ '@/views/example/query.vue') 58 | }, 59 | { 60 | path: 'reload', 61 | component: () => 62 | import(/* webpackChunkName: 'example' */ '@/views/example/reload.vue') 63 | }, 64 | { 65 | path: 'permission/router', 66 | component: () => 67 | import(/* webpackChunkName: 'example' */ '@/views/example/permission.router.vue'), 68 | meta: { 69 | requireLogin: true // 鉴权 70 | } 71 | }, 72 | { 73 | path: 'permission/js', 74 | component: () => 75 | import(/* webpackChunkName: 'example' */ '@/views/example/permission.js.vue') 76 | } 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /docs/mock.md: -------------------------------------------------------------------------------- 1 | # Mock 与联调 2 | 3 | 框架使用 [Mockjs](https://github.com/nuysoft/Mock) 做为模拟数据生成,mock 数据编写规则请阅读官方文档。 4 | 5 | 框架提供两套 mock 解决方案,请对比下述的介绍后自行选择。需注意,两套方案的 mock 数据无法通用,在编写上有一定差异。 6 | 7 | Mockjs 虽然很好用,但是在大型项目中其实并不合适,正规的测试应该是搭建专门的测试服务器进行测试,只是在一些中小型公司,没有这样的资源,使用 Mockjs 是一个折中的办法。 8 | 9 | > 以下两套方案均需要在 `.env.development` 中设置 `VUE_APP_API_ROOT` 为真实接口地址,例如 `VUE_APP_API_ROOT = http://baidu.com/api/` 10 | 11 | ## 方案一 mockjs 12 | 13 | ### 使用说明 14 | 15 | 这是最常见的使用方式,你只需在 `./src/main.js` 中找到 `import './mock'` 并将其注释去掉,然后到 `./src/mock/modules/` 目录下新增 js 文件,然后在里面编写 mock 数据代码即可,例如: 16 | 17 | ```js 18 | // ./src/mock/modules/test.js 19 | module.exports = [ 20 | { 21 | url: 'test', 22 | type: 'get', 23 | result: { 24 | error: '', 25 | state: 1, 26 | data: { 27 | title: '测试', 28 | images: '@image(\'200x200\',\'red\',\'#fff\',\'avatar\')' 29 | } 30 | } 31 | } 32 | ] 33 | ``` 34 | 35 | 当你配置好 mock 数据后,在页面中就可以通过 `this.$api` 进行测试了 36 | 37 | ```js 38 | this.$api.get('mock/test').then(res => { 39 | console.log(res) 40 | }) 41 | ``` 42 | 43 | 这时候可以在控制台看到 mock 数据正常打印出来了。 44 | 45 | 你可能会问,我在 `test.js` 里定义的 `url` 是 `test` ,为什么在调用接口的时候,需要写成 `mock/test` ,这其实是框架的 mock 约定,在 `./src/mock/index.js` 里可以看到这句代码: 46 | 47 | ```js 48 | Mock.mock(new RegExp(`${process.env.VUE_APP_API_ROOT}mock/${mock.url}`), mock.type || 'get', mock.result) 49 | ``` 50 | 51 | 其中需要拦截的 URL 是拼接出来的,中间强制带上了 `mock/` ,这么做的目的是为了方便开发中进行 mock 和真实接口进行切换。例如还是同样的 `test` 接口,当后端开发完毕,只需将调用接口的地方把 `mock/` 删掉即可。 52 | 53 | ```js 54 | this.$api.get('test').then(res => { 55 | console.log(res) 56 | }) 57 | ``` 58 | 59 | 因为请求 URL 改变了,mock 拦截不到,所以这个请求就会切换为真实接口。 60 | 61 | :::tip 扩展 62 | 如果你不喜欢框架的这个 mock 约定,你也可以将 `./src/mock/index.js` 里改为: 63 | 64 | ```js 65 | Mock.mock(new RegExp(`${process.env.VUE_APP_API_ROOT}${mock.url}`), mock.type || 'get', mock.result) 66 | ``` 67 | 68 | 这样调用的时候直接这样就可以: 69 | 70 | ```js 71 | this.$api.get('test').then(res => { 72 | console.log(res) 73 | }) 74 | ``` 75 | 76 | 如果要切换为真实接口,到 `./src/mock/modules/test.js` 里注释或删除对应的 mock 数据即可。 77 | ::: 78 | 79 | :::warning 注意 80 | mock 数据一般仅存在于开发环境,打包的时候注意将 `./src/main.js` 中的 `import './mock'` 删除或注释掉 81 | ::: 82 | 83 | ### 弊端 84 | 85 | 它的最大问题是就是它的实现机制,因为通过重写浏览器的 `XMLHttpRequest` 对象,从而才能拦截请求。大部分情况下用起来还是蛮方便的,但就因为它重写了 `XMLHttpRequest` 对象,所以比如 `progress` 方法,或者一些底层依赖 `XMLHttpRequest` 的库都会和它发生不兼容。 86 | 87 | 其次因为它是本地模拟的数据,实际上不会走任何网络请求,开发过程中,只能通过 `console.log` 进行调试。 88 | 89 | ## 方案二 mock-server 90 | 91 | 这个方案依托于 [vue-cli-plugin-mock](https://github.com/xuxihai123/vue-cli-plugin-mock) 插件实现,主要目的是解决方案一的几个开发弊端,因为是一个真正的 server ,所以你可以通过浏览器开发者工具中的 network ,清楚的看到接口返回的数据结构,并且同时解决了之前 `mockjs` 会重写 `XMLHttpRequest` 对象,导致很多第三方库失效的问题。 92 | 93 | ### 使用说明 94 | 95 | 首先将 `./src/api/index.js` 的 `baseURL` 注释掉或设为空 96 | 97 | ```js 98 | const api = axios.create({ 99 | // baseURL: process.env.VUE_APP_API_ROOT, 100 | timeout: 10000, 101 | responseType: 'json' 102 | // withCredentials: true 103 | }) 104 | ``` 105 | 106 | 然后打开 `vue.config.js` 修改 107 | 108 | ```js 109 | module.exports = { 110 | ... 111 | devServer: { 112 | open: true, 113 | proxy: { 114 | '/mock': { 115 | target: '/', 116 | changeOrigin: true 117 | }, 118 | '/': { 119 | target: process.env.VUE_APP_API_ROOT, 120 | changeOrigin: true 121 | } 122 | } 123 | }, 124 | ... 125 | pluginOptions: { 126 | lintStyleOnBuild: true, 127 | stylelint: { 128 | fix: true 129 | }, 130 | mock: { 131 | entry: './src/mock/server.js', 132 | debug: true 133 | } 134 | }, 135 | ... 136 | } 137 | ``` 138 | 139 | 剩下的操作和方案一类似,在 `./src/mock/server-modules/` 目录下新增 js 文件,然后在里面编写 mock 数据代码即可,注意下编写的规则。 140 | 141 | 142 | 编写好 mock 后,执行下面那段请求代码,就可以在 Network 里看到真实的网络请求了,并且返回的是我们编写的 mock 数据。 143 | 144 | ```js 145 | this.$api.get('mock/test') 146 | ``` 147 | 148 | 如果需要在 mock 和真实接口切换调试只需删除 `mock/` 即可 149 | 150 | ```js 151 | this.$api.get('test') 152 | ``` 153 | 154 | 因为我们设置的本地代理规则是,`/mock` 转发到 `/` 也就是本地,而 `/` 转发到 `process.env.VUE_APP_API_ROOT` ,也就是我们的真实接口地址。 155 | 156 | ### 弊端 157 | 158 | 此方案只是优化了本地开发,因为是本地启用 server ,但如果线上环境需要使用 mock ,只能通过方案一实现。 159 | 160 | ## 总结 161 | 162 | > 两种方案均支持开发环境下 mock 和真实接口的快速切换 163 | 164 | 方案一适合简单场景,并且线上环境如果也需要调用 mock 数据,那只能选这种,本框架演示站的登录以及权限获取就是使用此方案。 165 | 166 | 方案二因为启用了真实 server ,所以适合复杂场景,加上会触发真实网络请求,开发效率比方案一高,并且 mock 文件的编写更容易上手,缺点是 mock 文件无法和方案一共用,如果你即需要使用方案二,又要在线上环境调用 mock 数据,那就需要你维护两份 mock 文件。 167 | -------------------------------------------------------------------------------- /src/assets/icons/example.color.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const spritesmithPlugin = require('webpack-spritesmith') 4 | const terserPlugin = require('terser-webpack-plugin') 5 | const CompressionPlugin = require('compression-webpack-plugin') 6 | const cdnDependencies = require('./dependencies.cdn') 7 | 8 | const spritesmithTasks = [] 9 | fs.readdirSync('src/assets/sprites').map(dirname => { 10 | if (fs.statSync(`src/assets/sprites/${dirname}`).isDirectory()) { 11 | spritesmithTasks.push( 12 | new spritesmithPlugin({ 13 | src: { 14 | cwd: path.resolve(__dirname, `src/assets/sprites/${dirname}`), 15 | glob: '*.png' 16 | }, 17 | target: { 18 | image: path.resolve(__dirname, `src/assets/sprites/${dirname}.[hash].png`), 19 | css: [ 20 | [path.resolve(__dirname, `src/assets/sprites/_${dirname}.scss`), { 21 | format: 'handlebars_based_template', 22 | spritesheetName: dirname 23 | }] 24 | ] 25 | }, 26 | customTemplates: { 27 | 'handlebars_based_template': path.resolve(__dirname, 'scss.template.hbs') 28 | }, 29 | // 样式文件中调用雪碧图地址写法 30 | apiOptions: { 31 | cssImageRef: `~${dirname}.[hash].png` 32 | }, 33 | spritesmithOptions: { 34 | algorithm: 'binary-tree', 35 | padding: 10 36 | } 37 | }) 38 | ) 39 | } 40 | }) 41 | 42 | // CDN 相关 43 | const isCDN = process.env.VUE_APP_CDN == 'ON' 44 | const externals = {} 45 | cdnDependencies.forEach(pkg => { 46 | externals[pkg.name] = pkg.library 47 | }) 48 | const cdn = { 49 | css: cdnDependencies.map(e => e.css).filter(e => e), 50 | js: cdnDependencies.map(e => e.js).filter(e => e) 51 | } 52 | // gzip 相关 53 | const isGZIP = process.env.VUE_APP_GZIP == 'ON' 54 | 55 | module.exports = { 56 | publicPath: '', 57 | productionSourceMap: false, 58 | devServer: { 59 | open: true, 60 | // proxy: { 61 | // '/': { 62 | // target: process.env.VUE_APP_API_ROOT, 63 | // changeOrigin: true 64 | // } 65 | // }, 66 | // 用于 mock-server 67 | // proxy: { 68 | // '/mock': { 69 | // target: '/', 70 | // changeOrigin: true 71 | // }, 72 | // '/': { 73 | // target: process.env.VUE_APP_API_ROOT, 74 | // changeOrigin: true 75 | // } 76 | // }, 77 | }, 78 | configureWebpack: config => { 79 | config.resolve.modules = ['node_modules', 'assets/sprites'] 80 | config.plugins.push(...spritesmithTasks) 81 | if (isCDN) { 82 | config.externals = externals 83 | } 84 | config.optimization = { 85 | minimizer: [ 86 | new terserPlugin({ 87 | terserOptions: { 88 | compress: { 89 | warnings: false, 90 | drop_console: true, 91 | drop_debugger: true, 92 | pure_funcs: ['console.log'] 93 | } 94 | } 95 | }) 96 | ] 97 | } 98 | if (isGZIP) { 99 | return { 100 | plugins: [ 101 | new CompressionPlugin({ 102 | algorithm: 'gzip', 103 | test: /\.(js|css)$/, // 匹配文件名 104 | threshold: 10240, // 对超过10k的数据压缩 105 | deleteOriginalAssets: false, // 不删除源文件 106 | minRatio: 0.8 // 压缩比 107 | }) 108 | ] 109 | } 110 | } 111 | }, 112 | pluginOptions: { 113 | lintStyleOnBuild: true, 114 | stylelint: { 115 | fix: true 116 | }, 117 | mock: { 118 | entry: './src/mock/server.js', 119 | debug: true, 120 | disable: true 121 | } 122 | }, 123 | chainWebpack: config => { 124 | const oneOfsMap = config.module.rule('scss').oneOfs.store 125 | oneOfsMap.forEach(item => { 126 | item.use('sass-resources-loader') 127 | .loader('sass-resources-loader') 128 | .options({ 129 | resources: [ 130 | './src/assets/styles/resources/*.scss', 131 | './src/assets/sprites/*.scss' 132 | ] 133 | }) 134 | .end() 135 | }) 136 | config.module 137 | .rule('svg') 138 | .exclude.add(path.join(__dirname, 'src/assets/icons')) 139 | .end() 140 | config.module 141 | .rule('icons') 142 | .test(/\.svg$/) 143 | .include.add(path.join(__dirname, 'src/assets/icons')) 144 | .end() 145 | .use('svg-sprite-loader') 146 | .loader('svg-sprite-loader') 147 | .options({ 148 | symbolId: 'icon-[name]' 149 | }) 150 | .end() 151 | config.plugin('html') 152 | .tap(args => { 153 | args[0].title = process.env.VUE_APP_TITLE 154 | if (isCDN) { 155 | args[0].cdn = cdn 156 | } 157 | args[0].debugTool = process.env.VUE_APP_DEBUG_TOOL 158 | return args 159 | }) 160 | .end() 161 | } 162 | } 163 | --------------------------------------------------------------------------------