├── 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 |
2 |
3 | 我真的尽力了,但还是找不到页面
4 |
5 |
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 |
2 | token信息:{{ $store.state.token.token }}
3 |
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 |
2 |
3 | 演示Demo
4 |
5 |
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 |
2 |
3 |
params:{{ $route.params.test }}
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/views/example/query.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
query:{{ $route.query.test }}
4 |
5 |
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 |
2 | 注意 title 的变化
3 |
4 |
5 |
19 |
--------------------------------------------------------------------------------
/src/views/example/components/ExampleList/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
17 |
--------------------------------------------------------------------------------
/plop-templates/page/index.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | 页面内容区域
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
21 |
24 |
--------------------------------------------------------------------------------
/docs/plop.md:
--------------------------------------------------------------------------------
1 | # 快速创建文件
2 |
3 | 开发过程中,避免不了手动去频繁创建页面、组件等文件,并且还要在文件里写一些必要的代码,是不是觉得很麻烦?现在你可以用更简洁的方式来处理这一切。
4 |
5 | 
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 |
2 |
3 |
这是一个非全局组件,需要在页面上引用该组件才能使用
4 |
5 |
6 |
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 |
2 |
3 |
如果未登录,会跳转到登录页,如果已登录,则弹出用户信息。
4 |
5 |
6 |
7 |
8 |
21 |
--------------------------------------------------------------------------------
/src/views/example/svgicon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
这是两个 Svg Icon 图标
4 |
5 |
6 |
使用方法:
7 |
8 | - 上 Iconfont 下载需要的 svg 图标
9 | - 将 svg 文件放入 assets/icons 目录,文件名即为 name
10 |
11 |
12 |
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 |
2 |
5 |
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 |
2 |
3 |
可以修改一下 input 框内的值,然后点击刷新按钮查看效果
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/src/views/example/global.component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
全局组件会自动注册
4 |
使用方法:
5 |
6 | - 全局组件统一放在 ./src/components/global/ 目录下,需要注意各个组件按文件夹区分,文件夹名称与组件名无关联
7 | - 文件夹内至少保留一个文件名为 index 的组件入口,例如 index.vue
8 | - 普通组件必须设置 name 并保证其唯一,自动注册会将组件的 name 设为组件名,可参考 SvgIcon 组件写法
9 | - 如果组件是通过 js 进行调用,则确保组件入口文件为 index.js,下面演示 ExampleNotice 组件,通过 js 调用并展示 Notice
10 |
11 |
显示Notice
12 |
13 |
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 |
2 |
8 |
9 |
10 |
31 |
32 |
38 |
--------------------------------------------------------------------------------
/src/views/example/cookie.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
a的cookie值是:{{ cookie }}
7 |
8 |
9 |
10 |
32 |
--------------------------------------------------------------------------------
/src/components/ExampleNotice/main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ content }}
5 |
6 |
7 |
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 | [](https://github.com/hooray/vue-automation/stargazers)
24 |
25 | [](https://gitee.com/eoner/vue-automation/stargazers)
26 |
27 | ## 生态
28 |
29 | [](https://hooray.gitee.io/fantastic-admin)
30 |
31 | [Fantastic-admin](https://hooray.gitee.io/fantastic-admin)
32 |
33 | 一款开箱即用的 Vue 中后台管理系统框架
34 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 | [](https://github.com/hooray/vue-automation/stargazers)
30 |
31 | [](https://gitee.com/eoner/vue-automation/stargazers)
32 |
33 | ## 推广
34 |
35 | [](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 |
2 |
3 |
4 |
5 |
6 |
![]()
7 |
现在你可以切换路由,你会发现,切换回来后,数据还在。
8 |
9 |
10 |
11 |
36 |
37 |
43 |
--------------------------------------------------------------------------------
/src/views/example/sprite.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 在 vue.config.js 里配置精灵图路径等信息,如果要新增一个精灵图目录,则先复制一份 new SpritesmithPlugin() ,修改目录名和文件名,然后重新运行 serve 任务即可。
8 |
9 |
![]()
10 |
11 |
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 |
2 |
3 |
4 | sprite精灵图
5 | svg icon
6 | 全局组件
7 | axios
8 | cookie
9 | meta
10 | vuex
11 | 组件
12 | 路由params
13 | 路由query
14 | 刷新当前页面
15 | router鉴权
16 | js鉴权
17 |
18 |
19 |
20 |
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 |
--------------------------------------------------------------------------------