├── src
├── views
│ ├── product
│ │ ├── new.vue
│ │ └── index.vue
│ ├── form
│ │ ├── adminList
│ │ │ └── index.vue
│ │ ├── addPerson.vue
│ │ └── index.vue
│ ├── nested
│ │ ├── menu1
│ │ │ ├── index.vue
│ │ │ ├── menu1-3
│ │ │ │ └── index.vue
│ │ │ ├── menu1-2
│ │ │ │ └── index.vue
│ │ │ └── menu1-1
│ │ │ │ └── index.vue
│ │ └── menu2
│ │ │ └── index.vue
│ ├── permission
│ │ ├── page.vue
│ │ ├── components
│ │ │ └── SwitchRoles.vue
│ │ └── directive.vue
│ ├── dashboard
│ │ └── index.vue
│ ├── tree
│ │ └── index.vue
│ ├── table
│ │ └── index.vue
│ ├── 404.vue
│ └── login
│ │ └── index.vue
├── assets
│ ├── earth.jpg
│ └── 404_images
│ │ ├── 404.png
│ │ └── 404_cloud.png
├── layout
│ ├── components
│ │ ├── index.js
│ │ ├── Sidebar
│ │ │ ├── Item.vue
│ │ │ ├── Link.vue
│ │ │ ├── FixiOSBug.js
│ │ │ ├── index.vue
│ │ │ ├── Logo.vue
│ │ │ └── SidebarItem.vue
│ │ ├── AppMain.vue
│ │ └── Navbar.vue
│ ├── mixin
│ │ └── ResizeHandler.js
│ └── index.vue
├── App.vue
├── api
│ ├── table.js
│ └── user.js
├── icons
│ ├── svg
│ │ ├── link.svg
│ │ ├── user.svg
│ │ ├── example.svg
│ │ ├── table.svg
│ │ ├── password.svg
│ │ ├── nested.svg
│ │ ├── eye.svg
│ │ ├── eye-open.svg
│ │ ├── tree.svg
│ │ ├── dashboard.svg
│ │ └── form.svg
│ ├── index.js
│ └── svgo.yml
├── utils
│ ├── get-page-title.js
│ ├── auth.js
│ ├── validate.js
│ ├── permission.js
│ ├── request.js
│ └── index.js
├── directive
│ ├── waves
│ │ ├── index.js
│ │ ├── waves.css
│ │ └── waves.js
│ ├── el-drag-dialog
│ │ ├── index.js
│ │ └── drag.js
│ ├── clipboard
│ │ ├── index.js
│ │ └── clipboard.js
│ ├── permission
│ │ ├── index.js
│ │ └── permission.js
│ ├── el-table
│ │ ├── index.js
│ │ └── adaptive.js
│ └── sticky.js
├── settings.js
├── styles
│ ├── mixin.scss
│ ├── variables.scss
│ ├── element-ui.scss
│ ├── transition.scss
│ ├── index.scss
│ └── sidebar.scss
├── store
│ ├── getters.js
│ ├── modules
│ │ ├── settings.js
│ │ ├── app.js
│ │ ├── permission.js
│ │ ├── user.js
│ │ └── tagsView.js
│ └── index.js
├── main.js
├── components
│ ├── Hamburger
│ │ └── index.vue
│ ├── SvgIcon
│ │ └── index.vue
│ └── Breadcrumb
│ │ └── index.vue
├── permission.js
└── router
│ └── index.js
├── debug.log
├── babel.config.js
├── tests
└── unit
│ ├── .eslintrc.js
│ ├── components
│ ├── Hamburger.spec.js
│ ├── SvgIcon.spec.js
│ └── Breadcrumb.spec.js
│ └── utils
│ ├── validate.spec.js
│ ├── parseTime.spec.js
│ └── formatTime.spec.js
├── public
├── favicon.ico
└── index.html
├── jsconfig.json
├── postcss.config.js
├── mock
├── product.js
├── table.js
├── index.js
├── mock-server.js
└── user.js
├── jest.config.js
├── LICENSE
├── package.json
├── README.md
├── README-zh.md
└── vue.config.js
/src/views/product/new.vue:
--------------------------------------------------------------------------------
1 |
2 | 新增商品
3 |
4 |
--------------------------------------------------------------------------------
/debug.log:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rainnnnyall/huishouplatform/HEAD/debug.log
--------------------------------------------------------------------------------
/src/views/product/index.vue:
--------------------------------------------------------------------------------
1 |
2 | 商品模块
3 |
4 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rainnnnyall/huishouplatform/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/earth.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rainnnnyall/huishouplatform/HEAD/src/assets/earth.jpg
--------------------------------------------------------------------------------
/src/views/form/adminList/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 这是管理员页面
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/404_images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rainnnnyall/huishouplatform/HEAD/src/assets/404_images/404.png
--------------------------------------------------------------------------------
/src/assets/404_images/404_cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rainnnnyall/huishouplatform/HEAD/src/assets/404_images/404_cloud.png
--------------------------------------------------------------------------------
/src/views/nested/menu1/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/layout/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar'
2 | export { default as Sidebar } from './Sidebar'
3 | export { default as AppMain } from './AppMain'
4 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": ["src/*"]
6 | }
7 | },
8 | "exclude": ["node_modules", "dist"]
9 | }
10 |
--------------------------------------------------------------------------------
/src/views/nested/menu1/menu1-3/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/views/form/addPerson.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
新增成员
4 | 用户名
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/views/nested/menu1/menu1-2/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/api/table.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function getList(params) {
4 | return request({
5 | url: '/vue-admin-template/table/list',
6 | method: 'get',
7 | params
8 | })
9 | }
10 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | 'plugins': {
5 | // to edit target browsers: use "browserslist" field in package.json
6 | 'autoprefixer': {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/icons/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/get-page-title.js:
--------------------------------------------------------------------------------
1 | import defaultSettings from '@/settings'
2 |
3 | const title = defaultSettings.title || 'Vue Admin Template'
4 |
5 | export default function getPageTitle(pageTitle) {
6 | if (pageTitle) {
7 | return `${pageTitle} - ${title}`
8 | }
9 | return `${title}`
10 | }
11 |
--------------------------------------------------------------------------------
/src/directive/waves/index.js:
--------------------------------------------------------------------------------
1 | import waves from './waves'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('waves', waves)
5 | }
6 |
7 | if (window.Vue) {
8 | window.waves = waves
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | waves.install = install
13 | export default waves
14 |
--------------------------------------------------------------------------------
/src/directive/el-drag-dialog/index.js:
--------------------------------------------------------------------------------
1 | import drag from './drag'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('el-drag-dialog', drag)
5 | }
6 |
7 | if (window.Vue) {
8 | window['el-drag-dialog'] = drag
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | drag.install = install
13 | export default drag
14 |
--------------------------------------------------------------------------------
/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import SvgIcon from '@/components/SvgIcon'// svg component
3 |
4 | // register globally
5 | Vue.component('svg-icon', SvgIcon)
6 |
7 | const req = require.context('./svg', false, /\.svg$/)
8 | const requireAll = requireContext => requireContext.keys().map(requireContext)
9 | requireAll(req)
10 |
--------------------------------------------------------------------------------
/src/directive/clipboard/index.js:
--------------------------------------------------------------------------------
1 | import Clipboard from './clipboard'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('Clipboard', Clipboard)
5 | }
6 |
7 | if (window.Vue) {
8 | window.clipboard = Clipboard
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | Clipboard.install = install
13 | export default Clipboard
14 |
--------------------------------------------------------------------------------
/src/directive/permission/index.js:
--------------------------------------------------------------------------------
1 | import permission from './permission'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('permission', permission)
5 | }
6 |
7 | if (window.Vue) {
8 | window['permission'] = permission
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | permission.install = install
13 | export default permission
14 |
--------------------------------------------------------------------------------
/src/icons/svgo.yml:
--------------------------------------------------------------------------------
1 | # replace default config
2 |
3 | # multipass: true
4 | # full: true
5 |
6 | plugins:
7 |
8 | # - name
9 | #
10 | # or:
11 | # - name: false
12 | # - name: true
13 | #
14 | # or:
15 | # - name:
16 | # param1: 1
17 | # param2: 2
18 |
19 | - removeAttrs:
20 | attrs:
21 | - 'fill'
22 | - 'fill-rule'
23 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | title: 'Vue Admin Template',
4 |
5 | /**
6 | * @type {boolean} true | false
7 | * @description Whether fix the header
8 | */
9 | fixedHeader: true,
10 |
11 | /**
12 | * @type {boolean} true | false
13 | * @description Whether show the logo in sidebar
14 | */
15 | sidebarLogo: true
16 | }
17 |
--------------------------------------------------------------------------------
/src/directive/el-table/index.js:
--------------------------------------------------------------------------------
1 | import adaptive from './adaptive'
2 |
3 | const install = function(Vue) {
4 | Vue.directive('el-height-adaptive-table', adaptive)
5 | }
6 |
7 | if (window.Vue) {
8 | window['el-height-adaptive-table'] = adaptive
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | adaptive.install = install
13 | export default adaptive
14 |
--------------------------------------------------------------------------------
/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | //设置获取tocken
2 | import Cookies from 'js-cookie'
3 |
4 | const TokenKey = 'vue_admin_template_token'
5 |
6 | export function getToken() {
7 | return Cookies.get(TokenKey)
8 | }
9 |
10 | export function setToken(token) {
11 | return Cookies.set(TokenKey, token)
12 | }
13 |
14 | export function removeToken() {
15 | return Cookies.remove(TokenKey)
16 | }
17 |
--------------------------------------------------------------------------------
/src/icons/svg/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/permission/page.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
--------------------------------------------------------------------------------
/src/icons/svg/example.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * @param {string} path
7 | * @returns {Boolean}
8 | */
9 | export function isExternal(path) {
10 | return /^(https?:|mailto:|tel:)/.test(path)
11 | }
12 |
13 | /**
14 | * @param {string} str
15 | * @returns {Boolean}
16 | */
17 | export function validUsername(str) {
18 | if(str.trim()){
19 | return true
20 | }
21 | // const valid_map = ['admin', 'recyclrer','user']
22 | // return str.trim()>= 0
23 | }
24 |
--------------------------------------------------------------------------------
/src/styles/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin clearfix {
2 | &:after {
3 | content: "";
4 | display: table;
5 | clear: both;
6 | }
7 | }
8 |
9 | @mixin scrollBar {
10 | &::-webkit-scrollbar-track-piece {
11 | background: #d3dce6;
12 | }
13 |
14 | &::-webkit-scrollbar {
15 | width: 6px;
16 | }
17 |
18 | &::-webkit-scrollbar-thumb {
19 | background: #99a9bf;
20 | border-radius: 20px;
21 | }
22 | }
23 |
24 | @mixin relative {
25 | position: relative;
26 | width: 100%;
27 | height: 100%;
28 | }
29 |
--------------------------------------------------------------------------------
/src/api/user.js:
--------------------------------------------------------------------------------
1 | import request from '@/utils/request'
2 |
3 | export function login(data) {
4 | return request({
5 | url: '/vue-admin-template/user/login',
6 | method: 'post',
7 | data
8 | })
9 | }
10 |
11 | export function getInfo(token) {
12 | return request({
13 | url: '/vue-admin-template/user/info',
14 | method: 'get',
15 | params: { token }
16 | })
17 | }
18 |
19 | export function logout() {
20 | return request({
21 | url: '/vue-admin-template/user/logout',
22 | method: 'post'
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | const getters = {
2 | sidebar: state => state.app.sidebar,
3 | device: state => state.app.device,
4 | token: state => state.user.token,
5 | avatar: state => state.user.avatar,
6 | name: state => state.user.name,
7 | introduction: state => state.user.introduction,
8 | roles: state => state.user.roles,
9 | permission_routes: state => state.permission.routes,
10 | visitedViews: state => state.tagsView.visitedViews,
11 | cachedViews: state => state.tagsView.cachedViews
12 | }
13 | export default getters
14 |
--------------------------------------------------------------------------------
/src/icons/svg/table.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/svg/password.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Item.vue:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/directive/permission/permission.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | export default {
4 | inserted(el, binding, vnode) {
5 | const { value } = binding
6 | const roles = store.getters && store.getters.roles
7 |
8 | if (value && value instanceof Array && value.length > 0) {
9 | const permissionRoles = value
10 |
11 | const hasPermission = roles.some(role => {
12 | return permissionRoles.includes(role)
13 | })
14 |
15 | if (!hasPermission) {
16 | el.parentNode && el.parentNode.removeChild(el)
17 | }
18 | } else {
19 | throw new Error(`need roles! Like v-permission="['admin','editor']"`)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= webpackConfig.name %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | //菜单栏logo的配置
2 | import defaultSettings from '@/settings'
3 |
4 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
5 |
6 | const state = {
7 | showSettings: showSettings,
8 | fixedHeader: fixedHeader,
9 | sidebarLogo: sidebarLogo
10 | }
11 |
12 | const mutations = {
13 | CHANGE_SETTING: (state, { key, value }) => {
14 | if (state.hasOwnProperty(key)) {
15 | state[key] = value
16 | }
17 | }
18 | }
19 |
20 | const actions = {
21 | changeSetting({ commit }, data) {
22 | commit('CHANGE_SETTING', data)
23 | }
24 | }
25 |
26 | export default {
27 | namespaced: true,
28 | state,
29 | mutations,
30 | actions
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/mock/product.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 |
3 | const data = Mock.mock({
4 | //造了30条数据
5 | 'products|10': [{
6 | 'id|+1': 1,
7 | name: '@sentence(10, 20)',
8 | 'price|1000-2000': 1234,
9 | 'comment|1-30': [
10 | {
11 | 'id':'@id',
12 | 'content': '@paragraph'
13 | }
14 | ]
15 | }]
16 | })
17 |
18 |
19 | export default [
20 | {
21 | url: '/vue-admin-template/product/list',
22 | type: 'get',
23 | response: config => {
24 | const products = data.products
25 | return {
26 | code: 20000,
27 | data: {
28 | total: products.length,
29 | items: products
30 | }
31 | }
32 | }
33 | }
34 | ]
35 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // sidebar
2 | $menuText:#bfcbd9;
3 | $menuActiveText:#409EFF;
4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
5 |
6 | $menuBg:#304156;
7 | $menuHover:#263445;
8 |
9 | $subMenuBg:#1f2d3d;
10 | $subMenuHover:#001528;
11 |
12 | $sideBarWidth: 210px;
13 |
14 | // the :export directive is the magic sauce for webpack
15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
16 | :export {
17 | menuText: $menuText;
18 | menuActiveText: $menuActiveText;
19 | subMenuActiveText: $subMenuActiveText;
20 | menuBg: $menuBg;
21 | menuHover: $menuHover;
22 | subMenuBg: $subMenuBg;
23 | subMenuHover: $subMenuHover;
24 | sideBarWidth: $sideBarWidth;
25 | }
26 |
--------------------------------------------------------------------------------
/tests/unit/components/Hamburger.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import Hamburger from '@/components/Hamburger/index.vue'
3 | describe('Hamburger.vue', () => {
4 | it('toggle click', () => {
5 | const wrapper = shallowMount(Hamburger)
6 | const mockFn = jest.fn()
7 | wrapper.vm.$on('toggleClick', mockFn)
8 | wrapper.find('.hamburger').trigger('click')
9 | expect(mockFn).toBeCalled()
10 | })
11 | it('prop isActive', () => {
12 | const wrapper = shallowMount(Hamburger)
13 | wrapper.setProps({ isActive: true })
14 | expect(wrapper.contains('.is-active')).toBe(true)
15 | wrapper.setProps({ isActive: false })
16 | expect(wrapper.contains('.is-active')).toBe(false)
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/tests/unit/components/SvgIcon.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import SvgIcon from '@/components/SvgIcon/index.vue'
3 | describe('SvgIcon.vue', () => {
4 | it('iconClass', () => {
5 | const wrapper = shallowMount(SvgIcon, {
6 | propsData: {
7 | iconClass: 'test'
8 | }
9 | })
10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test')
11 | })
12 | it('className', () => {
13 | const wrapper = shallowMount(SvgIcon, {
14 | propsData: {
15 | iconClass: 'test'
16 | }
17 | })
18 | expect(wrapper.classes().length).toBe(1)
19 | wrapper.setProps({ className: 'test' })
20 | expect(wrapper.classes().includes('test')).toBe(true)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Link.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
37 |
--------------------------------------------------------------------------------
/src/utils/permission.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | /**
4 | * 检测当前登录用户是否含有指定的角色,在页面使用,用于根据当前登录用户的角色显示不同的页面
5 | * @param {Array} value
6 | * @returns {Boolean}
7 | * @example see @/views/permission/directive.vue
8 | */
9 | export default function checkPermission(value) {
10 | if (value && value instanceof Array && value.length > 0) {
11 | const roles = store.getters && store.getters.roles
12 | const permissionRoles = value
13 | const hasPermission = roles.some(role => {
14 | return permissionRoles.includes(role)
15 | })
16 |
17 | if (!hasPermission) {
18 | return false
19 | }
20 | return true
21 | } else {
22 | console.error(`need roles! Like v-permission="['admin','editor']"`)
23 | return false
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/FixiOSBug.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | device() {
4 | return this.$store.state.app.device
5 | }
6 | },
7 | mounted() {
8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug
9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135
10 | this.fixBugIniOS()
11 | },
12 | methods: {
13 | fixBugIniOS() {
14 | const $subMenu = this.$refs.subMenu
15 | if ($subMenu) {
16 | const handleMouseleave = $subMenu.handleMouseleave
17 | $subMenu.handleMouseleave = (e) => {
18 | if (this.device === 'mobile') {
19 | return
20 | }
21 | handleMouseleave(e)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/views/permission/components/SwitchRoles.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Your roles: {{ roles }}
5 |
6 | Switch roles:
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
33 |
--------------------------------------------------------------------------------
/tests/unit/utils/validate.spec.js:
--------------------------------------------------------------------------------
1 | import { validUsername, isExternal } from '@/utils/validate.js'
2 |
3 | describe('Utils:validate', () => {
4 | it('validUsername', () => {
5 | expect(validUsername('admin')).toBe(true)
6 | expect(validUsername('editor')).toBe(true)
7 | expect(validUsername('xxxx')).toBe(false)
8 | })
9 | it('isExternal', () => {
10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)
13 | expect(isExternal('/dashboard')).toBe(false)
14 | expect(isExternal('./dashboard')).toBe(false)
15 | expect(isExternal('dashboard')).toBe(false)
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
19 |
20 |
32 |
33 |
40 |
--------------------------------------------------------------------------------
/src/icons/svg/nested.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
点我获取数据
4 |
name: {{ name }}
5 |
6 |
7 |
8 |
26 |
27 |
38 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
3 | transform: {
4 | '^.+\\.vue$': 'vue-jest',
5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
6 | 'jest-transform-stub',
7 | '^.+\\.jsx?$': 'babel-jest'
8 | },
9 | moduleNameMapper: {
10 | '^@/(.*)$': '/src/$1'
11 | },
12 | snapshotSerializers: ['jest-serializer-vue'],
13 | testMatch: [
14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
15 | ],
16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
17 | coverageDirectory: '/tests/unit/coverage',
18 | // 'collectCoverage': true,
19 | 'coverageReporters': [
20 | 'lcov',
21 | 'text-summary'
22 | ],
23 | testURL: 'http://localhost/'
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/element-ui.scss:
--------------------------------------------------------------------------------
1 | // cover some element-ui styles
2 |
3 | .el-breadcrumb__inner,
4 | .el-breadcrumb__inner a {
5 | font-weight: 400 !important;
6 | }
7 |
8 | .el-upload {
9 | input[type="file"] {
10 | display: none !important;
11 | }
12 | }
13 |
14 | .el-upload__input {
15 | display: none;
16 | }
17 |
18 |
19 | // to fixed https://github.com/ElemeFE/element/issues/2461
20 | .el-dialog {
21 | transform: none;
22 | left: 0;
23 | position: relative;
24 | margin: 0 auto;
25 | }
26 |
27 | // refine element ui upload
28 | .upload-container {
29 | .el-upload {
30 | width: 100%;
31 |
32 | .el-upload-dragger {
33 | width: 100%;
34 | height: 200px;
35 | }
36 | }
37 | }
38 |
39 | // dropdown
40 | .el-dropdown-menu {
41 | a {
42 | display: block
43 | }
44 | }
45 |
46 | // to fix el-date-picker css style
47 | .el-range-separator {
48 | box-sizing: content-box;
49 | }
50 |
--------------------------------------------------------------------------------
/mock/table.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 |
3 | const data = Mock.mock({
4 | //造了30条数据
5 | 'items|10': [{
6 | id: '@integer(1,100)',
7 | title: '@cword(2,5)',
8 | num: '@integer(1,100)',
9 | price: '@integer(300, 5000)',
10 | display_time: '@datetime'
11 | }]
12 | })
13 |
14 | export default [
15 | {
16 | url: '/vue-admin-template/table/list',
17 | type: 'get',
18 | response: config => {
19 | const items = data.items
20 | return {
21 | code: 20000,
22 | data: {
23 | total: items.length,
24 | items: items
25 | }
26 | }
27 | }
28 | },{
29 | url: '/vue-admin-template/table/tese2',
30 | type: 'get',
31 | response: config => {
32 | const items = data.items
33 | return {
34 | code: 20000,
35 | data: {
36 | items: [{name:'小明'},{age:12}]
37 | }
38 | }
39 | }
40 | }
41 | ]
42 |
--------------------------------------------------------------------------------
/src/icons/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/directive/waves/waves.css:
--------------------------------------------------------------------------------
1 | .waves-ripple {
2 | position: absolute;
3 | border-radius: 100%;
4 | background-color: rgba(0, 0, 0, 0.15);
5 | background-clip: padding-box;
6 | pointer-events: none;
7 | -webkit-user-select: none;
8 | -moz-user-select: none;
9 | -ms-user-select: none;
10 | user-select: none;
11 | -webkit-transform: scale(0);
12 | -ms-transform: scale(0);
13 | transform: scale(0);
14 | opacity: 1;
15 | }
16 |
17 | .waves-ripple.z-active {
18 | opacity: 0;
19 | -webkit-transform: scale(2);
20 | -ms-transform: scale(2);
21 | transform: scale(2);
22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out;
25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
26 | }
--------------------------------------------------------------------------------
/src/styles/transition.scss:
--------------------------------------------------------------------------------
1 | // global transition css
2 |
3 | /* fade */
4 | .fade-enter-active,
5 | .fade-leave-active {
6 | transition: opacity 0.28s;
7 | }
8 |
9 | .fade-enter,
10 | .fade-leave-active {
11 | opacity: 0;
12 | }
13 |
14 | /* fade-transform */
15 | .fade-transform-leave-active,
16 | .fade-transform-enter-active {
17 | transition: all .5s;
18 | }
19 |
20 | .fade-transform-enter {
21 | opacity: 0;
22 | transform: translateX(-30px);
23 | }
24 |
25 | .fade-transform-leave-to {
26 | opacity: 0;
27 | transform: translateX(30px);
28 | }
29 |
30 | /* breadcrumb transition */
31 | .breadcrumb-enter-active,
32 | .breadcrumb-leave-active {
33 | transition: all .5s;
34 | }
35 |
36 | .breadcrumb-enter,
37 | .breadcrumb-leave-active {
38 | opacity: 0;
39 | transform: translateX(20px);
40 | }
41 |
42 | .breadcrumb-move {
43 | transition: all .5s;
44 | }
45 |
46 | .breadcrumb-leave-active {
47 | position: absolute;
48 | }
49 |
--------------------------------------------------------------------------------
/tests/unit/utils/parseTime.spec.js:
--------------------------------------------------------------------------------
1 | import { parseTime } from '@/utils/index.js'
2 |
3 | describe('Utils:parseTime', () => {
4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5 | it('timestamp', () => {
6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01')
7 | })
8 | it('ten digits timestamp', () => {
9 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
10 | })
11 | it('new Date', () => {
12 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
13 | })
14 | it('format', () => {
15 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
16 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
17 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
18 | })
19 | it('get the day of the week', () => {
20 | expect(parseTime(d, '{a}')).toBe('五') // 星期五
21 | })
22 | it('get the day of the week', () => {
23 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
24 | })
25 | it('empty argument', () => {
26 | expect(parseTime()).toBeNull()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-present PanJiaChen
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 |
--------------------------------------------------------------------------------
/tests/unit/utils/formatTime.spec.js:
--------------------------------------------------------------------------------
1 | import { formatTime } from '@/utils/index.js'
2 |
3 | describe('Utils:formatTime', () => {
4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
5 | const retrofit = 5 * 1000
6 |
7 | it('ten digits timestamp', () => {
8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
9 | })
10 | it('test now', () => {
11 | expect(formatTime(+new Date() - 1)).toBe('刚刚')
12 | })
13 | it('less two minute', () => {
14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
15 | })
16 | it('less two hour', () => {
17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
18 | })
19 | it('less one day', () => {
20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
21 | })
22 | it('more than one day', () => {
23 | expect(formatTime(d)).toBe('7月13日17时54分')
24 | })
25 | it('format', () => {
26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './variables.scss';
2 | @import './mixin.scss';
3 | @import './transition.scss';
4 | @import './element-ui.scss';
5 | @import './sidebar.scss';
6 |
7 | body {
8 | height: 100%;
9 | -moz-osx-font-smoothing: grayscale;
10 | -webkit-font-smoothing: antialiased;
11 | text-rendering: optimizeLegibility;
12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
13 | }
14 |
15 | label {
16 | font-weight: 700;
17 | }
18 |
19 | html {
20 | height: 100%;
21 | box-sizing: border-box;
22 | }
23 |
24 | #app {
25 | height: 100%;
26 | }
27 |
28 | *,
29 | *:before,
30 | *:after {
31 | box-sizing: inherit;
32 | }
33 |
34 | a:focus,
35 | a:active {
36 | outline: none;
37 | }
38 |
39 | a,
40 | a:focus,
41 | a:hover {
42 | cursor: pointer;
43 | color: inherit;
44 | text-decoration: none;
45 | }
46 |
47 | div:focus {
48 | outline: none;
49 | }
50 |
51 | .clearfix {
52 | &:after {
53 | visibility: hidden;
54 | display: block;
55 | font-size: 0;
56 | content: " ";
57 | clear: both;
58 | height: 0;
59 | }
60 | }
61 |
62 | // main-container global css
63 | .app-container {
64 | padding: 20px;
65 | }
66 |
--------------------------------------------------------------------------------
/src/icons/svg/eye-open.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets
4 |
5 | import ElementUI from 'element-ui'
6 | import 'element-ui/lib/theme-chalk/index.css'
7 | import locale from 'element-ui/lib/locale/lang/en' // lang i18n
8 |
9 | import '@/styles/index.scss' // global css
10 |
11 | import App from './App'
12 | import store from './store'
13 | import router from './router'
14 |
15 | import '@/icons' // icon
16 | import '@/permission' // permission control
17 |
18 | // import Vue from 'vue'
19 | // import Vuex from 'vuex'
20 |
21 |
22 | /**
23 | * If you don't want to use mock-server
24 | * you want to use MockJs for mock api
25 | * you can execute: mockXHR()
26 | *
27 | * Currently MockJs will be used in the production environment,
28 | * please remove it before going online ! ! !
29 | */
30 | if (process.env.NODE_ENV === 'production') {
31 | const { mockXHR } = require('../mock')
32 | mockXHR()
33 | }
34 |
35 | // set ElementUI lang to EN
36 | Vue.use(ElementUI, { locale })
37 | // 如果想要中文版 element-ui,按如下方式声明
38 | // Vue.use(ElementUI)
39 |
40 | Vue.config.productionTip = false
41 |
42 | new Vue({
43 | el: '#app',
44 | router,
45 | store,
46 | render: h => h(App)
47 | })
48 |
49 | // Vue.use(Vuex)
50 |
--------------------------------------------------------------------------------
/src/store/modules/app.js:
--------------------------------------------------------------------------------
1 | //记录slideBar的开关
2 | import Cookies from 'js-cookie'
3 |
4 | const state = {
5 | sidebar: {
6 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
7 | withoutAnimation: false
8 | },
9 | device: 'desktop'
10 | }
11 |
12 | const mutations = {
13 | TOGGLE_SIDEBAR: state => {
14 | state.sidebar.opened = !state.sidebar.opened
15 | state.sidebar.withoutAnimation = false
16 | if (state.sidebar.opened) {
17 | Cookies.set('sidebarStatus', 1)
18 | } else {
19 | Cookies.set('sidebarStatus', 0)
20 | }
21 | },
22 | CLOSE_SIDEBAR: (state, withoutAnimation) => {
23 | Cookies.set('sidebarStatus', 0)
24 | state.sidebar.opened = false
25 | state.sidebar.withoutAnimation = withoutAnimation
26 | },
27 | TOGGLE_DEVICE: (state, device) => {
28 | state.device = device
29 | }
30 | }
31 |
32 | const actions = {
33 | toggleSideBar({ commit }) {
34 | commit('TOGGLE_SIDEBAR')
35 | },
36 | closeSideBar({ commit }, { withoutAnimation }) {
37 | commit('CLOSE_SIDEBAR', withoutAnimation)
38 | },
39 | toggleDevice({ commit }, device) {
40 | commit('TOGGLE_DEVICE', device)
41 | }
42 | }
43 |
44 | export default {
45 | namespaced: true,
46 | state,
47 | mutations,
48 | actions
49 | }
50 |
--------------------------------------------------------------------------------
/src/directive/el-table/adaptive.js:
--------------------------------------------------------------------------------
1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
2 |
3 | /**
4 | * How to use
5 | * ...
6 | * el-table height is must be set
7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page.
8 | */
9 |
10 | const doResize = (el, binding, vnode) => {
11 | const { componentInstance: $table } = vnode
12 |
13 | const { value } = binding
14 |
15 | if (!$table.height) {
16 | throw new Error(`el-$table must set the height. Such as height='100px'`)
17 | }
18 | const bottomOffset = (value && value.bottomOffset) || 30
19 |
20 | if (!$table) return
21 |
22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
23 | $table.layout.setHeight(height)
24 | $table.doLayout()
25 | }
26 |
27 | export default {
28 | bind(el, binding, vnode) {
29 | el.resizeListener = () => {
30 | doResize(el, binding, vnode)
31 | }
32 | // parameter 1 is must be "Element" type
33 | addResizeListener(window.document.body, el.resizeListener)
34 | },
35 | inserted(el, binding, vnode) {
36 | doResize(el, binding, vnode)
37 | },
38 | unbind(el) {
39 | removeResizeListener(window.document.body, el.resizeListener)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import getters from './getters'
4 | import app from './modules/app'
5 | import settings from './modules/settings'
6 | import user from './modules/user'
7 | import permission from './modules/permission'
8 | import tagsView from './modules/tagsView'
9 |
10 | Vue.use(Vuex)
11 |
12 | const store = new Vuex.Store({
13 | state: {
14 | login: [{
15 | username: 'admin',
16 | password: '123456',
17 | id: 0
18 | },
19 | {
20 | username: 'recyclrer',
21 | password: '123456',
22 | id: 1
23 | },
24 | {
25 | username: 'user',
26 | password: '123456',
27 | id: 2
28 | }]
29 | },
30 | modules: {
31 | app,
32 | settings,
33 | user,
34 | permission,
35 | tagsView
36 | },
37 | mutations: {
38 | newPassword(state, obj) {
39 | console.log(state, obj, '传递数据')
40 | var arr = store.state.login
41 | console.log(store.state.login, 'vuex数据')
42 | for (var i in arr) {
43 | if (arr[i].username == obj.username) {
44 | arr[i].password = obj.password
45 | }
46 | }
47 | },
48 | addAdmin(state, obj) {
49 | store.state.login.push(obj)
50 | console.log(store.state.login)
51 | },
52 | },
53 |
54 | getters
55 | })
56 |
57 | export default store
58 |
--------------------------------------------------------------------------------
/src/layout/mixin/ResizeHandler.js:
--------------------------------------------------------------------------------
1 | import store from '@/store'
2 |
3 | const { body } = document
4 | const WIDTH = 992 // refer to Bootstrap's responsive design
5 |
6 | export default {
7 | watch: {
8 | $route(route) {
9 | if (this.device === 'mobile' && this.sidebar.opened) {
10 | store.dispatch('app/closeSideBar', { withoutAnimation: false })
11 | }
12 | }
13 | },
14 | beforeMount() {
15 | window.addEventListener('resize', this.$_resizeHandler)
16 | },
17 | beforeDestroy() {
18 | window.removeEventListener('resize', this.$_resizeHandler)
19 | },
20 | mounted() {
21 | const isMobile = this.$_isMobile()
22 | if (isMobile) {
23 | store.dispatch('app/toggleDevice', 'mobile')
24 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
25 | }
26 | },
27 | methods: {
28 | // use $_ for mixins properties
29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
30 | $_isMobile() {
31 | const rect = body.getBoundingClientRect()
32 | return rect.width - 1 < WIDTH
33 | },
34 | $_resizeHandler() {
35 | if (!document.hidden) {
36 | const isMobile = this.$_isMobile()
37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
38 |
39 | if (isMobile) {
40 | store.dispatch('app/closeSideBar', { withoutAnimation: true })
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
47 |
48 |
63 |
--------------------------------------------------------------------------------
/src/store/modules/permission.js:
--------------------------------------------------------------------------------
1 | //用户路由菜单
2 | import { asyncRoutes, constantRoutes } from '@/router'
3 |
4 | /**
5 | * Use meta.role to determine if the current user has permission
6 | * 判断该用户对应的角色有没有指定的路由
7 | * @param roles
8 | * @param route
9 | */
10 | function hasPermission(roles, route) {
11 | if (route.meta && route.meta.roles) {
12 | return roles.some(role => route.meta.roles.includes(role))
13 | } else {
14 | return true
15 | }
16 | }
17 |
18 | /**
19 | * Filter asynchronous routing tables by recursion
20 | * 根据用户对应的角色过滤对应的路由
21 | * @param routes asyncRoutes
22 | * @param roles
23 | */
24 | export function filterAsyncRoutes(routes, roles) {
25 | const res = []
26 |
27 | routes.forEach(route => {
28 | const tmp = { ...route }
29 | if (hasPermission(roles, tmp)) {
30 | if (tmp.children) {
31 | tmp.children = filterAsyncRoutes(tmp.children, roles)
32 | }
33 | res.push(tmp)
34 | }
35 | })
36 |
37 | return res
38 | }
39 |
40 | const state = {
41 | routes: [],
42 | addRoutes: []
43 | }
44 |
45 | const mutations = {
46 | // 将用户对应的路由信息保存到vuex以便页面显示
47 | SET_ROUTES: (state, routes) => {
48 | state.addRoutes = routes
49 | state.routes = constantRoutes.concat(routes)
50 | }
51 | }
52 |
53 | const actions = {
54 | //根据用户角色生成路由的方法
55 | generateRoutes({ commit }, roles) {
56 | return new Promise(resolve => {
57 | let accessedRoutes
58 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
59 | commit('SET_ROUTES', accessedRoutes)
60 | resolve(accessedRoutes)
61 | })
62 | }
63 | }
64 |
65 |
66 | export default {
67 | namespaced: true,
68 | state,
69 | mutations,
70 | actions
71 | }
72 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
59 |
--------------------------------------------------------------------------------
/src/directive/clipboard/clipboard.js:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/Inndy/vue-clipboard2
2 | const Clipboard = require('clipboard')
3 | if (!Clipboard) {
4 | throw new Error('you should npm install `clipboard` --save at first ')
5 | }
6 |
7 | export default {
8 | bind(el, binding) {
9 | if (binding.arg === 'success') {
10 | el._v_clipboard_success = binding.value
11 | } else if (binding.arg === 'error') {
12 | el._v_clipboard_error = binding.value
13 | } else {
14 | const clipboard = new Clipboard(el, {
15 | text() { return binding.value },
16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
17 | })
18 | clipboard.on('success', e => {
19 | const callback = el._v_clipboard_success
20 | callback && callback(e) // eslint-disable-line
21 | })
22 | clipboard.on('error', e => {
23 | const callback = el._v_clipboard_error
24 | callback && callback(e) // eslint-disable-line
25 | })
26 | el._v_clipboard = clipboard
27 | }
28 | },
29 | update(el, binding) {
30 | if (binding.arg === 'success') {
31 | el._v_clipboard_success = binding.value
32 | } else if (binding.arg === 'error') {
33 | el._v_clipboard_error = binding.value
34 | } else {
35 | el._v_clipboard.text = function() { return binding.value }
36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
37 | }
38 | },
39 | unbind(el, binding) {
40 | if (binding.arg === 'success') {
41 | delete el._v_clipboard_success
42 | } else if (binding.arg === 'error') {
43 | delete el._v_clipboard_error
44 | } else {
45 | el._v_clipboard.destroy()
46 | delete el._v_clipboard
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/icons/svg/tree.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mock/index.js:
--------------------------------------------------------------------------------
1 | import Mock from 'mockjs'
2 | import { param2Obj } from '../src/utils'
3 |
4 | import user from './user'
5 | import table from './table'
6 | import product from './product'
7 |
8 | const mocks = [
9 | ...user,
10 | ...table,
11 | ...product
12 | ]
13 |
14 | // for front mock
15 | // please use it cautiously, it will redefine XMLHttpRequest,
16 | // which will cause many of your third-party libraries to be invalidated(like progress event).
17 | export function mockXHR() {
18 | // mock patch
19 | // https://github.com/nuysoft/Mock/issues/300
20 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
21 | Mock.XHR.prototype.send = function() {
22 | if (this.custom.xhr) {
23 | this.custom.xhr.withCredentials = this.withCredentials || false
24 |
25 | if (this.responseType) {
26 | this.custom.xhr.responseType = this.responseType
27 | }
28 | }
29 | this.proxy_send(...arguments)
30 | }
31 |
32 | function XHR2ExpressReqWrap(respond) {
33 | return function(options) {
34 | let result = null
35 | if (respond instanceof Function) {
36 | const { body, type, url } = options
37 | // https://expressjs.com/en/4x/api.html#req
38 | result = respond({
39 | method: type,
40 | body: JSON.parse(body),
41 | query: param2Obj(url)
42 | })
43 | } else {
44 | result = respond
45 | }
46 | return Mock.mock(result)
47 | }
48 | }
49 |
50 | for (const i of mocks) {
51 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
52 | }
53 | }
54 |
55 | // for mock server
56 | const responseFake = (url, type, respond) => {
57 | return {
58 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
59 | type: type || 'get',
60 | response(req, res) {
61 | console.log('request invoke:' + req.path)
62 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
63 | }
64 | }
65 | }
66 |
67 | export default mocks.map(route => {
68 | return responseFake(route.url, route.type, route.response)
69 | })
70 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
33 |
34 |
83 |
--------------------------------------------------------------------------------
/mock/mock-server.js:
--------------------------------------------------------------------------------
1 | const chokidar = require('chokidar')
2 | const bodyParser = require('body-parser')
3 | const chalk = require('chalk')
4 | const path = require('path')
5 |
6 | const mockDir = path.join(process.cwd(), 'mock')
7 |
8 | function registerRoutes(app) {
9 | let mockLastIndex
10 | const { default: mocks } = require('./index.js')
11 | for (const mock of mocks) {
12 | app[mock.type](mock.url, mock.response)
13 | mockLastIndex = app._router.stack.length
14 | }
15 | const mockRoutesLength = Object.keys(mocks).length
16 | return {
17 | mockRoutesLength: mockRoutesLength,
18 | mockStartIndex: mockLastIndex - mockRoutesLength
19 | }
20 | }
21 |
22 | function unregisterRoutes() {
23 | Object.keys(require.cache).forEach(i => {
24 | if (i.includes(mockDir)) {
25 | delete require.cache[require.resolve(i)]
26 | }
27 | })
28 | }
29 |
30 | module.exports = app => {
31 | // es6 polyfill
32 | require('@babel/register')
33 |
34 | // parse app.body
35 | // https://expressjs.com/en/4x/api.html#req.body
36 | app.use(bodyParser.json())
37 | app.use(bodyParser.urlencoded({
38 | extended: true
39 | }))
40 |
41 | const mockRoutes = registerRoutes(app)
42 | var mockRoutesLength = mockRoutes.mockRoutesLength
43 | var mockStartIndex = mockRoutes.mockStartIndex
44 |
45 | // watch files, hot reload mock server
46 | chokidar.watch(mockDir, {
47 | ignored: /mock-server/,
48 | ignoreInitial: true
49 | }).on('all', (event, path) => {
50 | if (event === 'change' || event === 'add') {
51 | try {
52 | // remove mock routes stack
53 | app._router.stack.splice(mockStartIndex, mockRoutesLength)
54 |
55 | // clear routes cache
56 | unregisterRoutes()
57 |
58 | const mockRoutes = registerRoutes(app)
59 | mockRoutesLength = mockRoutes.mockRoutesLength
60 | mockStartIndex = mockRoutes.mockStartIndex
61 |
62 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
63 | } catch (error) {
64 | console.log(chalk.redBright(error))
65 | }
66 | }
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-admin-template",
3 | "version": "4.2.1",
4 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
5 | "author": "Pan ",
6 | "license": "MIT",
7 | "scripts": {
8 | "dev": "vue-cli-service serve",
9 | "build:prod": "vue-cli-service build",
10 | "build:stage": "vue-cli-service build --mode staging",
11 | "preview": "node build/index.js --preview",
12 | "lint": "eslint --ext .js,.vue src",
13 | "test:unit": "jest --clearCache && vue-cli-service test:unit",
14 | "test:ci": "npm run lint && npm run test:unit",
15 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
16 | },
17 | "dependencies": {
18 | "axios": "^0.21.1",
19 | "element-ui": "2.13.0",
20 | "js-cookie": "2.2.0",
21 | "node-sass": "^4.14.1",
22 | "normalize.css": "7.0.0",
23 | "nprogress": "0.2.0",
24 | "path-to-regexp": "2.4.0",
25 | "vue": "2.6.10",
26 | "vue-router": "3.0.6",
27 | "vuex": "3.1.0"
28 | },
29 | "devDependencies": {
30 | "@babel/core": "7.0.0",
31 | "@babel/register": "7.0.0",
32 | "@vue/cli-plugin-babel": "3.6.0",
33 | "@vue/cli-plugin-eslint": "^3.9.1",
34 | "@vue/cli-plugin-unit-jest": "3.6.3",
35 | "@vue/cli-service": "3.6.0",
36 | "@vue/test-utils": "1.0.0-beta.29",
37 | "autoprefixer": "^9.5.1",
38 | "babel-core": "7.0.0-bridge.0",
39 | "babel-eslint": "10.0.1",
40 | "babel-jest": "23.6.0",
41 | "chalk": "2.4.2",
42 | "connect": "3.6.6",
43 | "eslint": "5.15.3",
44 | "eslint-plugin-vue": "5.2.2",
45 | "html-webpack-plugin": "3.2.0",
46 | "mockjs": "1.0.1-beta3",
47 | "runjs": "^4.3.2",
48 | "sass-loader": "^7.1.0",
49 | "script-ext-html-webpack-plugin": "2.1.3",
50 | "script-loader": "0.7.2",
51 | "serve-static": "^1.13.2",
52 | "svg-sprite-loader": "4.1.3",
53 | "svgo": "1.2.2",
54 | "vue-template-compiler": "2.6.10"
55 | },
56 | "engines": {
57 | "node": ">=8.9",
58 | "npm": ">= 3.0.0"
59 | },
60 | "browserslist": [
61 | "> 1%",
62 | "last 2 versions"
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/src/permission.js:
--------------------------------------------------------------------------------
1 | import router from './router'
2 | import store from './store'
3 | import { Message } from 'element-ui'
4 | import NProgress from 'nprogress' // progress bar
5 | import 'nprogress/nprogress.css' // progress bar style
6 | import { getToken } from '@/utils/auth' // get token from cookie
7 | import getPageTitle from '@/utils/get-page-title'
8 |
9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration
10 |
11 | const whiteList = ['/login'] // no redirect whitelist
12 |
13 | // 路由跳转之前,获取当前登录用户信息,动态加载用户角色对应的路由
14 | router.beforeEach(async(to, from, next) => {
15 | // 全局的NProgress开始工作
16 | NProgress.start()
17 | // 设置页面标题
18 | document.title = getPageTitle(to.meta.title)
19 | // 获取用户tocken
20 | const hasToken = getToken()
21 | // 如果用户已经登录了
22 | if (hasToken) {
23 | // 不能再访问登录页面,跳转首页
24 | if (to.path === '/login') {
25 | next({ path: '/' })
26 | NProgress.done()
27 | } else {
28 | const hasGetUserInfo = store.getters.name
29 | // 如果已经获取过用户信息了,直接放行
30 | if (hasGetUserInfo) {
31 | next()
32 | } else {
33 | // 如果没有获取过用户信息,调用vuex的user/getInfo获取用户信息
34 | try {
35 | const { roles } = await store.dispatch('user/getInfo')
36 | // 获取用户对应的权限菜单(这里通过vuex保存用户菜单信息,router.addRoutes没有意义)
37 | const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
38 | router.addRoutes(accessRoutes)
39 | // replace: true 确保导航不会有历史记录
40 | next({ ...to, replace: true })
41 | } catch (error) {
42 | // 如果有错,重置tocken并且来到登录页面
43 | await store.dispatch('user/resetToken')
44 | Message.error(error || 'Has Error')
45 | next(`/login?redirect=${to.path}`)
46 | NProgress.done()
47 | }
48 | }
49 | }
50 | } else {
51 | // 没有tocken
52 | if (whiteList.indexOf(to.path) !== -1) {
53 | // 在白名单中直接放行
54 | next()
55 | } else {
56 | // 不在白名单中来到登录页面
57 | next(`/login?redirect=${to.path}`)
58 | NProgress.done()
59 | }
60 | }
61 | })
62 |
63 | router.afterEach(() => {
64 | // finish progress bar
65 | NProgress.done()
66 | })
67 |
--------------------------------------------------------------------------------
/mock/user.js:
--------------------------------------------------------------------------------
1 |
2 | //后台给前台返回的tocken信息
3 | const tokens = {
4 | admin: {
5 | token: 'admin-token'
6 | },
7 | recyclrer: {
8 | token: 'recyclrer-token'
9 | },
10 | user: {
11 | token: 'user-token'
12 | }
13 | }
14 |
15 | //用户登录后给前台返回的用户信息
16 | const users = {
17 | 'admin-token': {
18 | roles: ['admin'],
19 | introduction: 'I am a super administrator',
20 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
21 | name: 'admin'
22 | },
23 | 'recyclrer-token': {
24 | roles: ['recyclrer'],
25 | introduction: 'I am an recyclrer',
26 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
27 | name: 'recyclrer'
28 | },
29 | 'user-token': {
30 | roles: ['user'],
31 | introduction: 'I am an user',
32 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
33 | name: 'user'
34 | }
35 | }
36 |
37 | export default [
38 | // 用户登录接口
39 | {
40 | url: '/vue-admin-template/user/login',
41 | type: 'post',
42 | response: config => {
43 | const { username } = config.body
44 | const token = tokens[username]
45 |
46 | // mock error
47 | if (!token) {
48 | return {
49 | code: 60204,
50 | message: '用户名密码输入不正确!'
51 | }
52 | }
53 | return {
54 | code: 20000,
55 | data: token
56 | }
57 | }
58 | },
59 |
60 | // 获取用户信息接口
61 | {
62 | url: '/vue-admin-template/user/info\.*',
63 | type: 'get',
64 | response: config => {
65 | const { token } = config.query
66 | const info = users[token]
67 |
68 | // mock error
69 | if (!info) {
70 | return {
71 | code: 50008,
72 | message: 'Login failed, unable to get user details.'
73 | }
74 | }
75 |
76 | return {
77 | code: 20000,
78 | data: info
79 | }
80 | }
81 | },
82 |
83 | // 用户退出登录接口
84 | {
85 | url: '/vue-admin-template/user/logout',
86 | type: 'post',
87 | response: _ => {
88 | return {
89 | code: 20000,
90 | data: 'success'
91 | }
92 | }
93 | }
94 | ]
95 |
--------------------------------------------------------------------------------
/src/icons/svg/dashboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
52 |
53 |
94 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ item.meta.title }}
6 | {{ item.meta.title }}
7 |
8 |
9 |
10 |
11 |
12 |
65 |
66 |
79 |
--------------------------------------------------------------------------------
/src/icons/svg/form.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/directive/waves/waves.js:
--------------------------------------------------------------------------------
1 | import './waves.css'
2 |
3 | const context = '@@wavesContext'
4 |
5 | function handleClick(el, binding) {
6 | function handle(e) {
7 | const customOpts = Object.assign({}, binding.value)
8 | const opts = Object.assign({
9 | ele: el, // 波纹作用元素
10 | type: 'hit', // hit 点击位置扩散 center中心点扩展
11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
12 | },
13 | customOpts
14 | )
15 | const target = opts.ele
16 | if (target) {
17 | target.style.position = 'relative'
18 | target.style.overflow = 'hidden'
19 | const rect = target.getBoundingClientRect()
20 | let ripple = target.querySelector('.waves-ripple')
21 | if (!ripple) {
22 | ripple = document.createElement('span')
23 | ripple.className = 'waves-ripple'
24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
25 | target.appendChild(ripple)
26 | } else {
27 | ripple.className = 'waves-ripple'
28 | }
29 | switch (opts.type) {
30 | case 'center':
31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
33 | break
34 | default:
35 | ripple.style.top =
36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
37 | document.body.scrollTop) + 'px'
38 | ripple.style.left =
39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
40 | document.body.scrollLeft) + 'px'
41 | }
42 | ripple.style.backgroundColor = opts.color
43 | ripple.className = 'waves-ripple z-active'
44 | return false
45 | }
46 | }
47 |
48 | if (!el[context]) {
49 | el[context] = {
50 | removeHandle: handle
51 | }
52 | } else {
53 | el[context].removeHandle = handle
54 | }
55 |
56 | return handle
57 | }
58 |
59 | export default {
60 | bind(el, binding) {
61 | el.addEventListener('click', handleClick(el, binding), false)
62 | },
63 | update(el, binding) {
64 | el.removeEventListener('click', el[context].removeHandle, false)
65 | el.addEventListener('click', handleClick(el, binding), false)
66 | },
67 | unbind(el) {
68 | el.removeEventListener('click', el[context].removeHandle, false)
69 | el[context] = null
70 | delete el[context]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { MessageBox, Message } from 'element-ui'
3 | import store from '@/store'
4 | import { getToken } from '@/utils/auth'
5 |
6 | // create an axios instance
7 | const service = axios.create({
8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
9 | // withCredentials: true, // send cookies when cross-domain requests
10 | timeout: 5000 // request timeout
11 | })
12 |
13 | // 请求的时候携带tocken
14 | service.interceptors.request.use(
15 | config => {
16 | // do something before request is sent
17 |
18 | if (store.getters.token) {
19 | // let each request carry token
20 | // ['X-Token'] is a custom headers key
21 | // please modify it according to the actual situation
22 | config.headers['X-Token'] = getToken()
23 | }
24 | return config
25 | },
26 | error => {
27 | // do something with request error
28 | console.log(error) // for debug
29 | return Promise.reject(error)
30 | }
31 | )
32 |
33 | // response interceptor
34 | service.interceptors.response.use(
35 | /**
36 | * If you want to get http information such as headers or status
37 | * Please return response => response
38 | */
39 |
40 | /**
41 | * Determine the request status by custom code
42 | * Here is just an example
43 | * You can also judge the status by HTTP Status Code
44 | */
45 | response => {
46 | const res = response.data
47 |
48 | // if the custom code is not 20000, it is judged as an error.
49 | if (res.code !== 20000) {
50 | Message({
51 | message: res.message || 'Error',
52 | type: 'error',
53 | duration: 5 * 1000
54 | })
55 |
56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
58 | // to re-login
59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
60 | confirmButtonText: 'Re-Login',
61 | cancelButtonText: 'Cancel',
62 | type: 'warning'
63 | }).then(() => {
64 | store.dispatch('user/resetToken').then(() => {
65 | location.reload()
66 | })
67 | })
68 | }
69 | return Promise.reject(new Error(res.message || 'Error'))
70 | } else {
71 | return res
72 | }
73 | },
74 | error => {
75 | console.log('err' + error) // for debug
76 | Message({
77 | message: error.message,
78 | type: 'error',
79 | duration: 5 * 1000
80 | })
81 | return Promise.reject(error)
82 | }
83 | )
84 |
85 | export default service
86 |
--------------------------------------------------------------------------------
/src/directive/el-drag-dialog/drag.js:
--------------------------------------------------------------------------------
1 | export default {
2 | bind(el, binding, vnode) {
3 | const dialogHeaderEl = el.querySelector('.el-dialog__header')
4 | const dragDom = el.querySelector('.el-dialog')
5 | dialogHeaderEl.style.cssText += ';cursor:move;'
6 | dragDom.style.cssText += ';top:0px;'
7 |
8 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
9 | const getStyle = (function() {
10 | if (window.document.currentStyle) {
11 | return (dom, attr) => dom.currentStyle[attr]
12 | } else {
13 | return (dom, attr) => getComputedStyle(dom, false)[attr]
14 | }
15 | })()
16 |
17 | dialogHeaderEl.onmousedown = (e) => {
18 | // 鼠标按下,计算当前元素距离可视区的距离
19 | const disX = e.clientX - dialogHeaderEl.offsetLeft
20 | const disY = e.clientY - dialogHeaderEl.offsetTop
21 |
22 | const dragDomWidth = dragDom.offsetWidth
23 | const dragDomHeight = dragDom.offsetHeight
24 |
25 | const screenWidth = document.body.clientWidth
26 | const screenHeight = document.body.clientHeight
27 |
28 | const minDragDomLeft = dragDom.offsetLeft
29 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
30 |
31 | const minDragDomTop = dragDom.offsetTop
32 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
33 |
34 | // 获取到的值带px 正则匹配替换
35 | let styL = getStyle(dragDom, 'left')
36 | let styT = getStyle(dragDom, 'top')
37 |
38 | if (styL.includes('%')) {
39 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
40 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
41 | } else {
42 | styL = +styL.replace(/\px/g, '')
43 | styT = +styT.replace(/\px/g, '')
44 | }
45 |
46 | document.onmousemove = function(e) {
47 | // 通过事件委托,计算移动的距离
48 | let left = e.clientX - disX
49 | let top = e.clientY - disY
50 |
51 | // 边界处理
52 | if (-(left) > minDragDomLeft) {
53 | left = -minDragDomLeft
54 | } else if (left > maxDragDomLeft) {
55 | left = maxDragDomLeft
56 | }
57 |
58 | if (-(top) > minDragDomTop) {
59 | top = -minDragDomTop
60 | } else if (top > maxDragDomTop) {
61 | top = maxDragDomTop
62 | }
63 |
64 | // 移动当前元素
65 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
66 |
67 | // emit onDrag event
68 | vnode.child.$emit('dragDialog')
69 | }
70 |
71 | document.onmouseup = function(e) {
72 | document.onmousemove = null
73 | document.onmouseup = null
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PanJiaChen on 16/11/18.
3 | */
4 |
5 | /**
6 | * Parse the time to string
7 | * @param {(Object|string|number)} time
8 | * @param {string} cFormat
9 | * @returns {string | null}
10 | */
11 | export function parseTime(time, cFormat) {
12 | if (arguments.length === 0) {
13 | return null
14 | }
15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
16 | let date
17 | if (typeof time === 'object') {
18 | date = time
19 | } else {
20 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
21 | time = parseInt(time)
22 | }
23 | if ((typeof time === 'number') && (time.toString().length === 10)) {
24 | time = time * 1000
25 | }
26 | date = new Date(time)
27 | }
28 | const formatObj = {
29 | y: date.getFullYear(),
30 | m: date.getMonth() + 1,
31 | d: date.getDate(),
32 | h: date.getHours(),
33 | i: date.getMinutes(),
34 | s: date.getSeconds(),
35 | a: date.getDay()
36 | }
37 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
38 | const value = formatObj[key]
39 | // Note: getDay() returns 0 on Sunday
40 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
41 | return value.toString().padStart(2, '0')
42 | })
43 | return time_str
44 | }
45 |
46 | /**
47 | * @param {number} time
48 | * @param {string} option
49 | * @returns {string}
50 | */
51 | export function formatTime(time, option) {
52 | if (('' + time).length === 10) {
53 | time = parseInt(time) * 1000
54 | } else {
55 | time = +time
56 | }
57 | const d = new Date(time)
58 | const now = Date.now()
59 |
60 | const diff = (now - d) / 1000
61 |
62 | if (diff < 30) {
63 | return '刚刚'
64 | } else if (diff < 3600) {
65 | // less 1 hour
66 | return Math.ceil(diff / 60) + '分钟前'
67 | } else if (diff < 3600 * 24) {
68 | return Math.ceil(diff / 3600) + '小时前'
69 | } else if (diff < 3600 * 24 * 2) {
70 | return '1天前'
71 | }
72 | if (option) {
73 | return parseTime(time, option)
74 | } else {
75 | return (
76 | d.getMonth() +
77 | 1 +
78 | '月' +
79 | d.getDate() +
80 | '日' +
81 | d.getHours() +
82 | '时' +
83 | d.getMinutes() +
84 | '分'
85 | )
86 | }
87 | }
88 |
89 | /**
90 | * @param {string} url
91 | * @returns {Object}
92 | */
93 | export function param2Obj(url) {
94 | const search = url.split('?')[1]
95 | if (!search) {
96 | return {}
97 | }
98 | return JSON.parse(
99 | '{"' +
100 | decodeURIComponent(search)
101 | .replace(/"/g, '\\"')
102 | .replace(/&/g, '","')
103 | .replace(/=/g, '":"')
104 | .replace(/\+/g, ' ') +
105 | '"}'
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/src/directive/sticky.js:
--------------------------------------------------------------------------------
1 | const vueSticky = {}
2 | let listenAction
3 | vueSticky.install = Vue => {
4 | Vue.directive('sticky', {
5 | inserted(el, binding) {
6 | const params = binding.value || {}
7 | const stickyTop = params.stickyTop || 0
8 | const zIndex = params.zIndex || 1000
9 | const elStyle = el.style
10 |
11 | elStyle.position = '-webkit-sticky'
12 | elStyle.position = 'sticky'
13 | // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
14 | // if (~elStyle.position.indexOf('sticky')) {
15 | // elStyle.top = `${stickyTop}px`;
16 | // elStyle.zIndex = zIndex;
17 | // return
18 | // }
19 | const elHeight = el.getBoundingClientRect().height
20 | const elWidth = el.getBoundingClientRect().width
21 | elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
22 |
23 | const parentElm = el.parentNode || document.documentElement
24 | const placeholder = document.createElement('div')
25 | placeholder.style.display = 'none'
26 | placeholder.style.width = `${elWidth}px`
27 | placeholder.style.height = `${elHeight}px`
28 | parentElm.insertBefore(placeholder, el)
29 |
30 | let active = false
31 |
32 | const getScroll = (target, top) => {
33 | const prop = top ? 'pageYOffset' : 'pageXOffset'
34 | const method = top ? 'scrollTop' : 'scrollLeft'
35 | let ret = target[prop]
36 | if (typeof ret !== 'number') {
37 | ret = window.document.documentElement[method]
38 | }
39 | return ret
40 | }
41 |
42 | const sticky = () => {
43 | if (active) {
44 | return
45 | }
46 | if (!elStyle.height) {
47 | elStyle.height = `${el.offsetHeight}px`
48 | }
49 |
50 | elStyle.position = 'fixed'
51 | elStyle.width = `${elWidth}px`
52 | placeholder.style.display = 'inline-block'
53 | active = true
54 | }
55 |
56 | const reset = () => {
57 | if (!active) {
58 | return
59 | }
60 |
61 | elStyle.position = ''
62 | placeholder.style.display = 'none'
63 | active = false
64 | }
65 |
66 | const check = () => {
67 | const scrollTop = getScroll(window, true)
68 | const offsetTop = el.getBoundingClientRect().top
69 | if (offsetTop < stickyTop) {
70 | sticky()
71 | } else {
72 | if (scrollTop < elHeight + stickyTop) {
73 | reset()
74 | }
75 | }
76 | }
77 | listenAction = () => {
78 | check()
79 | }
80 |
81 | window.addEventListener('scroll', listenAction)
82 | },
83 |
84 | unbind() {
85 | window.removeEventListener('scroll', listenAction)
86 | }
87 | })
88 | }
89 |
90 | export default vueSticky
91 |
92 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
96 |
--------------------------------------------------------------------------------
/tests/unit/components/Breadcrumb.spec.js:
--------------------------------------------------------------------------------
1 | import { mount, createLocalVue } from '@vue/test-utils'
2 | import VueRouter from 'vue-router'
3 | import ElementUI from 'element-ui'
4 | import Breadcrumb from '@/components/Breadcrumb/index.vue'
5 |
6 | const localVue = createLocalVue()
7 | localVue.use(VueRouter)
8 | localVue.use(ElementUI)
9 |
10 | const routes = [
11 | {
12 | path: '/',
13 | name: 'home',
14 | children: [{
15 | path: 'dashboard',
16 | name: 'dashboard'
17 | }]
18 | },
19 | {
20 | path: '/menu',
21 | name: 'menu',
22 | children: [{
23 | path: 'menu1',
24 | name: 'menu1',
25 | meta: { title: 'menu1' },
26 | children: [{
27 | path: 'menu1-1',
28 | name: 'menu1-1',
29 | meta: { title: 'menu1-1' }
30 | },
31 | {
32 | path: 'menu1-2',
33 | name: 'menu1-2',
34 | redirect: 'noredirect',
35 | meta: { title: 'menu1-2' },
36 | children: [{
37 | path: 'menu1-2-1',
38 | name: 'menu1-2-1',
39 | meta: { title: 'menu1-2-1' }
40 | },
41 | {
42 | path: 'menu1-2-2',
43 | name: 'menu1-2-2'
44 | }]
45 | }]
46 | }]
47 | }]
48 |
49 | const router = new VueRouter({
50 | routes
51 | })
52 |
53 | describe('Breadcrumb.vue', () => {
54 | const wrapper = mount(Breadcrumb, {
55 | localVue,
56 | router
57 | })
58 | it('dashboard', () => {
59 | router.push('/dashboard')
60 | const len = wrapper.findAll('.el-breadcrumb__inner').length
61 | expect(len).toBe(1)
62 | })
63 | it('normal route', () => {
64 | router.push('/menu/menu1')
65 | const len = wrapper.findAll('.el-breadcrumb__inner').length
66 | expect(len).toBe(2)
67 | })
68 | it('nested route', () => {
69 | router.push('/menu/menu1/menu1-2/menu1-2-1')
70 | const len = wrapper.findAll('.el-breadcrumb__inner').length
71 | expect(len).toBe(4)
72 | })
73 | it('no meta.title', () => {
74 | router.push('/menu/menu1/menu1-2/menu1-2-2')
75 | const len = wrapper.findAll('.el-breadcrumb__inner').length
76 | expect(len).toBe(3)
77 | })
78 | // it('click link', () => {
79 | // router.push('/menu/menu1/menu1-2/menu1-2-2')
80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
81 | // const second = breadcrumbArray.at(1)
82 | // console.log(breadcrumbArray)
83 | // const href = second.find('a').attributes().href
84 | // expect(href).toBe('#/menu/menu1')
85 | // })
86 | // it('noRedirect', () => {
87 | // router.push('/menu/menu1/menu1-2/menu1-2-1')
88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
89 | // const redirectBreadcrumb = breadcrumbArray.at(2)
90 | // expect(redirectBreadcrumb.contains('a')).toBe(false)
91 | // })
92 | it('last breadcrumb', () => {
93 | router.push('/menu/menu1/menu1-2/menu1-2-1')
94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
95 | const redirectBreadcrumb = breadcrumbArray.at(3)
96 | expect(redirectBreadcrumb.contains('a')).toBe(false)
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/src/views/form/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
修改密码
4 |
12 |
13 | 密码
14 |
19 |
20 |
21 | 确认密码
22 |
27 |
28 |
29 | 提交
32 | 重置
33 |
34 |
35 |
36 |
37 |
105 |
--------------------------------------------------------------------------------
/src/views/nested/menu2/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
修改密码
4 |
12 |
13 | 密码
14 |
19 |
20 |
21 | 确认密码
22 |
27 |
28 |
29 | 提交
32 | 重置
33 |
34 |
35 |
36 |
37 |
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-admin-template
2 |
3 | English | [简体中文](./README-zh.md)
4 |
5 | > A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
6 |
7 | **Live demo:** http://panjiachen.github.io/vue-admin-template
8 |
9 |
10 | **The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`**
11 |
12 | ## Build Setup
13 |
14 |
15 | ```bash
16 | # clone the project
17 | git clone https://github.com/PanJiaChen/vue-admin-template.git
18 |
19 | # enter the project directory
20 | cd vue-admin-template
21 |
22 | # install dependency
23 | npm install
24 |
25 | # develop
26 | npm run dev
27 | ```
28 |
29 | This will automatically open http://localhost:9528
30 |
31 | ## Build
32 |
33 | ```bash
34 | # build for test environment
35 | npm run build:stage
36 |
37 | # build for production environment
38 | npm run build:prod
39 | ```
40 |
41 | ## Advanced
42 |
43 | ```bash
44 | # preview the release environment effect
45 | npm run preview
46 |
47 | # preview the release environment effect + static resource analysis
48 | npm run preview -- --report
49 |
50 | # code format check
51 | npm run lint
52 |
53 | # code format check and auto fix
54 | npm run lint -- --fix
55 | ```
56 |
57 | Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
58 |
59 | ## Demo
60 |
61 | 
62 |
63 | ## Extra
64 |
65 | If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
66 |
67 | For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
68 |
69 | ## Related Project
70 |
71 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
72 |
73 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
74 |
75 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
76 |
77 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
78 |
79 | ## Browsers support
80 |
81 | Modern browsers and Internet Explorer 10+.
82 |
83 | | [
](http://godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari |
84 | | --------- | --------- | --------- | --------- |
85 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
86 |
87 | ## License
88 |
89 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
90 |
91 | Copyright (c) 2017-present PanJiaChen
92 |
--------------------------------------------------------------------------------
/README-zh.md:
--------------------------------------------------------------------------------
1 | # vue-admin-template
2 |
3 | > 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。
4 |
5 | [线上地址](http://panjiachen.github.io/vue-admin-template)
6 |
7 | [国内访问](https://panjiachen.gitee.io/vue-admin-template)
8 |
9 | 目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。
10 |
11 | ## Extra
12 |
13 | 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
14 |
15 | ## 相关项目
16 |
17 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
18 |
19 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
20 |
21 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
22 |
23 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
24 |
25 | 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
26 |
27 | - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
28 | - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
29 | - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
30 | - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
31 | - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
32 |
33 | ## Build Setup
34 |
35 | ```bash
36 | # 克隆项目
37 | git clone https://github.com/PanJiaChen/vue-admin-template.git
38 |
39 | # 进入项目目录
40 | cd vue-admin-template
41 |
42 | # 安装依赖
43 | npm install
44 |
45 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
46 | npm install --registry=https://registry.npm.taobao.org
47 |
48 | # 启动服务
49 | npm run dev
50 | ```
51 |
52 | 浏览器访问 [http://localhost:9528](http://localhost:9528)
53 |
54 | ## 发布
55 |
56 | ```bash
57 | # 构建测试环境
58 | npm run build:stage
59 |
60 | # 构建生产环境
61 | npm run build:prod
62 | ```
63 |
64 | ## 其它
65 |
66 | ```bash
67 | # 预览发布环境效果
68 | npm run preview
69 |
70 | # 预览发布环境效果 + 静态资源分析
71 | npm run preview -- --report
72 |
73 | # 代码格式检查
74 | npm run lint
75 |
76 | # 代码格式检查并自动修复
77 | npm run lint -- --fix
78 | ```
79 |
80 | 更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
81 |
82 | ## Demo
83 |
84 | 
85 |
86 | ## Browsers support
87 |
88 | Modern browsers and Internet Explorer 10+.
89 |
90 | | [
](http://godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari |
91 | | --------- | --------- | --------- | --------- |
92 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
93 |
94 | ## License
95 |
96 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
97 |
98 | Copyright (c) 2017-present PanJiaChen
99 |
--------------------------------------------------------------------------------
/src/store/modules/user.js:
--------------------------------------------------------------------------------
1 | import { login, logout, getInfo } from '@/api/user'
2 | import { getToken, setToken, removeToken } from '@/utils/auth'
3 | import { resetRouter } from '@/router'
4 |
5 | const getDefaultState = () => {
6 | return {
7 | token: getToken(),
8 | name: '',
9 | avatar: '',
10 | roles: [],
11 | introduction: ''
12 | }
13 | }
14 |
15 | const state = getDefaultState()
16 |
17 | const mutations = {
18 | RESET_STATE: (state) => {
19 | Object.assign(state, getDefaultState())
20 | },
21 | SET_TOKEN: (state, token) => {
22 | state.token = token
23 | },
24 | SET_NAME: (state, name) => {
25 | state.name = name
26 | },
27 | SET_AVATAR: (state, avatar) => {
28 | state.avatar = avatar
29 | },
30 | SET_ROLES: (state, roles) => {
31 | state.roles = roles
32 | },
33 | SET_INTRODUCTION: (state, introduction) => {
34 | state.introduction = introduction
35 | }
36 | }
37 |
38 | const actions = {
39 | // 2.用户登录的方法
40 | login({ commit }, userInfo) {
41 | const { username, password } = userInfo
42 | return new Promise((resolve, reject) => {
43 | // 3.调用api/user中的login方法登录
44 | login({ username: username.trim(), password: password }).then(response => {
45 | const { data } = response
46 | // 4.保存后台返回的token信息
47 | commit('SET_TOKEN', data.token)
48 | setToken(data.token)
49 | resolve()
50 | }).catch(error => {
51 | reject(error)
52 | })
53 | })
54 | },
55 |
56 | // 获取用户信息的方法
57 | getInfo({ commit, state }) {
58 | return new Promise((resolve, reject) => {
59 | getInfo(state.token).then(response => {
60 | const { data } = response
61 |
62 | if (!data) {
63 | reject('Verification failed, please Login again.')
64 | }
65 |
66 | const { roles, name, avatar, introduction } = data
67 |
68 | if (!roles || roles.length <= 0) {
69 | reject('getInfo: roles must be a non-null array!')
70 | }
71 |
72 | commit('SET_ROLES', roles)
73 | commit('SET_NAME', name)
74 | commit('SET_AVATAR', avatar)
75 | commit('SET_INTRODUCTION', introduction)
76 |
77 | resolve(data)
78 | }).catch(error => {
79 | reject(error)
80 | })
81 | })
82 | },
83 |
84 | // user logout
85 | logout({ commit, state }) {
86 | return new Promise((resolve, reject) => {
87 | logout(state.token).then(() => {
88 | removeToken() // must remove token first
89 | resetRouter()
90 | commit('RESET_STATE')
91 | resolve()
92 | }).catch(error => {
93 | reject(error)
94 | })
95 | })
96 | },
97 |
98 | changeRoles({ commit, dispatch }, role) {
99 | return new Promise(async resolve => {
100 | const token = role + '-token'
101 |
102 | commit('SET_TOKEN', token)
103 | setToken(token)
104 |
105 | const { roles } = await dispatch('getInfo')
106 | resetRouter()
107 | // generate accessible routes map based on roles
108 | const accessRoutes = await dispatch('permission/generateRoutes', roles, { root: true })
109 | // dynamically add accessible routes
110 | // router.addRoutes(accessRoutes)
111 | // reset visited views and cached views
112 | dispatch('tagsView/delAllViews', null, { root: true })
113 |
114 | resolve()
115 | })
116 | },
117 |
118 | // remove token
119 | resetToken({ commit }) {
120 | return new Promise(resolve => {
121 | removeToken() // must remove token first
122 | commit('RESET_STATE')
123 | resolve()
124 | })
125 | }
126 | }
127 |
128 | export default {
129 | namespaced: true,
130 | state,
131 | mutations,
132 | actions
133 | }
134 |
135 |
--------------------------------------------------------------------------------
/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
31 |
32 |
33 |
34 |
61 |
62 |
140 |
--------------------------------------------------------------------------------
/src/views/permission/directive.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Only
9 | admin can see this
10 |
11 |
12 | v-permission="['admin']"
13 |
14 |
15 |
16 |
17 |
18 | Only
19 | editor can see this
20 |
21 |
22 | v-permission="['editor']"
23 |
24 |
25 |
26 |
27 |
28 | Both
29 | admin and
30 | editor can see this
31 |
32 |
33 | v-permission="['admin','editor']"
34 |
35 |
36 |
37 |
38 |
只有管理员能够看到
39 |
只有普通用户能够看到
40 |
41 |
42 |
46 |
47 |
48 |
49 |
53 | 我是管理员
54 |
55 |
56 |
57 |
61 | 我是编辑员
62 |
63 |
64 |
65 | Both admin or editor can see this
66 |
67 | v-if="checkPermission(['admin','editor'])"
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
97 |
98 |
117 |
118 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const defaultSettings = require('./src/settings.js')
4 |
5 | function resolve(dir) {
6 | return path.join(__dirname, dir)
7 | }
8 |
9 | const name = defaultSettings.title || 'vue Admin Template' // page title
10 |
11 | // If your port is set to 80,
12 | // use administrator privileges to execute the command line.
13 | // For example, Mac: sudo npm run
14 | // You can change the port by the following methods:
15 | // port = 9528 npm run dev OR npm run dev --port = 9528
16 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port
17 |
18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/
19 | module.exports = {
20 | /**
21 | * You will need to set publicPath if you plan to deploy your site under a sub path,
22 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
23 | * then publicPath should be set to "/bar/".
24 | * In most cases please use '/' !!!
25 | * Detail: https://cli.vuejs.org/config/#publicpath
26 | */
27 | publicPath: '/',
28 | outputDir: 'dist',
29 | assetsDir: 'static',
30 | lintOnSave: process.env.NODE_ENV === 'development',
31 | productionSourceMap: false,
32 | devServer: {
33 | port: port,
34 | open: true,
35 | overlay: {
36 | warnings: false,
37 | errors: true
38 | },
39 | before: require('./mock/mock-server.js')
40 | },
41 | configureWebpack: {
42 | // provide the app's title in webpack's name field, so that
43 | // it can be accessed in index.html to inject the correct title.
44 | name: name,
45 | resolve: {
46 | alias: {
47 | '@': resolve('src')
48 | }
49 | }
50 | },
51 | chainWebpack(config) {
52 | config.plugins.delete('preload') // TODO: need test
53 | config.plugins.delete('prefetch') // TODO: need test
54 |
55 | // set svg-sprite-loader
56 | config.module
57 | .rule('svg')
58 | .exclude.add(resolve('src/icons'))
59 | .end()
60 | config.module
61 | .rule('icons')
62 | .test(/\.svg$/)
63 | .include.add(resolve('src/icons'))
64 | .end()
65 | .use('svg-sprite-loader')
66 | .loader('svg-sprite-loader')
67 | .options({
68 | symbolId: 'icon-[name]'
69 | })
70 | .end()
71 |
72 | // set preserveWhitespace
73 | config.module
74 | .rule('vue')
75 | .use('vue-loader')
76 | .loader('vue-loader')
77 | .tap(options => {
78 | options.compilerOptions.preserveWhitespace = true
79 | return options
80 | })
81 | .end()
82 |
83 | config
84 | // https://webpack.js.org/configuration/devtool/#development
85 | .when(process.env.NODE_ENV === 'development',
86 | config => config.devtool('cheap-source-map')
87 | )
88 |
89 | config
90 | .when(process.env.NODE_ENV !== 'development',
91 | config => {
92 | config
93 | .plugin('ScriptExtHtmlWebpackPlugin')
94 | .after('html')
95 | .use('script-ext-html-webpack-plugin', [{
96 | // `runtime` must same as runtimeChunk name. default is `runtime`
97 | inline: /runtime\..*\.js$/
98 | }])
99 | .end()
100 | config
101 | .optimization.splitChunks({
102 | chunks: 'all',
103 | cacheGroups: {
104 | libs: {
105 | name: 'chunk-libs',
106 | test: /[\\/]node_modules[\\/]/,
107 | priority: 10,
108 | chunks: 'initial' // only package third parties that are initially dependent
109 | },
110 | elementUI: {
111 | name: 'chunk-elementUI', // split elementUI into a single package
112 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
113 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
114 | },
115 | commons: {
116 | name: 'chunk-commons',
117 | test: resolve('src/components'), // can customize your rules
118 | minChunks: 3, // minimum common number
119 | priority: 5,
120 | reuseExistingChunk: true
121 | }
122 | }
123 | })
124 | config.optimization.runtimeChunk('single')
125 | }
126 | )
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/views/tree/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | {{ scope.row.id }}
14 |
15 |
16 |
17 |
18 | {{ scope.row.title }}
19 |
20 |
21 |
22 |
23 | {{ scope.row.num }}
24 |
25 |
26 |
27 |
28 | {{ scope.row.price }}
29 |
30 |
31 |
37 |
38 |
39 | {{ scope.row.display_time }}
40 |
41 |
42 |
48 |
49 |
54 | 分配订单
55 |
56 |
61 |
62 |
63 |
64 |
65 | {{ scope.row.id }}
66 |
67 |
68 |
69 |
70 |
71 | {{ scope.row.name }}
72 |
73 |
74 |
75 |
76 | 提交
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
158 |
--------------------------------------------------------------------------------
/src/styles/sidebar.scss:
--------------------------------------------------------------------------------
1 | #app {
2 |
3 | .main-container {
4 | min-height: 100%;
5 | transition: margin-left .28s;
6 | margin-left: $sideBarWidth;
7 | position: relative;
8 | }
9 |
10 | .sidebar-container {
11 | transition: width 0.28s;
12 | width: $sideBarWidth !important;
13 | background-color: $menuBg;
14 | height: 100%;
15 | position: fixed;
16 | font-size: 0px;
17 | top: 0;
18 | bottom: 0;
19 | left: 0;
20 | z-index: 1001;
21 | overflow: hidden;
22 |
23 | // reset element-ui css
24 | .horizontal-collapse-transition {
25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
26 | }
27 |
28 | .scrollbar-wrapper {
29 | overflow-x: hidden !important;
30 | }
31 |
32 | .el-scrollbar__bar.is-vertical {
33 | right: 0px;
34 | }
35 |
36 | .el-scrollbar {
37 | height: 100%;
38 | }
39 |
40 | &.has-logo {
41 | .el-scrollbar {
42 | height: calc(100% - 50px);
43 | }
44 | }
45 |
46 | .is-horizontal {
47 | display: none;
48 | }
49 |
50 | a {
51 | display: inline-block;
52 | width: 100%;
53 | overflow: hidden;
54 | }
55 |
56 | .svg-icon {
57 | margin-right: 16px;
58 | }
59 |
60 | .el-menu {
61 | border: none;
62 | height: 100%;
63 | width: 100% !important;
64 | }
65 |
66 | // menu hover
67 | .submenu-title-noDropdown,
68 | .el-submenu__title {
69 | &:hover {
70 | background-color: $menuHover !important;
71 | }
72 | }
73 |
74 | .is-active>.el-submenu__title {
75 | color: $subMenuActiveText !important;
76 | }
77 |
78 | & .nest-menu .el-submenu>.el-submenu__title,
79 | & .el-submenu .el-menu-item {
80 | min-width: $sideBarWidth !important;
81 | background-color: $subMenuBg !important;
82 |
83 | &:hover {
84 | background-color: $subMenuHover !important;
85 | }
86 | }
87 | }
88 |
89 | .hideSidebar {
90 | .sidebar-container {
91 | width: 54px !important;
92 | }
93 |
94 | .main-container {
95 | margin-left: 54px;
96 | }
97 |
98 | .submenu-title-noDropdown {
99 | padding: 0 !important;
100 | position: relative;
101 |
102 | .el-tooltip {
103 | padding: 0 !important;
104 |
105 | .svg-icon {
106 | margin-left: 20px;
107 | }
108 | }
109 | }
110 |
111 | .el-submenu {
112 | overflow: hidden;
113 |
114 | &>.el-submenu__title {
115 | padding: 0 !important;
116 |
117 | .svg-icon {
118 | margin-left: 20px;
119 | }
120 |
121 | .el-submenu__icon-arrow {
122 | display: none;
123 | }
124 | }
125 | }
126 |
127 | .el-menu--collapse {
128 | .el-submenu {
129 | &>.el-submenu__title {
130 | &>span {
131 | height: 0;
132 | width: 0;
133 | overflow: hidden;
134 | visibility: hidden;
135 | display: inline-block;
136 | }
137 | }
138 | }
139 | }
140 | }
141 |
142 | .el-menu--collapse .el-menu .el-submenu {
143 | min-width: $sideBarWidth !important;
144 | }
145 |
146 | // mobile responsive
147 | .mobile {
148 | .main-container {
149 | margin-left: 0px;
150 | }
151 |
152 | .sidebar-container {
153 | transition: transform .28s;
154 | width: $sideBarWidth !important;
155 | }
156 |
157 | &.hideSidebar {
158 | .sidebar-container {
159 | pointer-events: none;
160 | transition-duration: 0.3s;
161 | transform: translate3d(-$sideBarWidth, 0, 0);
162 | }
163 | }
164 | }
165 |
166 | .withoutAnimation {
167 |
168 | .main-container,
169 | .sidebar-container {
170 | transition: none;
171 | }
172 | }
173 | }
174 |
175 | // when menu collapsed
176 | .el-menu--vertical {
177 | &>.el-menu {
178 | .svg-icon {
179 | margin-right: 16px;
180 | }
181 | }
182 |
183 | .nest-menu .el-submenu>.el-submenu__title,
184 | .el-menu-item {
185 | &:hover {
186 | // you can use $subMenuHover
187 | background-color: $menuHover !important;
188 | }
189 | }
190 |
191 | // the scroll bar appears when the subMenu is too long
192 | >.el-menu--popup {
193 | max-height: 100vh;
194 | overflow-y: auto;
195 |
196 | &::-webkit-scrollbar-track-piece {
197 | background: #d3dce6;
198 | }
199 |
200 | &::-webkit-scrollbar {
201 | width: 6px;
202 | }
203 |
204 | &::-webkit-scrollbar-thumb {
205 | background: #99a9bf;
206 | border-radius: 20px;
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/views/table/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 | {{ scope.row.id }}
14 |
15 |
16 |
17 |
18 | {{ scope.row.title }}
19 |
20 |
21 |
22 |
23 | {{ scope.row.num }}
24 |
25 |
26 |
27 |
28 | {{ scope.row.price }}
29 |
30 |
31 |
37 |
38 |
39 | {{ scope.row.display_time }}
40 |
41 |
42 |
48 |
49 |
54 | 分配订单
55 |
56 |
61 |
62 |
63 |
64 |
65 | {{ scope.row.id }}
66 |
67 |
68 |
69 |
70 |
71 | {{ scope.row.name }}
72 |
73 |
74 |
75 |
76 | 提交
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
160 |
--------------------------------------------------------------------------------
/src/store/modules/tagsView.js:
--------------------------------------------------------------------------------
1 | //记录访问和缓存的视图
2 | const state = {
3 | visitedViews: [],
4 | cachedViews: []
5 | }
6 |
7 | const mutations = {
8 | ADD_VISITED_VIEW: (state, view) => {
9 | if (state.visitedViews.some(v => v.path === view.path)) return
10 | state.visitedViews.push(
11 | Object.assign({}, view, {
12 | title: view.meta.title || 'no-name'
13 | })
14 | )
15 | },
16 | ADD_CACHED_VIEW: (state, view) => {
17 | if (state.cachedViews.includes(view.name)) return
18 | if (!view.meta.noCache) {
19 | state.cachedViews.push(view.name)
20 | }
21 | },
22 |
23 | DEL_VISITED_VIEW: (state, view) => {
24 | for (const [i, v] of state.visitedViews.entries()) {
25 | if (v.path === view.path) {
26 | state.visitedViews.splice(i, 1)
27 | break
28 | }
29 | }
30 | },
31 | DEL_CACHED_VIEW: (state, view) => {
32 | const index = state.cachedViews.indexOf(view.name)
33 | index > -1 && state.cachedViews.splice(index, 1)
34 | },
35 |
36 | DEL_OTHERS_VISITED_VIEWS: (state, view) => {
37 | state.visitedViews = state.visitedViews.filter(v => {
38 | return v.meta.affix || v.path === view.path
39 | })
40 | },
41 | DEL_OTHERS_CACHED_VIEWS: (state, view) => {
42 | const index = state.cachedViews.indexOf(view.name)
43 | if (index > -1) {
44 | state.cachedViews = state.cachedViews.slice(index, index + 1)
45 | } else {
46 | // if index = -1, there is no cached tags
47 | state.cachedViews = []
48 | }
49 | },
50 |
51 | DEL_ALL_VISITED_VIEWS: state => {
52 | // keep affix tags
53 | const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
54 | state.visitedViews = affixTags
55 | },
56 | DEL_ALL_CACHED_VIEWS: state => {
57 | state.cachedViews = []
58 | },
59 |
60 | UPDATE_VISITED_VIEW: (state, view) => {
61 | for (let v of state.visitedViews) {
62 | if (v.path === view.path) {
63 | v = Object.assign(v, view)
64 | break
65 | }
66 | }
67 | }
68 | }
69 |
70 | const actions = {
71 | addView({ dispatch }, view) {
72 | dispatch('addVisitedView', view)
73 | dispatch('addCachedView', view)
74 | },
75 | addVisitedView({ commit }, view) {
76 | commit('ADD_VISITED_VIEW', view)
77 | },
78 | addCachedView({ commit }, view) {
79 | commit('ADD_CACHED_VIEW', view)
80 | },
81 |
82 | delView({ dispatch, state }, view) {
83 | return new Promise(resolve => {
84 | dispatch('delVisitedView', view)
85 | dispatch('delCachedView', view)
86 | resolve({
87 | visitedViews: [...state.visitedViews],
88 | cachedViews: [...state.cachedViews]
89 | })
90 | })
91 | },
92 | delVisitedView({ commit, state }, view) {
93 | return new Promise(resolve => {
94 | commit('DEL_VISITED_VIEW', view)
95 | resolve([...state.visitedViews])
96 | })
97 | },
98 | delCachedView({ commit, state }, view) {
99 | return new Promise(resolve => {
100 | commit('DEL_CACHED_VIEW', view)
101 | resolve([...state.cachedViews])
102 | })
103 | },
104 |
105 | delOthersViews({ dispatch, state }, view) {
106 | return new Promise(resolve => {
107 | dispatch('delOthersVisitedViews', view)
108 | dispatch('delOthersCachedViews', view)
109 | resolve({
110 | visitedViews: [...state.visitedViews],
111 | cachedViews: [...state.cachedViews]
112 | })
113 | })
114 | },
115 | delOthersVisitedViews({ commit, state }, view) {
116 | return new Promise(resolve => {
117 | commit('DEL_OTHERS_VISITED_VIEWS', view)
118 | resolve([...state.visitedViews])
119 | })
120 | },
121 | delOthersCachedViews({ commit, state }, view) {
122 | return new Promise(resolve => {
123 | commit('DEL_OTHERS_CACHED_VIEWS', view)
124 | resolve([...state.cachedViews])
125 | })
126 | },
127 |
128 | delAllViews({ dispatch, state }, view) {
129 | return new Promise(resolve => {
130 | dispatch('delAllVisitedViews', view)
131 | dispatch('delAllCachedViews', view)
132 | resolve({
133 | visitedViews: [...state.visitedViews],
134 | cachedViews: [...state.cachedViews]
135 | })
136 | })
137 | },
138 | delAllVisitedViews({ commit, state }) {
139 | return new Promise(resolve => {
140 | commit('DEL_ALL_VISITED_VIEWS')
141 | resolve([...state.visitedViews])
142 | })
143 | },
144 | delAllCachedViews({ commit, state }) {
145 | return new Promise(resolve => {
146 | commit('DEL_ALL_CACHED_VIEWS')
147 | resolve([...state.cachedViews])
148 | })
149 | },
150 |
151 | updateVisitedView({ commit }, view) {
152 | commit('UPDATE_VISITED_VIEW', view)
153 | }
154 | }
155 |
156 | export default {
157 | namespaced: true,
158 | state,
159 | mutations,
160 | actions
161 | }
162 |
--------------------------------------------------------------------------------
/src/views/nested/menu1/menu1-1/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
添加管理员
4 |
5 |
6 |
用户名:
7 |
14 |
15 |
16 |
24 |
25 | 密码
26 |
33 |
34 |
35 | 提交
38 |
39 |
40 |
41 |
42 |
删除管理员
43 |
51 |
52 |
53 | {{ scope.row.username }}
54 |
55 |
56 |
57 |
58 | {{ scope.row.password }}
59 |
60 |
61 |
67 |
68 |
73 | 删除
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
163 |
178 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
OOPS!
12 |
15 |
{{ message }}
16 |
Please check that the URL you entered is correct, or click the button below to return to the homepage.
17 |
Back to home
18 |
19 |
20 |
21 |
22 |
23 |
34 |
35 |
229 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | /* Layout */
7 | import Layout from '@/layout'
8 |
9 | /**
10 | * Note: sub-menu only appear when route children.length >= 1
11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
12 | *
13 | * hidden: true if set true, item will not show in the sidebar(default is false)
14 | * alwaysShow: true if set true, will always show the root menu
15 | * if not set alwaysShow, when item has more than one children route,
16 | * it will becomes nested mode, otherwise not show the root menu
17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
18 | * name:'router-name' the name is used by (must set!!!)
19 | * meta : {
20 | roles: ['admin','editor'] control the page roles (you can set multiple roles)
21 | title: 'title' the name show in sidebar and breadcrumb (recommend set)
22 | icon: 'svg-name' the icon show in the sidebar
23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
25 | }
26 | */
27 |
28 | /**
29 | * constantRoutes
30 | * a base page that does not have permission requirements
31 | * all roles can be accessed
32 | */
33 |
34 | // constantRouterMap:主要是通用部分,每个用户都有的页面
35 | export const constantRoutes = [
36 | {
37 | path: '/login',
38 | component: () => import('@/views/login/index'),
39 | hidden: true
40 | },
41 | {
42 | path: '/404',
43 | component: () => import('@/views/404'),
44 | hidden: true
45 | },
46 | {
47 | path: '/',
48 | component: Layout,
49 | redirect: '/dashboard',
50 | children: [{
51 | path: 'dashboard',
52 | name: 'Dashboard',
53 | component: () => import('@/views/dashboard/index'),
54 | meta: { title: '首页', icon: 'dashboard' }
55 | }]
56 | },
57 | {
58 | path: '/example',
59 | component: Layout,
60 | redirect: '/example/table',
61 | name: 'Example',
62 | meta: { title: 'Example', icon: 'example' },
63 | children: [
64 | {
65 | path: 'table',
66 | name: 'Table',
67 | component: () => import('@/views/table/index'),
68 | meta: { title: '订单列表', icon: 'table' }
69 | },
70 | {
71 | path: 'tree',
72 | name: 'Tree',
73 | component: () => import('@/views/tree/index'),
74 | meta: { title: '回收员订单列表', icon: 'tree' }
75 | }
76 | ]
77 | },
78 | // {
79 | // path: '/form',
80 | // component: Layout,
81 | // name:"Center",
82 | // meta: {
83 | // title: '安全中心',
84 | // icon: 'example'
85 | // },
86 | // children: [
87 | // {
88 | // path: 'index',
89 | // name: 'Center',
90 | // component: () => import('@/views/form/index'),
91 | // meta: { title: '修改密码', icon: 'eye' }
92 | // },{
93 | // path: 'addPerson',
94 | // name: 'addPerson',
95 | // component: () => import('@/views/form/addPerson'),
96 | // meta: { title: '用户管理', icon: 'tree' },
97 |
98 | // }
99 | // ]
100 | // },
101 | {
102 | path: '/nested',
103 | component: Layout,
104 | redirect: '/nested/menu1',
105 | name: 'Nested',
106 | meta: {
107 | title: '安全中心',
108 | icon: 'example'
109 | },
110 | children: [
111 | {
112 | path: 'menu1',
113 | component: () => import('@/views/nested/menu1/index'), // Parent router-view
114 | name: 'Menu1',
115 | meta: { title: '用户管理',icon: 'tree' },
116 | children: [
117 | {
118 | path: 'menu1-1',
119 | component: () => import('@/views/nested/menu1/menu1-1'),
120 | name: 'Menu1-1',
121 | meta: { title: '管理员权限',icon: 'user' }
122 | },
123 | {
124 | path: 'menu1-2',
125 | component: () => import('@/views/nested/menu1/menu1-2'),
126 | name: 'Menu1-2',
127 | meta: { title: '回收员权限',icon: 'user' },
128 | },
129 | {
130 | path: 'menu1-3',
131 | component: () => import('@/views/nested/menu1/menu1-3'),
132 | name: 'Menu1-3',
133 | meta: { title: '普通用户权限',icon: 'user' }
134 | }
135 | ]
136 | },
137 | {
138 | path: 'menu2',
139 | component: () => import('@/views/nested/menu2/index'),
140 | meta: { title: '修改密码', icon: 'eye' }
141 | }
142 | ]
143 | },
144 | {
145 | path: 'external-link',
146 | component: Layout,
147 | children: [
148 | {
149 | path: 'https://panjiachen.github.io/vue-element-admin-site/#/',
150 | meta: { title: 'External Link', icon: 'link' }
151 | }
152 | ]
153 | },
154 | {
155 | path: '/product',
156 | component: Layout,
157 | name: 'product',
158 | meta: {
159 | title: '商品模块',
160 | icon: 'nested'
161 | },
162 | children: [
163 | {
164 | path: 'index',
165 | name: 'Product',
166 | component: () => import('@/views/product/index'),
167 | meta: { title: '商品', icon: 'eye' }
168 | },
169 | {
170 | path: 'new',
171 | component: () => import('@/views/product/new'),
172 | meta: { title: '新增商品', icon: 'form' }
173 | }
174 | ]
175 | },
176 | // 404 page must be placed at the end !!!
177 | { path: '*', redirect: '/404', hidden: true }
178 | ]
179 |
180 | // asyncRouterMap:需要进行权限过滤的页面
181 | export const asyncRoutes = [
182 | {
183 | path: '/permission',
184 | component: Layout,
185 | redirect: '/permission/page',
186 | alwaysShow: true, // will always show the root menu
187 | name: 'Permission',
188 | meta: {
189 | title: 'Permission',
190 | icon: 'tree',
191 | roles: ['admin','editor'] // you can set roles in root nav
192 | },
193 | children: [
194 | {
195 | path: 'page',
196 | component: () => import('@/views/permission/page'),
197 | name: 'PagePermission',
198 | meta: {
199 | title: 'Page Permission',
200 | roles: ['admin'] // or you can only set roles in sub nav
201 | }
202 | },
203 | {
204 | path: 'directive',
205 | component: () => import('@/views/permission/directive'),
206 | name: 'DirectivePermission',
207 | meta: {
208 | title: 'Directive Permission'
209 | // if do not set roles, means: this page does not require permission
210 | }
211 | }
212 | ]
213 | }
214 | ]
215 |
216 | const createRouter = () => new Router({
217 | // mode: 'history', // require service support
218 | scrollBehavior: () => ({ y: 0 }),
219 | routes: constantRoutes
220 | })
221 |
222 | const router = createRouter()
223 |
224 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
225 | export function resetRouter() {
226 | const newRouter = createRouter()
227 | router.matcher = newRouter.matcher // reset router
228 | }
229 |
230 | export default router
231 |
--------------------------------------------------------------------------------
/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
20 |
21 |
互联网+物品回收平台系统
22 |
23 |
24 |
25 |
26 |
27 |
28 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
54 |
55 |
58 |
59 |
60 |
61 | 登录
68 |
69 |
70 | 职位: {{ item.id==0?p1:(item.id==1?p2:p3) }}
71 | 用户名: {{ item.username }}
72 | 密码: {{ item.password }}
73 |
74 |
75 |
76 |
77 |
78 |
216 |
217 |
269 |
270 |
334 |
--------------------------------------------------------------------------------