├── public ├── robots.txt ├── favicon.ico ├── img │ └── icons │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── mstile-150x150.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── msapplication-icon-144x144.png │ │ └── safari-pinned-tab.svg ├── manifest.json └── index.html ├── .browserslistrc ├── babel.config.js ├── .github └── demo.gif ├── tests └── unit │ ├── .eslintrc.js │ └── example.spec.js ├── postcss.config.js ├── src ├── assets │ └── logo.png ├── constants │ └── index.js ├── styles │ ├── app │ │ ├── index.scss │ │ ├── base │ │ │ └── index.scss │ │ ├── helper │ │ │ └── index.scss │ │ ├── form │ │ │ └── index.scss │ │ └── table │ │ │ └── index.scss │ └── theme │ │ └── element-variables.scss ├── registerElementUI.js ├── components │ ├── errors │ │ ├── index.scss │ │ ├── index.js │ │ ├── 403.vue │ │ ├── 404.vue │ │ └── 500.vue │ └── layout │ │ ├── BlankLayout.vue │ │ ├── AppFooter.vue │ │ ├── AppLayout.vue │ │ ├── AppBreadcrumb.vue │ │ └── AppHeader.vue ├── App.vue ├── router │ ├── interceptors │ │ ├── user.js │ │ └── role.js │ └── index.js ├── views │ ├── example │ │ ├── AdminAuthorized.vue │ │ ├── config.js │ │ ├── Form.vue │ │ └── Table.vue │ ├── About.vue │ ├── Login.vue │ └── Home.vue ├── main.js ├── utils │ └── request.js ├── services │ └── index.js ├── store │ ├── index.js │ └── modules │ │ └── example.js ├── registerServiceWorker.js └── api │ └── index.js ├── .editorconfig ├── .gitignore ├── .eslintrc.js ├── now.json ├── jest.config.js ├── LICENSE ├── README.md ├── README.zh-CN.md └── package.json /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.github/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/.github/demo.gif -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const RESPONSE_STATUS = { 2 | SUCCESS: '1', 3 | SERVER_ERROR: '1000', 4 | NETWORK_ERROR: '1001' 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/app/index.scss: -------------------------------------------------------------------------------- 1 | @import './base/index'; 2 | @import './form/index'; 3 | @import './table/index'; 4 | @import './helper/index'; 5 | -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codetrial/element-admin/HEAD/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /src/components/errors/403.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/components/errors/404.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/components/layout/BlankLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/errors/500.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/views/example/AdminAuthorized.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /src/components/layout/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element-admin", 3 | "short_name": "element-admin", 4 | "icons": [ 5 | { 6 | "src": "./img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "display": "standalone", 18 | "background_color": "#000000", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element-admin", 3 | "version": 2, 4 | "alias": "element-admin", 5 | "builds": [{ "src": "**", "use": "@now/static" }], 6 | "routes": [ 7 | { "src": "^/js/(.*)", "dest": "/js/$1" }, 8 | { "src": "^/css/(.*)", "dest": "/css/$1" }, 9 | { "src": "^/fonts/(.*)", "dest": "/fonts/$1" }, 10 | { "src": "^/img/(.*)", "dest": "/img/$1" }, 11 | { 12 | "src": "^/(favicon|manifest|precache|service-worker|robots)(.*)", 13 | "dest": "/$1$2" 14 | }, 15 | { "src": ".*", "dest": "/index.html" } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue' 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest' 12 | }, 13 | moduleNameMapper: { 14 | '^@/(.*)$': '/src/$1' 15 | }, 16 | snapshotSerializers: [ 17 | 'jest-serializer-vue' 18 | ], 19 | testMatch: [ 20 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 21 | ], 22 | testURL: 'http://localhost/' 23 | } 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Codetrial Element Admin 9 | 10 | 11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from 'register-service-worker' 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready () { 8 | console.log( 9 | 'App is being served from cache by a service worker.\n' + 10 | 'For more details, visit https://goo.gl/AFskqB' 11 | ) 12 | }, 13 | registered () { 14 | console.log('Service worker has been registered.') 15 | }, 16 | cached () { 17 | console.log('Content has been cached for offline use.') 18 | }, 19 | updatefound () { 20 | console.log('New content is downloading.') 21 | }, 22 | updated () { 23 | console.log('New content is available; please refresh.') 24 | }, 25 | offline () { 26 | console.log('No internet connection found. App is running in offline mode.') 27 | }, 28 | error (error) { 29 | console.error('Error during service worker registration:', error) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Felix Yang 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 | -------------------------------------------------------------------------------- /src/components/layout/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | 56 | -------------------------------------------------------------------------------- /src/components/layout/AppBreadcrumb.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | 23 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Element Admin 2 | 3 | [![License](https://img.shields.io/github/license/codetrial/element-admin.svg)](https://github.com/codetrial/element-admin) 4 | 5 | A dead simple but powerful vue admin with Vue CLI 3 and ElementUI. 6 | 7 | This project is built by a magic vue cli plugin - [@codetrial/vue-cli-plugin-element](https://github.com/codetrial/vue-cli-plugin-element) 8 | 9 | :us: English | [:cn: 简体中文](README.zh-CN.md) 10 | 11 | ## Docs 12 | 13 | [:zap: Live Demo](https://element-admin.now.sh) | [:book: Docs](https://codetrial.github.io/element-admin) 14 | 15 | ## Screen Capture 16 | 17 | ![Screen Capture](.github/demo.gif) 18 | 19 | ## Quick Start 20 | 21 | ### Using Git 22 | 23 | ```bash 24 | # clone this repository 25 | yarn install 26 | yarn run serve 27 | ``` 28 | 29 | ### Using Vue CLI 30 | 31 | ```bash 32 | vue create --preset codetrial/vue-cli-plugin-element your-project 33 | ``` 34 | 35 | ## Core Features 36 | 37 | - :camera: Minimal dependencies 38 | - :tv: Project Structure 39 | - :telephone_receiver: View Layout 40 | - :pager: Data Processing Layer 41 | - :watch: Authorization 42 | - :radio: Error Pages 43 | - :mag_right: List Example 44 | - :ghost: Form Example 45 | 46 | ## Contributing 47 | 48 | Looking forward to your pull requests. 49 | 50 | ## Built With 51 | 52 | - [Vue.js](https://github.com/vuejs/vue) 53 | - [ElementUI](https://github.com/ElemeFE/element) 54 | 55 | ## License 56 | 57 | [MIT](http://opensource.org/licenses/MIT) 58 | 59 | Copyright (c) 2018 - present, Felix Yang 60 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Element Admin 2 | 3 | [![License](https://img.shields.io/github/license/codetrial/element-admin.svg)](https://github.com/codetrial/element-admin) 4 | 5 | 一个简约而不简单的基于 Vue CLI 3 和 ElementUI 的中后台管理系统。 6 | 7 | 此项目由一个不可思议的 vue cli 插件构建 - [@codetrial/vue-cli-plugin-element](https://github.com/codetrial/vue-cli-plugin-element) 8 | 9 | :cn: 简体中文 | [:us: English](README.md) 10 | 11 | ## 文档 12 | 13 | [:zap: 在线示例](https://element-admin.now.sh) | [:book: 参考文档](https://codetrial.github.io/element-admin) 14 | 15 | ## 截屏预览 16 | 17 | ![Screen Capture](.github/demo.gif) 18 | 19 | ## 快速开始 20 | 21 | ### 使用 Git 22 | 23 | ```bash 24 | # 克隆这个仓库 25 | yarn install 26 | yarn run serve 27 | ``` 28 | 29 | ### 使用 Vue CLI 30 | 31 | ```bash 32 | vue create --preset codetrial/vue-cli-plugin-element your-project 33 | ``` 34 | 35 | ## 核心功能 36 | 37 | :camera: **最小依赖**:仅依赖 Vue 官方库及 ElementUI 组件库,未额外引入其它第三方库,为你提供自由发挥的空间。 38 | 39 | :tv: **目录结构**:根据项目实战经验,设计了合理、清晰的目录结构。 40 | 41 | :telephone_receiver: **页面布局**:使用 Vue Router 嵌套路由及 ElementUI 内置组件进行布局。 42 | 43 | :pager: **数据处理**:添加独立的 api 及 service 层,将业务逻辑从组件中抽离。 44 | 45 | :watch: **权限控制**:为路由添加配置式拦截器,默认支持用户登录鉴权及角色鉴权。 46 | 47 | :radio: **列表示例**:一个相对比较完整的列表页示例,包含字段查询(过滤),字段排序,页码跳转,批量操作等等。 48 | 49 | :mag_right: **表单示例**:一个相对比较完整的表单页示例,包含表单校验、提交等操作。 50 | 51 | :ghost: **错误页面**:内置简单的(其实是偷懒) 403、404 及 500 错误页。 52 | 53 | ## 贡献 54 | 55 | 期待你的 `pull requests`。如果你觉得有帮助,还请多多反馈! 56 | 57 | ## 技术栈 58 | 59 | - [Vue.js](https://github.com/vuejs/vue) 60 | - [ElementUI](https://github.com/ElemeFE/element) 61 | 62 | ## 许可 63 | 64 | [MIT](http://opensource.org/licenses/MIT) 65 | 66 | Copyright (c) 2018 - present, Felix Yang 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element-admin", 3 | "version": "1.0.3", 4 | "description": "A dead simple but powerful vue admin with Vue CLI 3 and ElementUI.", 5 | "keywords": [ 6 | "vue", 7 | "vue-cli", 8 | "vue-cli-plugin", 9 | "element", 10 | "element-ui", 11 | "admin", 12 | "dashboard" 13 | ], 14 | "private": true, 15 | "scripts": { 16 | "serve": "vue-cli-service serve", 17 | "build": "vue-cli-service build", 18 | "lint": "vue-cli-service lint", 19 | "test:unit": "vue-cli-service test:unit", 20 | "deploy": "npm run build && now ./dist -A ../now.json && now alias" 21 | }, 22 | "dependencies": { 23 | "element-ui": "^2.4.11", 24 | "register-service-worker": "^1.5.2", 25 | "vue": "^2.5.17", 26 | "vue-router": "^3.0.1", 27 | "vuex": "^3.0.1", 28 | "vuex-router-sync": "^5.0.0" 29 | }, 30 | "devDependencies": { 31 | "@vue/cli-plugin-babel": "^3.2.0", 32 | "@vue/cli-plugin-eslint": "^3.2.0", 33 | "@vue/cli-plugin-pwa": "^3.2.0", 34 | "@vue/cli-plugin-unit-jest": "^3.2.0", 35 | "@vue/cli-service": "^3.2.0", 36 | "@vue/eslint-config-standard": "^4.0.0", 37 | "@vue/test-utils": "^1.0.0-beta.20", 38 | "babel-core": "7.0.0-bridge.0", 39 | "babel-eslint": "^10.0.1", 40 | "babel-jest": "^23.6.0", 41 | "eslint": "^5.8.0", 42 | "eslint-plugin-vue": "^5.0.0-0", 43 | "node-sass": "^4.9.0", 44 | "sass-loader": "^7.0.1", 45 | "vue-template-compiler": "^2.5.17" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/codetrial/element-admin.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/codetrial/element-admin/issues" 53 | }, 54 | "homepage": "https://github.com/codetrial/element-admin#readme", 55 | "author": "felixpy", 56 | "license": "MIT" 57 | } 58 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 66 | 67 | 72 | -------------------------------------------------------------------------------- /src/components/layout/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 57 | 58 | 91 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 64 | 65 | 124 | -------------------------------------------------------------------------------- /src/views/example/Form.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 74 | 75 | 122 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 117 | 118 | 123 | -------------------------------------------------------------------------------- /src/views/example/Table.vue: -------------------------------------------------------------------------------- 1 | 133 | 134 | 136 | 137 | 230 | -------------------------------------------------------------------------------- /public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 150 | --------------------------------------------------------------------------------