├── .github └── demo.gif ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── generator.js ├── index.js ├── package.json ├── preset.json └── template └── structure ├── .eslintrc.js ├── src ├── App.vue ├── api │ └── index.js ├── components │ ├── errors │ │ ├── 403.vue │ │ ├── 404.vue │ │ ├── 500.vue │ │ ├── index.js │ │ └── index.scss │ └── layout │ │ ├── AppBreadcrumb.vue │ │ ├── AppFooter.vue │ │ ├── AppHeader.vue │ │ ├── AppLayout.vue │ │ └── BlankLayout.vue ├── constants │ └── index.js ├── main.js ├── registerElementUI.js ├── router │ ├── index.js │ └── interceptors │ │ ├── role.js │ │ └── user.js ├── services │ └── index.js ├── store │ ├── index.js │ └── modules │ │ └── example.js ├── styles │ ├── app │ │ ├── base │ │ │ └── index.scss │ │ ├── form │ │ │ └── index.scss │ │ ├── helper │ │ │ └── index.scss │ │ ├── index.scss │ │ └── table │ │ │ └── index.scss │ └── theme │ │ └── element-variables.scss ├── utils │ └── request.js └── views │ ├── About.vue │ ├── Home.vue │ ├── Login.vue │ └── example │ ├── AdminAuthorized.vue │ ├── Form.vue │ ├── Table.vue │ └── config.js └── tests └── unit └── example.spec.js /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/vue-cli-plugin-element/a98e32dfff37c3e9889ea6ad1b087f26532b326a/.github/demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-cli-plugin-element 2 | 3 | [![Version](https://img.shields.io/npm/v/@codetrial/vue-cli-plugin-element.svg)](https://www.npmjs.com/package/@codetrial/vue-cli-plugin-element) 4 | [![License](https://img.shields.io/npm/l/@codetrial/vue-cli-plugin-element.svg)](https://www.npmjs.com/package/@codetrial/vue-cli-plugin-element) 5 | [![Dependencies](https://img.shields.io/david/codetrial/vue-cli-plugin-element.svg)](https://www.npmjs.com/package/@codetrial/vue-cli-plugin-element) 6 | 7 | Quickly build an enterprise application with vue-cli and element-ui in seconds. 8 | 9 | This project is not only a vue-cli plugin but also a vue-cli preset. Have fun! 10 | 11 | :us: English | [:cn: 简体中文](README.zh-CN.md) 12 | 13 | ## Example 14 | 15 | A complete example project: [:zap: @codetrial/element-admin](https://github.com/codetrial/element-admin) 16 | 17 | Live Preview: [:telescope: element-admin.now.sh](https://element-admin.now.sh) 18 | 19 | ![Screen Capture](.github/demo.gif) 20 | 21 | ## Getting Started 22 | 23 | ### Prerequisites 24 | 25 | You must install Vue CLI 3 before you start: 26 | 27 | ```bash 28 | npm install -g @vue/cli 29 | # OR 30 | yarn global add @vue/cli 31 | ``` 32 | 33 | ### Install 34 | 35 | #### Preset 36 | 37 | You can create your project directly via preset, which already includes configurations such as ESLint and other plugins. 38 | 39 | ```bash 40 | vue create --preset codetrial/vue-cli-plugin-element your-awesome-project 41 | ``` 42 | 43 | #### Plugin 44 | 45 | If you don't want to use the preset , you can manually create an empty project via vue-cli. 46 | 47 | ```bash 48 | # make sure the following features are selected: 49 | # - Babel 50 | # - PWA 51 | # - Router 52 | # - Vuex 53 | # - CSS Pre-processors + Sass 54 | # - Linter - Formatter 55 | vue create your-awesome-project 56 | ``` 57 | 58 | Then add the plugin to your project via vue. 59 | 60 | ```bash 61 | vue add @codetrial/element 62 | ``` 63 | 64 | ## Docs 65 | 66 | The full documentation: [:book: codetrial.github.io/element-admin](https://codetrial.github.io/element-admin) 67 | 68 | ## Core Features 69 | 70 | - :camera: Minimal dependencies 71 | - :tv: Project Structure 72 | - :telephone_receiver: View Layout 73 | - :pager: Data Processing Layer 74 | - :watch: Authorization 75 | - :radio: Error Pages 76 | - :mag_right: List Example 77 | - :ghost: Form Example 78 | 79 | ## Contributing 80 | 81 | Looking forward to your pull requests. 82 | 83 | ## Built With 84 | 85 | - [Vue.js](https://github.com/vuejs/vue) 86 | - [ElementUI](https://github.com/ElemeFE/element) 87 | 88 | ## License 89 | 90 | [MIT](http://opensource.org/licenses/MIT) 91 | 92 | Copyright (c) 2018 - present, Felix Yang 93 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # vue-cli-plugin-element 2 | 3 | [![Version](https://img.shields.io/npm/v/@codetrial/vue-cli-plugin-element.svg)](https://www.npmjs.com/package/@codetrial/vue-cli-plugin-element) 4 | [![License](https://img.shields.io/npm/l/@codetrial/vue-cli-plugin-element.svg)](https://www.npmjs.com/package/@codetrial/vue-cli-plugin-element) 5 | [![Dependencies](https://img.shields.io/david/codetrial/vue-cli-plugin-element.svg)](https://www.npmjs.com/package/@codetrial/vue-cli-plugin-element) 6 | 7 | 使用 vue-cli 及 element-ui 闪电般构建一个企业级后台管理系统。 8 | 9 | 这个项目不仅仅是一个 vue-cli 插件,也是一个 vue-cli preset,希望能够帮到你! 10 | 11 | :cn: 简体中文 | [:us: English](README.md) 12 | 13 | ## 示例 14 | 15 | 一个使用该插件的完整示例:[:zap: @codetrial/element-admin](https://github.com/codetrial/element-admin) 16 | 17 | 在线预览:[:telescope: element-admin.now.sh](https://element-admin.now.sh) 18 | 19 | ![Screen Capture](.github/demo.gif) 20 | 21 | ## 入门指南 22 | 23 | ### 前置依赖 24 | 25 | 在开始前,你需要先安装 Vue CLI 3: 26 | 27 | ```bash 28 | npm install -g @vue/cli 29 | # OR 30 | yarn global add @vue/cli 31 | ``` 32 | 33 | ### 安装 34 | 35 | #### Preset 36 | 37 | 你可以通过 preset 的方式直接创建你的项目,它已经包含了 ESLint 等插件的配置。 38 | 39 | ```bash 40 | vue create --preset codetrial/vue-cli-plugin-element your-awesome-project 41 | ``` 42 | 43 | #### Plugin 44 | 45 | 如果你不想使用 preset 的方式,你也可以先通过 vue-cli 手动创建一个空的项目。 46 | 47 | ```bash 48 | # 确保你创建的项目选择了以下功能: 49 | # - Babel 50 | # - PWA 51 | # - Router 52 | # - Vuex 53 | # - CSS Pre-processors + Sass 54 | # - Linter - Formatter 55 | vue create your-awesome-project 56 | ``` 57 | 58 | 然后通过 vue 将插件添加到你的项目中。 59 | 60 | ```bash 61 | vue add @codetrial/element 62 | ``` 63 | 64 | ## 文档 65 | 66 | 完整的参考文档:[:book: codetrial.github.io/element-admin](https://codetrial.github.io/element-admin) 67 | 68 | ## 核心功能 69 | 70 | :camera: **最小依赖**:仅依赖 Vue 官方库及 ElementUI 组件库,未额外引入其它第三方库,为你提供自由发挥的空间。 71 | 72 | :tv: **目录结构**:根据项目实战经验,设计了合理、清晰的目录结构。 73 | 74 | :telephone_receiver: **页面布局**:使用 Vue Router 嵌套路由及 ElementUI 内置组件进行布局。 75 | 76 | :pager: **数据处理**:添加独立的 api 及 service 层,将业务逻辑从组件中抽离。 77 | 78 | :watch: **权限控制**:为路由添加配置式拦截器,默认支持用户登录鉴权及角色鉴权。 79 | 80 | :radio: **列表示例**:一个相对比较完整的列表页示例,包含字段查询(过滤),字段排序,页码跳转,批量操作等等。 81 | 82 | :mag_right: **表单示例**:一个相对比较完整的表单页示例,包含表单校验、提交等操作。 83 | 84 | :ghost: **错误页面**:内置简单的(其实是偷懒) 403、404 及 500 错误页。 85 | 86 | ## 贡献 87 | 88 | 期待你的 `pull requests`。如果你觉得有帮助,还请多多反馈! 89 | 90 | ## 技术栈 91 | 92 | - [Vue.js](https://github.com/vuejs/vue) 93 | - [ElementUI](https://github.com/ElemeFE/element) 94 | 95 | ## 许可 96 | 97 | [MIT](http://opensource.org/licenses/MIT) 98 | 99 | Copyright (c) 2018 - present, Felix Yang 100 | -------------------------------------------------------------------------------- /generator.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const removeFiles = (files = []) => { 4 | files.forEach(path => { 5 | if (fs.existsSync(path)) { 6 | fs.unlinkSync(path) 7 | } 8 | }) 9 | } 10 | 11 | const filesToRemove = [ 12 | 'src/components/HelloWorld.vue', 13 | 'src/router.js', 14 | 'src/store.js' 15 | ] 16 | 17 | const importsToRemove = [ 18 | `import store from './store'`, 19 | `import router from './router'` 20 | ] 21 | 22 | module.exports = (api, options, rootOptions) => { 23 | const { 24 | entryFile, 25 | generator: { imports } 26 | } = api 27 | 28 | api.extendPackage({ 29 | dependencies: { 30 | 'element-ui': '^2.4.11', 31 | 'vuex-router-sync': '^5.0.0' 32 | } 33 | }) 34 | 35 | // api.injectImports(entryFile, `import './registerElementUI'`) 36 | 37 | // Remove the default imports generated by vue-cli 38 | if (imports[entryFile]) { 39 | importsToRemove.forEach(itr => imports[entryFile].delete(itr)) 40 | } 41 | 42 | // Remove the default files generated by vue-cli 43 | api.render(function(files) { 44 | filesToRemove.forEach(ftr => delete files[ftr]) 45 | }) 46 | api.render('./template/structure') 47 | 48 | api.onCreateComplete(() => { 49 | removeFiles(filesToRemove.map(ftr => api.resolve(ftr))) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, projectOptions) => { 2 | api.chainWebpack(webpackConfig => {}) 3 | 4 | api.configureWebpack(webpackConfig => {}) 5 | 6 | // api.registerCommand('command', args => {}) 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@codetrial/vue-cli-plugin-element", 3 | "version": "1.1.0", 4 | "description": "Quickly build a backend system with vue-cli and element-ui in seconds.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/codetrial/vue-cli-plugin-element.git" 12 | }, 13 | "keywords": [ 14 | "vue", 15 | "vue-cli", 16 | "vue-cli-plugin", 17 | "vue-cli-preset", 18 | "preset", 19 | "plugin", 20 | "element", 21 | "layout", 22 | "structure" 23 | ], 24 | "author": "felixpy", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/codetrial/vue-cli-plugin-element/issues" 28 | }, 29 | "homepage": "https://github.com/codetrial/vue-cli-plugin-element#readme" 30 | } 31 | -------------------------------------------------------------------------------- /preset.json: -------------------------------------------------------------------------------- 1 | { 2 | "useConfigFiles": true, 3 | "plugins": { 4 | "@vue/cli-plugin-babel": {}, 5 | "@vue/cli-plugin-pwa": {}, 6 | "@vue/cli-plugin-eslint": { 7 | "config": "standard", 8 | "lintOn": ["save"] 9 | }, 10 | "@vue/cli-plugin-unit-jest": {} 11 | }, 12 | "configs": { 13 | "eslintConfig": { 14 | "rules": { 15 | "space-before-function-paren": "off" 16 | } 17 | } 18 | }, 19 | "router": true, 20 | "routerHistoryMode": true, 21 | "vuex": true, 22 | "cssPreprocessor": "sass" 23 | } 24 | -------------------------------------------------------------------------------- /template/structure/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/standard'], 7 | rules: { 8 | 'space-before-function-paren': 'off', 9 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 11 | }, 12 | parserOptions: { 13 | parser: 'babel-eslint' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /template/structure/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /template/structure/src/api/index.js: -------------------------------------------------------------------------------- 1 | import { delay, checkCode } from '@/utils/request' 2 | 3 | /* start of mocking user */ 4 | const users = { 5 | admin: { 6 | id: 100001, 7 | name: 'Administrator', 8 | roles: ['USER', 'ADMIN'] 9 | }, 10 | codetrial: { 11 | id: 100001, 12 | name: 'Codetrial', 13 | roles: ['USER'] 14 | } 15 | } 16 | 17 | export async function getUser(username) { 18 | await delay(200) 19 | 20 | const user = users[username] 21 | const response = user 22 | ? { 23 | status: '1', 24 | data: user 25 | } 26 | : { 27 | status: '1100', 28 | message: 'Wrong user or password' 29 | } 30 | 31 | return Promise.resolve(response).then(checkCode) 32 | } 33 | /* end of mocking user */ 34 | 35 | /* start of mocking example list */ 36 | const exampleSource = Array(235) 37 | .fill(1) 38 | .map((element, index) => { 39 | const personal = index % 2 === 0 40 | return { 41 | id: index + 10000, 42 | name: `Example - ${index} - ${personal ? 'felixpy' : 'codetrial'}`, 43 | type: personal ? 1 : 2, 44 | status: index % 5 === 0 ? 0 : 1, 45 | url: personal ? 'https://felixpy.com' : 'https://codetrial.github.io', 46 | createUser: 100001, 47 | createUserName: 'Felix Yang', 48 | updateUser: 100001, 49 | updateUserName: 'Felix Yang', 50 | createTime: '2018-12-22 11:00:00', 51 | updateTime: '2018-12-22 11:00:00' 52 | } 53 | }) 54 | 55 | export async function getExampleList({ filter = {}, page = {} }) { 56 | const { pageNo = 1, pageSize = 20 } = page 57 | const offset = (pageNo - 1) * pageSize 58 | let list = exampleSource 59 | let total = exampleSource.length 60 | 61 | // filter 62 | Object.keys(filter).forEach(key => { 63 | const filterValue = filter[key] 64 | if (filterValue != null && filterValue.length) { 65 | list = list.filter(item => { 66 | if (Array.isArray(filterValue)) { 67 | return filterValue.map(String).indexOf(String(item[key])) > -1 68 | } 69 | return String(item[key]) === String(filterValue) 70 | }) 71 | } 72 | }) 73 | 74 | total = list.length 75 | 76 | // sort 77 | list = list.sort((a, b) => { 78 | const { order, orderBy } = page 79 | 80 | if (!order || !orderBy) { 81 | return 82 | } 83 | 84 | const diff = (order === 'descending' ? -1 : 1) * (a[orderBy] - b[orderBy]) 85 | return diff > 0 ? 1 : -1 86 | }) 87 | 88 | // pagination 89 | list = list.slice(offset, offset + pageSize) 90 | 91 | await delay(1000) 92 | 93 | return Promise.resolve({ 94 | status: '1', 95 | data: { 96 | list, 97 | page: { 98 | ...page, 99 | total 100 | } 101 | } 102 | }).then(checkCode) 103 | } 104 | /* end of mocking example list */ 105 | -------------------------------------------------------------------------------- /template/structure/src/components/errors/403.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /template/structure/src/components/errors/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /template/structure/src/components/errors/500.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /template/structure/src/components/errors/index.js: -------------------------------------------------------------------------------- 1 | import Forbidden from './403.vue' 2 | import NotFound from './404.vue' 3 | import InternalServerError from './500.vue' 4 | 5 | export { Forbidden, NotFound, InternalServerError } 6 | -------------------------------------------------------------------------------- /template/structure/src/components/errors/index.scss: -------------------------------------------------------------------------------- 1 | .error-page { 2 | padding: 100px 0; 3 | 4 | .error-page__tip { 5 | font-size: 48px; 6 | font-weight: bold; 7 | text-align: center; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /template/structure/src/components/layout/AppBreadcrumb.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | 23 | 54 | -------------------------------------------------------------------------------- /template/structure/src/components/layout/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /template/structure/src/components/layout/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 57 | 58 | 91 | -------------------------------------------------------------------------------- /template/structure/src/components/layout/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | 56 | -------------------------------------------------------------------------------- /template/structure/src/components/layout/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /template/structure/src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const RESPONSE_STATUS = { 2 | SUCCESS: '1', 3 | SERVER_ERROR: '1000', 4 | NETWORK_ERROR: '1001' 5 | } 6 | -------------------------------------------------------------------------------- /template/structure/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { sync } from 'vuex-router-sync' 3 | import App from './App.vue' 4 | import router from './router/' 5 | import store from './store/' 6 | import './registerServiceWorker' 7 | 8 | import './registerElementUI' 9 | 10 | import './styles/app/index.scss' 11 | 12 | Vue.config.productionTip = false 13 | 14 | sync(store, router) 15 | 16 | new Vue({ 17 | router, 18 | store, 19 | render: h => h(App) 20 | }).$mount('#app') 21 | -------------------------------------------------------------------------------- /template/structure/src/registerElementUI.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ElementUI from 'element-ui' 3 | 4 | import './styles/theme/element-variables.scss' 5 | 6 | Vue.use(ElementUI) 7 | -------------------------------------------------------------------------------- /template/structure/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import AppLayout from '@/components/layout/AppLayout.vue' 4 | import BlankLayout from '@/components/layout/BlankLayout.vue' 5 | import Home from '@/views/Home.vue' 6 | import Login from '@/views/Login.vue' 7 | import { Forbidden, NotFound, InternalServerError } from '@/components/errors' 8 | 9 | import UserInterceptor from './interceptors/user' 10 | import RoleInterceptor from './interceptors/role' 11 | 12 | Vue.use(Router) 13 | 14 | const router = new Router({ 15 | mode: 'history', 16 | base: process.env.BASE_URL, 17 | routes: [ 18 | { 19 | path: '/login', 20 | component: BlankLayout, 21 | children: [{ path: '', name: 'login', component: Login }] 22 | }, 23 | { 24 | path: '/', 25 | component: AppLayout, 26 | children: [ 27 | { 28 | path: '', 29 | name: 'home', 30 | meta: { 31 | requiresUser: true 32 | }, 33 | component: Home 34 | }, 35 | { 36 | path: '/about', 37 | name: 'about', 38 | meta: { 39 | requiresUser: true 40 | }, 41 | component: () => 42 | import(/* webpackChunkName: "about" */ '@/views/About.vue') 43 | }, 44 | { 45 | path: '/example', 46 | name: 'example-list', 47 | meta: { 48 | requiresUser: true, 49 | 50 | breadcrumb: [ 51 | { name: '示例模块', path: '/example' }, 52 | { name: '列表页' } 53 | ] 54 | // breadcrumb: ['示例模块 /example', '列表页'] 55 | }, 56 | component: () => 57 | import(/* webpackChunkName: "example" */ '@/views/example/Table.vue') 58 | }, 59 | { 60 | path: '/example/new', 61 | name: 'example-form', 62 | meta: { 63 | requiresUser: true, 64 | 65 | breadcrumb: ['示例模块 /example', '表单页'] 66 | }, 67 | component: () => 68 | import(/* webpackChunkName: "example" */ '@/views/example/Form.vue') 69 | }, 70 | { 71 | path: '/example/admin-authorized', 72 | name: 'example-admin-authorized', 73 | meta: { 74 | requiresUser: true, 75 | requiresRole: 'ADMIN', 76 | 77 | breadcrumb: ['示例模块 /example', '管理员权限'] 78 | }, 79 | component: () => 80 | import(/* webpackChunkName: "example" */ '@/views/example/AdminAuthorized.vue') 81 | }, 82 | 83 | { path: '/403', component: Forbidden }, 84 | { path: '/404', component: NotFound }, 85 | { path: '/500', component: InternalServerError }, 86 | { path: '*', component: NotFound } 87 | ] 88 | } 89 | ] 90 | }) 91 | 92 | router.beforeEach(UserInterceptor) 93 | router.beforeEach(RoleInterceptor) 94 | 95 | export default router 96 | -------------------------------------------------------------------------------- /template/structure/src/router/interceptors/role.js: -------------------------------------------------------------------------------- 1 | import store from '@/store/' 2 | 3 | export default function checkRoles(to, from, next) { 4 | const { state } = store 5 | const { requiresRole } = to.meta 6 | 7 | if (!requiresRole) { 8 | next() 9 | } else { 10 | if (!state.user.id) { 11 | next('/login') 12 | } else { 13 | const { roles = [] } = state.user 14 | if (roles.indexOf(requiresRole) > -1) { 15 | next() 16 | } else { 17 | next('/403') 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /template/structure/src/router/interceptors/user.js: -------------------------------------------------------------------------------- 1 | import store from '@/store/' 2 | 3 | export default function checkUser(to, from, next) { 4 | const { state } = store 5 | 6 | if (to.matched.some(record => record.meta.requiresUser)) { 7 | if (state.user.id) { 8 | next() 9 | } else { 10 | next('/login') 11 | } 12 | } else { 13 | next() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /template/structure/src/services/index.js: -------------------------------------------------------------------------------- 1 | import * as api from '@/api' 2 | 3 | export async function getUser(username) { 4 | return api.getUser(username) 5 | } 6 | 7 | export async function getExampleList(param) { 8 | const TYPES = { 9 | '1': '个人', 10 | '2': '组织' 11 | } 12 | 13 | const STATUS = { 14 | '0': '离线', 15 | '1': '在线' 16 | } 17 | 18 | return api.getExampleList(param).then(data => { 19 | return { 20 | ...data, 21 | list: data.list.map(item => { 22 | return { 23 | ...item, 24 | typeText: TYPES[item.type], 25 | statusText: STATUS[item.status] 26 | } 27 | }) 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /template/structure/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import * as service from '@/services' 5 | 6 | import example from './modules/example' 7 | 8 | Vue.use(Vuex) 9 | 10 | function initialState() { 11 | return { 12 | user: { 13 | id: 0, 14 | name: '', 15 | roles: [] 16 | } 17 | } 18 | } 19 | 20 | export default new Vuex.Store({ 21 | state: initialState(), 22 | mutations: { 23 | get(state, payload) { 24 | state.user = payload.data || {} 25 | } 26 | }, 27 | actions: { 28 | async getUser({ commit }, username) { 29 | const data = await service.getUser(username) 30 | 31 | commit({ 32 | type: 'get', 33 | data 34 | }) 35 | }, 36 | 37 | resetUser({ commit }) { 38 | const { user } = initialState() 39 | commit({ 40 | type: 'get', 41 | data: user 42 | }) 43 | } 44 | }, 45 | 46 | modules: { 47 | example 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /template/structure/src/store/modules/example.js: -------------------------------------------------------------------------------- 1 | import * as service from '@/services' 2 | 3 | export const example = { 4 | namespaced: true, 5 | state: { 6 | loading: true, 7 | exampleList: { 8 | page: { 9 | order: 'descending', 10 | orderBy: 'id', 11 | pageNo: 1, 12 | pageSize: 20, 13 | total: 0 14 | }, 15 | list: [] 16 | } 17 | }, 18 | mutations: { 19 | loading(state, loading) { 20 | state.loading = loading 21 | }, 22 | 23 | search(state, payload) { 24 | state.exampleList = payload.data 25 | } 26 | }, 27 | actions: { 28 | async searchExampleList({ commit }, param) { 29 | commit('loading', true) 30 | 31 | try { 32 | const data = await service.getExampleList(param) 33 | 34 | commit({ 35 | type: 'search', 36 | data 37 | }) 38 | } catch (err) { 39 | throw err 40 | } finally { 41 | commit('loading', false) 42 | } 43 | } 44 | } 45 | } 46 | 47 | export default example 48 | -------------------------------------------------------------------------------- /template/structure/src/styles/app/base/index.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | height: 100%; 5 | 6 | #app { 7 | height: 100%; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /template/structure/src/styles/app/form/index.scss: -------------------------------------------------------------------------------- 1 | .standard-form-container { 2 | .standard-form { 3 | width: 600px; 4 | 5 | .el-radio-group, 6 | .el-checkbox-group { 7 | line-height: 40px; 8 | } 9 | 10 | .el-radio-group .el-radio, 11 | .el-checkbox-group .el-checkbox { 12 | line-height: 40px; 13 | margin-right: 30px; 14 | } 15 | 16 | .el-radio-group .el-radio + .el-radio, 17 | .el-checkbox-group .el-checkbox + .el-checkbox { 18 | margin-left: 0; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /template/structure/src/styles/app/helper/index.scss: -------------------------------------------------------------------------------- 1 | .pull-left { 2 | float: left; 3 | } 4 | 5 | .pull-right { 6 | float: right; 7 | } 8 | 9 | .text-center { 10 | text-align: center; 11 | } 12 | 13 | .bottom-gutter { 14 | margin-bottom: 20px; 15 | } 16 | 17 | .clearfix:before, 18 | .clearfix:after { 19 | display: table; 20 | content: ''; 21 | } 22 | .clearfix:after { 23 | clear: both; 24 | } 25 | -------------------------------------------------------------------------------- /template/structure/src/styles/app/index.scss: -------------------------------------------------------------------------------- 1 | @import './base/index'; 2 | @import './form/index'; 3 | @import './table/index'; 4 | @import './helper/index'; 5 | -------------------------------------------------------------------------------- /template/structure/src/styles/app/table/index.scss: -------------------------------------------------------------------------------- 1 | .standard-table { 2 | .standard-table-filter { 3 | margin-bottom: 20px; 4 | } 5 | 6 | .standard-table-toolbar { 7 | margin-bottom: 20px; 8 | 9 | .standard-table-toolbar__row { 10 | display: flex; 11 | 12 | .standard-table-toolbar__buttons { 13 | flex-grow: 1; 14 | } 15 | 16 | .standard-table-toolbar__pagination { 17 | font-weight: bold; 18 | } 19 | } 20 | } 21 | 22 | .standard-table-list { 23 | .standard-table-list__expand { 24 | font-size: 0; 25 | 26 | label { 27 | width: 120px; 28 | color: #99a9bf; 29 | } 30 | .el-form-item { 31 | margin-right: 0; 32 | margin-bottom: 0; 33 | width: 33%; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /template/structure/src/styles/theme/element-variables.scss: -------------------------------------------------------------------------------- 1 | $--color-primary: #409eff; 2 | 3 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 4 | 5 | @import '~element-ui/packages/theme-chalk/src/index'; 6 | -------------------------------------------------------------------------------- /template/structure/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import { RESPONSE_STATUS } from '@/constants' 2 | 3 | export function delay(t = 500, resolved = true) { 4 | return new Promise((resolve, reject) => { 5 | setTimeout(() => { 6 | resolved ? resolve() : reject(new Error('delay error')) 7 | }, t) 8 | }) 9 | } 10 | 11 | export function checkCode(response) { 12 | if (RESPONSE_STATUS.SUCCESS === response.status) { 13 | return response.data 14 | } else { 15 | const { status = 0, message = 'request failed' } = response 16 | throw new Error(`Error ${status}: ${message}`) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /template/structure/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 66 | 67 | 72 | -------------------------------------------------------------------------------- /template/structure/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 117 | 118 | 123 | -------------------------------------------------------------------------------- /template/structure/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 64 | 65 | 124 | -------------------------------------------------------------------------------- /template/structure/src/views/example/AdminAuthorized.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /template/structure/src/views/example/Form.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 74 | 75 | 122 | -------------------------------------------------------------------------------- /template/structure/src/views/example/Table.vue: -------------------------------------------------------------------------------- 1 | 133 | 134 | 136 | 137 | 230 | -------------------------------------------------------------------------------- /template/structure/src/views/example/config.js: -------------------------------------------------------------------------------- 1 | export const rules = { 2 | name: [ 3 | { required: true, message: '请输入名称', trigger: 'blur' }, 4 | { min: 5, max: 20, message: '长度在 5 到 20 个字符', trigger: 'blur' } 5 | ], 6 | region: [{ required: true, message: '请选择地区', trigger: 'change' }], 7 | date: [ 8 | { 9 | type: 'date', 10 | required: true, 11 | message: '请选择日期', 12 | trigger: 'change' 13 | } 14 | ], 15 | time: [ 16 | { 17 | type: 'date', 18 | required: true, 19 | message: '请选择时间', 20 | trigger: 'change' 21 | } 22 | ], 23 | type: [ 24 | { 25 | type: 'array', 26 | required: true, 27 | message: '请至少选择一个类型', 28 | trigger: 'change' 29 | } 30 | ], 31 | status: [{ required: true, message: '请选择状态', trigger: 'change' }], 32 | desc: [{ required: true, message: '请填写详细描述', trigger: 'blur' }] 33 | } 34 | -------------------------------------------------------------------------------- /template/structure/tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import AppFooter from '@/components/layout/AppFooter.vue' 3 | 4 | describe('AppFooter.vue', () => { 5 | it('render app footer', () => { 6 | const wrapper = shallowMount(AppFooter, {}) 7 | expect(wrapper.text()).toMatch('Copyright') 8 | }) 9 | }) 10 | --------------------------------------------------------------------------------