(must set!!!)
+ * meta : {
+ roles: ['admin','editor'] control the page roles (you can set multiple roles)
+ title: 'title' the name show in sidebar and breadcrumb (recommend set)
+ icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
+ noCache: true if set true, the page will no be cached(default is false)
+ affix: true if set true, the tag will affix in the tags-view
+ breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
+ activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
+ }
+ */
+
+/**
+ * constantRoutes
+ * a base page that does not have permission requirements
+ * all roles can be accessed
+ */
+export const constantRoutes = [
+ {
+ path: '/redirect',
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: '/redirect/:path(.*)',
+ component: () => import('@/views/redirect/index')
+ }
+ ]
+ },
+ {
+ path: '/login',
+ component: () => import('@/views/login/index'),
+ hidden: true
+ },
+ {
+ path: '/auth-redirect',
+ component: () => import('@/views/login/auth-redirect'),
+ hidden: true
+ },
+ {
+ path: '/404',
+ component: () => import('@/views/error-page/404'),
+ hidden: true
+ },
+ {
+ path: '/401',
+ component: () => import('@/views/error-page/401'),
+ hidden: true
+ },
+ {
+ path: '/',
+ component: Layout,
+ redirect: '/dashboard',
+ children: [
+ {
+ path: 'dashboard',
+ component: () => import('@/views/dashboard/index'),
+ name: 'Dashboard',
+ meta: { title: 'Dashboard', icon: 'dashboard', affix: true }
+ }
+ ]
+ },
+ {
+ path: '/documentation',
+ component: Layout,
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/documentation/index'),
+ name: 'Documentation',
+ meta: { title: 'Documentation', icon: 'documentation', affix: true }
+ }
+ ]
+ },
+ {
+ path: '/guide',
+ component: Layout,
+ redirect: '/guide/index',
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/guide/index'),
+ name: 'Guide',
+ meta: { title: 'Guide', icon: 'guide', noCache: true }
+ }
+ ]
+ },
+ {
+ path: '/profile',
+ component: Layout,
+ redirect: '/profile/index',
+ hidden: true,
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/profile/index'),
+ name: 'Profile',
+ meta: { title: 'Profile', icon: 'user', noCache: true }
+ }
+ ]
+ }
+]
+
+/**
+ * asyncRoutes
+ * the routes that need to be dynamically loaded based on user roles
+ */
+export const asyncRoutes = [
+ {
+ path: '/permission',
+ component: Layout,
+ redirect: '/permission/page',
+ alwaysShow: true, // will always show the root menu
+ name: 'Permission',
+ meta: {
+ title: 'Permission',
+ icon: 'lock',
+ roles: ['admin', 'editor'] // you can set roles in root nav
+ },
+ children: [
+ {
+ path: 'page',
+ component: () => import('@/views/permission/page'),
+ name: 'PagePermission',
+ meta: {
+ title: 'Page Permission',
+ roles: ['admin'] // or you can only set roles in sub nav
+ }
+ },
+ {
+ path: 'directive',
+ component: () => import('@/views/permission/directive'),
+ name: 'DirectivePermission',
+ meta: {
+ title: 'Directive Permission'
+ // if do not set roles, means: this page does not require permission
+ }
+ },
+ {
+ path: 'role',
+ component: () => import('@/views/permission/role'),
+ name: 'RolePermission',
+ meta: {
+ title: 'Role Permission',
+ roles: ['admin']
+ }
+ }
+ ]
+ },
+
+ {
+ path: '/icon',
+ component: Layout,
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/icons/index'),
+ name: 'Icons',
+ meta: { title: 'Icons', icon: 'icon', noCache: true }
+ }
+ ]
+ },
+
+ /** when your routing map is too long, you can split it into small modules **/
+ componentsRouter,
+ chartsRouter,
+ nestedRouter,
+ tableRouter,
+
+ {
+ path: '/example',
+ component: Layout,
+ redirect: '/example/list',
+ name: 'Example',
+ meta: {
+ title: 'Example',
+ icon: 'el-icon-s-help'
+ },
+ children: [
+ {
+ path: 'create',
+ component: () => import('@/views/example/create'),
+ name: 'CreateArticle',
+ meta: { title: 'Create Article', icon: 'edit' }
+ },
+ {
+ path: 'edit/:id(\\d+)',
+ component: () => import('@/views/example/edit'),
+ name: 'EditArticle',
+ meta: { title: 'Edit Article', noCache: true, activeMenu: '/example/list' },
+ hidden: true
+ },
+ {
+ path: 'list',
+ component: () => import('@/views/example/list'),
+ name: 'ArticleList',
+ meta: { title: 'Article List', icon: 'list' }
+ }
+ ]
+ },
+
+ {
+ path: '/tab',
+ component: Layout,
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/tab/index'),
+ name: 'Tab',
+ meta: { title: 'Tab', icon: 'tab' }
+ }
+ ]
+ },
+
+ {
+ path: '/error',
+ component: Layout,
+ redirect: 'noRedirect',
+ name: 'ErrorPages',
+ meta: {
+ title: 'Error Pages',
+ icon: '404'
+ },
+ children: [
+ {
+ path: '401',
+ component: () => import('@/views/error-page/401'),
+ name: 'Page401',
+ meta: { title: '401', noCache: true }
+ },
+ {
+ path: '404',
+ component: () => import('@/views/error-page/404'),
+ name: 'Page404',
+ meta: { title: '404', noCache: true }
+ }
+ ]
+ },
+
+ {
+ path: '/error-log',
+ component: Layout,
+ children: [
+ {
+ path: 'log',
+ component: () => import('@/views/error-log/index'),
+ name: 'ErrorLog',
+ meta: { title: 'Error Log', icon: 'bug' }
+ }
+ ]
+ },
+
+ {
+ path: '/excel',
+ component: Layout,
+ redirect: '/excel/export-excel',
+ name: 'Excel',
+ meta: {
+ title: 'Excel',
+ icon: 'excel'
+ },
+ children: [
+ {
+ path: 'export-excel',
+ component: () => import('@/views/excel/export-excel'),
+ name: 'ExportExcel',
+ meta: { title: 'Export Excel' }
+ },
+ {
+ path: 'export-selected-excel',
+ component: () => import('@/views/excel/select-excel'),
+ name: 'SelectExcel',
+ meta: { title: 'Export Selected' }
+ },
+ {
+ path: 'export-merge-header',
+ component: () => import('@/views/excel/merge-header'),
+ name: 'MergeHeader',
+ meta: { title: 'Merge Header' }
+ },
+ {
+ path: 'upload-excel',
+ component: () => import('@/views/excel/upload-excel'),
+ name: 'UploadExcel',
+ meta: { title: 'Upload Excel' }
+ }
+ ]
+ },
+
+ {
+ path: '/zip',
+ component: Layout,
+ redirect: '/zip/download',
+ alwaysShow: true,
+ name: 'Zip',
+ meta: { title: 'Zip', icon: 'zip' },
+ children: [
+ {
+ path: 'download',
+ component: () => import('@/views/zip/index'),
+ name: 'ExportZip',
+ meta: { title: 'Export Zip' }
+ }
+ ]
+ },
+
+ {
+ path: '/pdf',
+ component: Layout,
+ redirect: '/pdf/index',
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/pdf/index'),
+ name: 'PDF',
+ meta: { title: 'PDF', icon: 'pdf' }
+ }
+ ]
+ },
+ {
+ path: '/pdf/download',
+ component: () => import('@/views/pdf/download'),
+ hidden: true
+ },
+
+ {
+ path: '/theme',
+ component: Layout,
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/theme/index'),
+ name: 'Theme',
+ meta: { title: 'Theme', icon: 'theme' }
+ }
+ ]
+ },
+
+ {
+ path: '/clipboard',
+ component: Layout,
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/clipboard/index'),
+ name: 'ClipboardDemo',
+ meta: { title: 'Clipboard', icon: 'clipboard' }
+ }
+ ]
+ },
+
+ {
+ path: 'external-link',
+ component: Layout,
+ children: [
+ {
+ path: 'https://github.com/PanJiaChen/vue-element-admin',
+ meta: { title: 'External Link', icon: 'link' }
+ }
+ ]
+ },
+
+ // 404 page must be placed at the end !!!
+ { path: '*', redirect: '/404', hidden: true }
+]
+
+const createRouter = () => new Router({
+ // mode: 'history', // require service support
+ scrollBehavior: () => ({ y: 0 }),
+ routes: constantRoutes
+})
+
+const router = createRouter()
+
+// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
+export function resetRouter() {
+ const newRouter = createRouter()
+ router.matcher = newRouter.matcher // reset router
+}
+
+export default router
diff --git a/src/router/modules/charts.js b/src/router/modules/charts.js
new file mode 100644
index 0000000..29684de
--- /dev/null
+++ b/src/router/modules/charts.js
@@ -0,0 +1,36 @@
+/** When your routing table is too long, you can split it into small modules**/
+
+import Layout from '@/layout'
+
+const chartsRouter = {
+ path: '/charts',
+ component: Layout,
+ redirect: 'noRedirect',
+ name: 'Charts',
+ meta: {
+ title: 'Charts',
+ icon: 'chart'
+ },
+ children: [
+ {
+ path: 'keyboard',
+ component: () => import('@/views/charts/keyboard'),
+ name: 'KeyboardChart',
+ meta: { title: 'Keyboard Chart', noCache: true }
+ },
+ {
+ path: 'line',
+ component: () => import('@/views/charts/line'),
+ name: 'LineChart',
+ meta: { title: 'Line Chart', noCache: true }
+ },
+ {
+ path: 'mix-chart',
+ component: () => import('@/views/charts/mix-chart'),
+ name: 'MixChart',
+ meta: { title: 'Mix Chart', noCache: true }
+ }
+ ]
+}
+
+export default chartsRouter
diff --git a/src/router/modules/components.js b/src/router/modules/components.js
new file mode 100644
index 0000000..0da96f9
--- /dev/null
+++ b/src/router/modules/components.js
@@ -0,0 +1,102 @@
+/** When your routing table is too long, you can split it into small modules **/
+
+import Layout from '@/layout'
+
+const componentsRouter = {
+ path: '/components',
+ component: Layout,
+ redirect: 'noRedirect',
+ name: 'ComponentDemo',
+ meta: {
+ title: 'Components',
+ icon: 'component'
+ },
+ children: [
+ {
+ path: 'tinymce',
+ component: () => import('@/views/components-demo/tinymce'),
+ name: 'TinymceDemo',
+ meta: { title: 'Tinymce' }
+ },
+ {
+ path: 'markdown',
+ component: () => import('@/views/components-demo/markdown'),
+ name: 'MarkdownDemo',
+ meta: { title: 'Markdown' }
+ },
+ {
+ path: 'json-editor',
+ component: () => import('@/views/components-demo/json-editor'),
+ name: 'JsonEditorDemo',
+ meta: { title: 'JSON Editor' }
+ },
+ {
+ path: 'split-pane',
+ component: () => import('@/views/components-demo/split-pane'),
+ name: 'SplitpaneDemo',
+ meta: { title: 'SplitPane' }
+ },
+ {
+ path: 'avatar-upload',
+ component: () => import('@/views/components-demo/avatar-upload'),
+ name: 'AvatarUploadDemo',
+ meta: { title: 'Upload' }
+ },
+ {
+ path: 'dropzone',
+ component: () => import('@/views/components-demo/dropzone'),
+ name: 'DropzoneDemo',
+ meta: { title: 'Dropzone' }
+ },
+ {
+ path: 'sticky',
+ component: () => import('@/views/components-demo/sticky'),
+ name: 'StickyDemo',
+ meta: { title: 'Sticky' }
+ },
+ {
+ path: 'count-to',
+ component: () => import('@/views/components-demo/count-to'),
+ name: 'CountToDemo',
+ meta: { title: 'Count To' }
+ },
+ {
+ path: 'mixin',
+ component: () => import('@/views/components-demo/mixin'),
+ name: 'ComponentMixinDemo',
+ meta: { title: 'Component Mixin' }
+ },
+ {
+ path: 'back-to-top',
+ component: () => import('@/views/components-demo/back-to-top'),
+ name: 'BackToTopDemo',
+ meta: { title: 'Back To Top' }
+ },
+ {
+ path: 'drag-dialog',
+ component: () => import('@/views/components-demo/drag-dialog'),
+ name: 'DragDialogDemo',
+ meta: { title: 'Drag Dialog' }
+ },
+ {
+ path: 'drag-select',
+ component: () => import('@/views/components-demo/drag-select'),
+ name: 'DragSelectDemo',
+ meta: { title: 'Drag Select' }
+ },
+ {
+ path: 'dnd-list',
+ component: () => import('@/views/components-demo/dnd-list'),
+ name: 'DndListDemo',
+ meta: { title: 'Dnd List' }
+ },
+ {
+ path: 'drag-kanban',
+ component: () => import('@/views/components-demo/drag-kanban'),
+ name: 'DragKanbanDemo',
+ meta: { title: 'Drag Kanban' }
+ }
+ ]
+}
+
+export default componentsRouter
diff --git a/src/router/modules/nested.js b/src/router/modules/nested.js
new file mode 100644
index 0000000..48033ed
--- /dev/null
+++ b/src/router/modules/nested.js
@@ -0,0 +1,66 @@
+/** When your routing table is too long, you can split it into small modules **/
+
+import Layout from '@/layout'
+
+const nestedRouter = {
+ path: '/nested',
+ component: Layout,
+ redirect: '/nested/menu1/menu1-1',
+ name: 'Nested',
+ meta: {
+ title: 'Nested Routes',
+ icon: 'nested'
+ },
+ children: [
+ {
+ path: 'menu1',
+ component: () => import('@/views/nested/menu1/index'), // Parent router-view
+ name: 'Menu1',
+ meta: { title: 'Menu 1' },
+ redirect: '/nested/menu1/menu1-1',
+ children: [
+ {
+ path: 'menu1-1',
+ component: () => import('@/views/nested/menu1/menu1-1'),
+ name: 'Menu1-1',
+ meta: { title: 'Menu 1-1' }
+ },
+ {
+ path: 'menu1-2',
+ component: () => import('@/views/nested/menu1/menu1-2'),
+ name: 'Menu1-2',
+ redirect: '/nested/menu1/menu1-2/menu1-2-1',
+ meta: { title: 'Menu 1-2' },
+ children: [
+ {
+ path: 'menu1-2-1',
+ component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
+ name: 'Menu1-2-1',
+ meta: { title: 'Menu 1-2-1' }
+ },
+ {
+ path: 'menu1-2-2',
+ component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
+ name: 'Menu1-2-2',
+ meta: { title: 'Menu 1-2-2' }
+ }
+ ]
+ },
+ {
+ path: 'menu1-3',
+ component: () => import('@/views/nested/menu1/menu1-3'),
+ name: 'Menu1-3',
+ meta: { title: 'Menu 1-3' }
+ }
+ ]
+ },
+ {
+ path: 'menu2',
+ name: 'Menu2',
+ component: () => import('@/views/nested/menu2/index'),
+ meta: { title: 'Menu 2' }
+ }
+ ]
+}
+
+export default nestedRouter
diff --git a/src/router/modules/table.js b/src/router/modules/table.js
new file mode 100644
index 0000000..ec28c52
--- /dev/null
+++ b/src/router/modules/table.js
@@ -0,0 +1,41 @@
+/** When your routing table is too long, you can split it into small modules **/
+
+import Layout from '@/layout'
+
+const tableRouter = {
+ path: '/table',
+ component: Layout,
+ redirect: '/table/complex-table',
+ name: 'Table',
+ meta: {
+ title: 'Table',
+ icon: 'table'
+ },
+ children: [
+ {
+ path: 'dynamic-table',
+ component: () => import('@/views/table/dynamic-table/index'),
+ name: 'DynamicTable',
+ meta: { title: 'Dynamic Table' }
+ },
+ {
+ path: 'drag-table',
+ component: () => import('@/views/table/drag-table'),
+ name: 'DragTable',
+ meta: { title: 'Drag Table' }
+ },
+ {
+ path: 'inline-edit-table',
+ component: () => import('@/views/table/inline-edit-table'),
+ name: 'InlineEditTable',
+ meta: { title: 'Inline Edit' }
+ },
+ {
+ path: 'complex-table',
+ component: () => import('@/views/table/complex-table'),
+ name: 'ComplexTable',
+ meta: { title: 'Complex Table' }
+ }
+ ]
+}
+export default tableRouter
diff --git a/src/settings.js b/src/settings.js
new file mode 100644
index 0000000..1ebc7f2
--- /dev/null
+++ b/src/settings.js
@@ -0,0 +1,35 @@
+module.exports = {
+ title: 'Vue Element Admin',
+
+ /**
+ * @type {boolean} true | false
+ * @description Whether show the settings right-panel
+ */
+ showSettings: true,
+
+ /**
+ * @type {boolean} true | false
+ * @description Whether need tagsView
+ */
+ tagsView: true,
+
+ /**
+ * @type {boolean} true | false
+ * @description Whether fix the header
+ */
+ fixedHeader: false,
+
+ /**
+ * @type {boolean} true | false
+ * @description Whether show the logo in sidebar
+ */
+ sidebarLogo: false,
+
+ /**
+ * @type {string | array} 'production' | ['production', 'development']
+ * @description Need show err logs component.
+ * The default is only used in the production env
+ * If you want to also use it in dev, you can pass ['production', 'development']
+ */
+ errorLog: 'production'
+}
diff --git a/src/store/getters.js b/src/store/getters.js
new file mode 100644
index 0000000..8fcf5a5
--- /dev/null
+++ b/src/store/getters.js
@@ -0,0 +1,15 @@
+const getters = {
+ sidebar: state => state.app.sidebar,
+ size: state => state.app.size,
+ device: state => state.app.device,
+ visitedViews: state => state.tagsView.visitedViews,
+ cachedViews: state => state.tagsView.cachedViews,
+ token: state => state.user.token,
+ avatar: state => state.user.avatar,
+ name: state => state.user.name,
+ introduction: state => state.user.introduction,
+ roles: state => state.user.roles,
+ permission_routes: state => state.permission.routes,
+ errorLogs: state => state.errorLog.logs
+}
+export default getters
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000..0fd8395
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+import getters from './getters'
+
+Vue.use(Vuex)
+
+// https://webpack.js.org/guides/dependency-management/#requirecontext
+const modulesFiles = require.context('./modules', true, /\.js$/)
+
+// you do not need `import app from './modules/app'`
+// it will auto require all vuex module from modules file
+const modules = modulesFiles.keys().reduce((modules, modulePath) => {
+ // set './app.js' => 'app'
+ const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
+ const value = modulesFiles(modulePath)
+ modules[moduleName] = value.default
+ return modules
+}, {})
+
+const store = new Vuex.Store({
+ modules,
+ getters
+})
+
+export default store
diff --git a/src/store/modules/app.js b/src/store/modules/app.js
new file mode 100644
index 0000000..45d89bb
--- /dev/null
+++ b/src/store/modules/app.js
@@ -0,0 +1,56 @@
+import Cookies from 'js-cookie'
+
+const state = {
+ sidebar: {
+ opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
+ withoutAnimation: false
+ },
+ device: 'desktop',
+ size: Cookies.get('size') || 'medium'
+}
+
+const mutations = {
+ TOGGLE_SIDEBAR: state => {
+ state.sidebar.opened = !state.sidebar.opened
+ state.sidebar.withoutAnimation = false
+ if (state.sidebar.opened) {
+ Cookies.set('sidebarStatus', 1)
+ } else {
+ Cookies.set('sidebarStatus', 0)
+ }
+ },
+ CLOSE_SIDEBAR: (state, withoutAnimation) => {
+ Cookies.set('sidebarStatus', 0)
+ state.sidebar.opened = false
+ state.sidebar.withoutAnimation = withoutAnimation
+ },
+ TOGGLE_DEVICE: (state, device) => {
+ state.device = device
+ },
+ SET_SIZE: (state, size) => {
+ state.size = size
+ Cookies.set('size', size)
+ }
+}
+
+const actions = {
+ toggleSideBar({ commit }) {
+ commit('TOGGLE_SIDEBAR')
+ },
+ closeSideBar({ commit }, { withoutAnimation }) {
+ commit('CLOSE_SIDEBAR', withoutAnimation)
+ },
+ toggleDevice({ commit }, device) {
+ commit('TOGGLE_DEVICE', device)
+ },
+ setSize({ commit }, size) {
+ commit('SET_SIZE', size)
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ mutations,
+ actions
+}
diff --git a/src/store/modules/errorLog.js b/src/store/modules/errorLog.js
new file mode 100644
index 0000000..6b01f95
--- /dev/null
+++ b/src/store/modules/errorLog.js
@@ -0,0 +1,28 @@
+const state = {
+ logs: []
+}
+
+const mutations = {
+ ADD_ERROR_LOG: (state, log) => {
+ state.logs.push(log)
+ },
+ CLEAR_ERROR_LOG: (state) => {
+ state.logs.splice(0)
+ }
+}
+
+const actions = {
+ addErrorLog({ commit }, log) {
+ commit('ADD_ERROR_LOG', log)
+ },
+ clearErrorLog({ commit }) {
+ commit('CLEAR_ERROR_LOG')
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ mutations,
+ actions
+}
diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js
new file mode 100644
index 0000000..aeb5ee5
--- /dev/null
+++ b/src/store/modules/permission.js
@@ -0,0 +1,69 @@
+import { asyncRoutes, constantRoutes } from '@/router'
+
+/**
+ * Use meta.role to determine if the current user has permission
+ * @param roles
+ * @param route
+ */
+function hasPermission(roles, route) {
+ if (route.meta && route.meta.roles) {
+ return roles.some(role => route.meta.roles.includes(role))
+ } else {
+ return true
+ }
+}
+
+/**
+ * Filter asynchronous routing tables by recursion
+ * @param routes asyncRoutes
+ * @param roles
+ */
+export function filterAsyncRoutes(routes, roles) {
+ const res = []
+
+ routes.forEach(route => {
+ const tmp = { ...route }
+ if (hasPermission(roles, tmp)) {
+ if (tmp.children) {
+ tmp.children = filterAsyncRoutes(tmp.children, roles)
+ }
+ res.push(tmp)
+ }
+ })
+
+ return res
+}
+
+const state = {
+ routes: [],
+ addRoutes: []
+}
+
+const mutations = {
+ SET_ROUTES: (state, routes) => {
+ state.addRoutes = routes
+ state.routes = constantRoutes.concat(routes)
+ }
+}
+
+const actions = {
+ generateRoutes({ commit }, roles) {
+ return new Promise(resolve => {
+ let accessedRoutes
+ if (roles.includes('admin')) {
+ accessedRoutes = asyncRoutes || []
+ } else {
+ accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
+ }
+ commit('SET_ROUTES', accessedRoutes)
+ resolve(accessedRoutes)
+ })
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ mutations,
+ actions
+}
diff --git a/src/store/modules/settings.js b/src/store/modules/settings.js
new file mode 100644
index 0000000..110533f
--- /dev/null
+++ b/src/store/modules/settings.js
@@ -0,0 +1,35 @@
+import variables from '@/styles/element-variables.scss'
+import defaultSettings from '@/settings'
+
+const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings
+
+const state = {
+ theme: variables.theme,
+ showSettings: showSettings,
+ tagsView: tagsView,
+ fixedHeader: fixedHeader,
+ sidebarLogo: sidebarLogo
+}
+
+const mutations = {
+ CHANGE_SETTING: (state, { key, value }) => {
+ // eslint-disable-next-line no-prototype-builtins
+ if (state.hasOwnProperty(key)) {
+ state[key] = value
+ }
+ }
+}
+
+const actions = {
+ changeSetting({ commit }, data) {
+ commit('CHANGE_SETTING', data)
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ mutations,
+ actions
+}
+
diff --git a/src/store/modules/tagsView.js b/src/store/modules/tagsView.js
new file mode 100644
index 0000000..57e7242
--- /dev/null
+++ b/src/store/modules/tagsView.js
@@ -0,0 +1,160 @@
+const state = {
+ visitedViews: [],
+ cachedViews: []
+}
+
+const mutations = {
+ ADD_VISITED_VIEW: (state, view) => {
+ if (state.visitedViews.some(v => v.path === view.path)) return
+ state.visitedViews.push(
+ Object.assign({}, view, {
+ title: view.meta.title || 'no-name'
+ })
+ )
+ },
+ ADD_CACHED_VIEW: (state, view) => {
+ if (state.cachedViews.includes(view.name)) return
+ if (!view.meta.noCache) {
+ state.cachedViews.push(view.name)
+ }
+ },
+
+ DEL_VISITED_VIEW: (state, view) => {
+ for (const [i, v] of state.visitedViews.entries()) {
+ if (v.path === view.path) {
+ state.visitedViews.splice(i, 1)
+ break
+ }
+ }
+ },
+ DEL_CACHED_VIEW: (state, view) => {
+ const index = state.cachedViews.indexOf(view.name)
+ index > -1 && state.cachedViews.splice(index, 1)
+ },
+
+ DEL_OTHERS_VISITED_VIEWS: (state, view) => {
+ state.visitedViews = state.visitedViews.filter(v => {
+ return v.meta.affix || v.path === view.path
+ })
+ },
+ DEL_OTHERS_CACHED_VIEWS: (state, view) => {
+ const index = state.cachedViews.indexOf(view.name)
+ if (index > -1) {
+ state.cachedViews = state.cachedViews.slice(index, index + 1)
+ } else {
+ // if index = -1, there is no cached tags
+ state.cachedViews = []
+ }
+ },
+
+ DEL_ALL_VISITED_VIEWS: state => {
+ // keep affix tags
+ const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
+ state.visitedViews = affixTags
+ },
+ DEL_ALL_CACHED_VIEWS: state => {
+ state.cachedViews = []
+ },
+
+ UPDATE_VISITED_VIEW: (state, view) => {
+ for (let v of state.visitedViews) {
+ if (v.path === view.path) {
+ v = Object.assign(v, view)
+ break
+ }
+ }
+ }
+}
+
+const actions = {
+ addView({ dispatch }, view) {
+ dispatch('addVisitedView', view)
+ dispatch('addCachedView', view)
+ },
+ addVisitedView({ commit }, view) {
+ commit('ADD_VISITED_VIEW', view)
+ },
+ addCachedView({ commit }, view) {
+ commit('ADD_CACHED_VIEW', view)
+ },
+
+ delView({ dispatch, state }, view) {
+ return new Promise(resolve => {
+ dispatch('delVisitedView', view)
+ dispatch('delCachedView', view)
+ resolve({
+ visitedViews: [...state.visitedViews],
+ cachedViews: [...state.cachedViews]
+ })
+ })
+ },
+ delVisitedView({ commit, state }, view) {
+ return new Promise(resolve => {
+ commit('DEL_VISITED_VIEW', view)
+ resolve([...state.visitedViews])
+ })
+ },
+ delCachedView({ commit, state }, view) {
+ return new Promise(resolve => {
+ commit('DEL_CACHED_VIEW', view)
+ resolve([...state.cachedViews])
+ })
+ },
+
+ delOthersViews({ dispatch, state }, view) {
+ return new Promise(resolve => {
+ dispatch('delOthersVisitedViews', view)
+ dispatch('delOthersCachedViews', view)
+ resolve({
+ visitedViews: [...state.visitedViews],
+ cachedViews: [...state.cachedViews]
+ })
+ })
+ },
+ delOthersVisitedViews({ commit, state }, view) {
+ return new Promise(resolve => {
+ commit('DEL_OTHERS_VISITED_VIEWS', view)
+ resolve([...state.visitedViews])
+ })
+ },
+ delOthersCachedViews({ commit, state }, view) {
+ return new Promise(resolve => {
+ commit('DEL_OTHERS_CACHED_VIEWS', view)
+ resolve([...state.cachedViews])
+ })
+ },
+
+ delAllViews({ dispatch, state }, view) {
+ return new Promise(resolve => {
+ dispatch('delAllVisitedViews', view)
+ dispatch('delAllCachedViews', view)
+ resolve({
+ visitedViews: [...state.visitedViews],
+ cachedViews: [...state.cachedViews]
+ })
+ })
+ },
+ delAllVisitedViews({ commit, state }) {
+ return new Promise(resolve => {
+ commit('DEL_ALL_VISITED_VIEWS')
+ resolve([...state.visitedViews])
+ })
+ },
+ delAllCachedViews({ commit, state }) {
+ return new Promise(resolve => {
+ commit('DEL_ALL_CACHED_VIEWS')
+ resolve([...state.cachedViews])
+ })
+ },
+
+ updateVisitedView({ commit }, view) {
+ commit('UPDATE_VISITED_VIEW', view)
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ mutations,
+ actions
+}
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
new file mode 100644
index 0000000..7800941
--- /dev/null
+++ b/src/store/modules/user.js
@@ -0,0 +1,131 @@
+import { login, logout, getInfo } from '@/api/user'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+import router, { resetRouter } from '@/router'
+
+const state = {
+ token: getToken(),
+ name: '',
+ avatar: '',
+ introduction: '',
+ roles: []
+}
+
+const mutations = {
+ SET_TOKEN: (state, token) => {
+ state.token = token
+ },
+ SET_INTRODUCTION: (state, introduction) => {
+ state.introduction = introduction
+ },
+ SET_NAME: (state, name) => {
+ state.name = name
+ },
+ SET_AVATAR: (state, avatar) => {
+ state.avatar = avatar
+ },
+ SET_ROLES: (state, roles) => {
+ state.roles = roles
+ }
+}
+
+const actions = {
+ // user login
+ login({ commit }, userInfo) {
+ const { username, password } = userInfo
+ return new Promise((resolve, reject) => {
+ login({ username: username.trim(), password: password }).then(response => {
+ const { data } = response
+ commit('SET_TOKEN', data.token)
+ setToken(data.token)
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+
+ // get user info
+ getInfo({ commit, state }) {
+ return new Promise((resolve, reject) => {
+ getInfo(state.token).then(response => {
+ const { data } = response
+
+ if (!data) {
+ reject('Verification failed, please Login again.')
+ }
+
+ const { roles, name, avatar, introduction } = data
+
+ // roles must be a non-empty array
+ if (!roles || roles.length <= 0) {
+ reject('getInfo: roles must be a non-null array!')
+ }
+
+ commit('SET_ROLES', roles)
+ commit('SET_NAME', name)
+ commit('SET_AVATAR', avatar)
+ commit('SET_INTRODUCTION', introduction)
+ resolve(data)
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+
+ // user logout
+ logout({ commit, state, dispatch }) {
+ return new Promise((resolve, reject) => {
+ logout(state.token).then(() => {
+ commit('SET_TOKEN', '')
+ commit('SET_ROLES', [])
+ removeToken()
+ resetRouter()
+
+ // reset visited views and cached views
+ // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
+ dispatch('tagsView/delAllViews', null, { root: true })
+
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+
+ // remove token
+ resetToken({ commit }) {
+ return new Promise(resolve => {
+ commit('SET_TOKEN', '')
+ commit('SET_ROLES', [])
+ removeToken()
+ resolve()
+ })
+ },
+
+ // dynamically modify permissions
+ async changeRoles({ commit, dispatch }, role) {
+ const token = role + '-token'
+
+ commit('SET_TOKEN', token)
+ setToken(token)
+
+ const { roles } = await dispatch('getInfo')
+
+ resetRouter()
+
+ // generate accessible routes map based on roles
+ const accessRoutes = await dispatch('permission/generateRoutes', roles, { root: true })
+ // dynamically add accessible routes
+ router.addRoutes(accessRoutes)
+
+ // reset visited views and cached views
+ dispatch('tagsView/delAllViews', null, { root: true })
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ mutations,
+ actions
+}
diff --git a/src/styles/btn.scss b/src/styles/btn.scss
new file mode 100644
index 0000000..e6ba1a8
--- /dev/null
+++ b/src/styles/btn.scss
@@ -0,0 +1,99 @@
+@import './variables.scss';
+
+@mixin colorBtn($color) {
+ background: $color;
+
+ &:hover {
+ color: $color;
+
+ &:before,
+ &:after {
+ background: $color;
+ }
+ }
+}
+
+.blue-btn {
+ @include colorBtn($blue)
+}
+
+.light-blue-btn {
+ @include colorBtn($light-blue)
+}
+
+.red-btn {
+ @include colorBtn($red)
+}
+
+.pink-btn {
+ @include colorBtn($pink)
+}
+
+.green-btn {
+ @include colorBtn($green)
+}
+
+.tiffany-btn {
+ @include colorBtn($tiffany)
+}
+
+.yellow-btn {
+ @include colorBtn($yellow)
+}
+
+.pan-btn {
+ font-size: 14px;
+ color: #fff;
+ padding: 14px 36px;
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ transition: 600ms ease all;
+ position: relative;
+ display: inline-block;
+
+ &:hover {
+ background: #fff;
+
+ &:before,
+ &:after {
+ width: 100%;
+ transition: 600ms ease all;
+ }
+ }
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 2px;
+ width: 0;
+ transition: 400ms ease all;
+ }
+
+ &::after {
+ right: inherit;
+ top: inherit;
+ left: 0;
+ bottom: 0;
+ }
+}
+
+.custom-button {
+ display: inline-block;
+ line-height: 1;
+ white-space: nowrap;
+ cursor: pointer;
+ background: #fff;
+ color: #fff;
+ -webkit-appearance: none;
+ text-align: center;
+ box-sizing: border-box;
+ outline: 0;
+ margin: 0;
+ padding: 10px 15px;
+ font-size: 14px;
+ border-radius: 4px;
+}
diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss
new file mode 100644
index 0000000..49474de
--- /dev/null
+++ b/src/styles/element-ui.scss
@@ -0,0 +1,84 @@
+// cover some element-ui styles
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+ font-weight: 400 !important;
+}
+
+.el-upload {
+ input[type="file"] {
+ display: none !important;
+ }
+}
+
+.el-upload__input {
+ display: none;
+}
+
+.cell {
+ .el-tag {
+ margin-right: 0px;
+ }
+}
+
+.small-padding {
+ .cell {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
+
+.fixed-width {
+ .el-button--mini {
+ padding: 7px 10px;
+ min-width: 60px;
+ }
+}
+
+.status-col {
+ .cell {
+ padding: 0 10px;
+ text-align: center;
+
+ .el-tag {
+ margin-right: 0px;
+ }
+ }
+}
+
+// to fixed https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+ transform: none;
+ left: 0;
+ position: relative;
+ margin: 0 auto;
+}
+
+// refine element ui upload
+.upload-container {
+ .el-upload {
+ width: 100%;
+
+ .el-upload-dragger {
+ width: 100%;
+ height: 200px;
+ }
+ }
+}
+
+// dropdown
+.el-dropdown-menu {
+ a {
+ display: block
+ }
+}
+
+// fix date-picker ui bug in filter-item
+.el-range-editor.el-input__inner {
+ display: inline-flex !important;
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+ box-sizing: content-box;
+}
diff --git a/src/styles/element-variables.scss b/src/styles/element-variables.scss
new file mode 100644
index 0000000..5bdc4da
--- /dev/null
+++ b/src/styles/element-variables.scss
@@ -0,0 +1,31 @@
+/**
+* I think element-ui's default theme color is too light for long-term use.
+* So I modified the default color and you can modify it to your liking.
+**/
+
+/* theme color */
+$--color-primary: #1890ff;
+$--color-success: #13ce66;
+$--color-warning: #ffba00;
+$--color-danger: #ff4949;
+// $--color-info: #1E1E1E;
+
+$--button-font-weight: 400;
+
+// $--color-text-regular: #1f2d3d;
+
+$--border-color-light: #dfe4ed;
+$--border-color-lighter: #e6ebf5;
+
+$--table-border: 1px solid #dfe6ec;
+
+/* icon font path, required */
+$--font-path: "~element-ui/lib/theme-chalk/fonts";
+
+@import "~element-ui/packages/theme-chalk/src/index";
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+ theme: $--color-primary;
+}
diff --git a/src/styles/index.scss b/src/styles/index.scss
new file mode 100644
index 0000000..96095ef
--- /dev/null
+++ b/src/styles/index.scss
@@ -0,0 +1,191 @@
+@import './variables.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+@import './btn.scss';
+
+body {
+ height: 100%;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+ font-weight: 700;
+}
+
+html {
+ height: 100%;
+ box-sizing: border-box;
+}
+
+#app {
+ height: 100%;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+.no-padding {
+ padding: 0px !important;
+}
+
+.padding-content {
+ padding: 4px 0;
+}
+
+a:focus,
+a:active {
+ outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+ cursor: pointer;
+ color: inherit;
+ text-decoration: none;
+}
+
+div:focus {
+ outline: none;
+}
+
+.fr {
+ float: right;
+}
+
+.fl {
+ float: left;
+}
+
+.pr-5 {
+ padding-right: 5px;
+}
+
+.pl-5 {
+ padding-left: 5px;
+}
+
+.block {
+ display: block;
+}
+
+.pointer {
+ cursor: pointer;
+}
+
+.inlineBlock {
+ display: block;
+}
+
+.clearfix {
+ &:after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: " ";
+ clear: both;
+ height: 0;
+ }
+}
+
+aside {
+ background: #eef1f6;
+ padding: 8px 24px;
+ margin-bottom: 20px;
+ border-radius: 2px;
+ display: block;
+ line-height: 32px;
+ font-size: 16px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ color: #2c3e50;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ a {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+ }
+}
+
+//main-container全局样式
+.app-container {
+ padding: 20px;
+}
+
+.components-container {
+ margin: 30px 50px;
+ position: relative;
+}
+
+.pagination-container {
+ margin-top: 30px;
+}
+
+.text-center {
+ text-align: center
+}
+
+.sub-navbar {
+ height: 50px;
+ line-height: 50px;
+ position: relative;
+ width: 100%;
+ text-align: right;
+ padding-right: 20px;
+ transition: 600ms ease position;
+ background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
+ .subtitle {
+ font-size: 20px;
+ color: #fff;
+ }
+
+ &.draft {
+ background: #d0d0d0;
+ }
+
+ &.deleted {
+ background: #d0d0d0;
+ }
+}
+
+.link-type,
+.link-type:focus {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+}
+
+.filter-container {
+ padding-bottom: 10px;
+
+ .filter-item {
+ display: inline-block;
+ vertical-align: middle;
+ margin-bottom: 10px;
+ }
+}
+
+//refine vue-multiselect plugin
+.multiselect {
+ line-height: 16px;
+}
+
+.multiselect--active {
+ z-index: 1000 !important;
+}
diff --git a/src/styles/mixin.scss b/src/styles/mixin.scss
new file mode 100644
index 0000000..06fa061
--- /dev/null
+++ b/src/styles/mixin.scss
@@ -0,0 +1,66 @@
+@mixin clearfix {
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+}
+
+@mixin scrollBar {
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+}
+
+@mixin relative {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+@mixin pct($pct) {
+ width: #{$pct};
+ position: relative;
+ margin: 0 auto;
+}
+
+@mixin triangle($width, $height, $color, $direction) {
+ $width: $width/2;
+ $color-border-style: $height solid $color;
+ $transparent-border-style: $width solid transparent;
+ height: 0;
+ width: 0;
+
+ @if $direction==up {
+ border-bottom: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ }
+
+ @else if $direction==right {
+ border-left: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+
+ @else if $direction==down {
+ border-top: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ }
+
+ @else if $direction==left {
+ border-right: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+}
diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss
new file mode 100644
index 0000000..94760cc
--- /dev/null
+++ b/src/styles/sidebar.scss
@@ -0,0 +1,226 @@
+#app {
+
+ .main-container {
+ min-height: 100%;
+ transition: margin-left .28s;
+ margin-left: $sideBarWidth;
+ position: relative;
+ }
+
+ .sidebar-container {
+ transition: width 0.28s;
+ width: $sideBarWidth !important;
+ background-color: $menuBg;
+ height: 100%;
+ position: fixed;
+ font-size: 0px;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1001;
+ overflow: hidden;
+
+ // reset element-ui css
+ .horizontal-collapse-transition {
+ transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+ }
+
+ .scrollbar-wrapper {
+ overflow-x: hidden !important;
+ }
+
+ .el-scrollbar__bar.is-vertical {
+ right: 0px;
+ }
+
+ .el-scrollbar {
+ height: 100%;
+ }
+
+ &.has-logo {
+ .el-scrollbar {
+ height: calc(100% - 50px);
+ }
+ }
+
+ .is-horizontal {
+ display: none;
+ }
+
+ a {
+ display: inline-block;
+ width: 100%;
+ overflow: hidden;
+ }
+
+ .svg-icon {
+ margin-right: 16px;
+ }
+
+ .sub-el-icon {
+ margin-right: 12px;
+ margin-left: -2px;
+ }
+
+ .el-menu {
+ border: none;
+ height: 100%;
+ width: 100% !important;
+ }
+
+ // menu hover
+ .submenu-title-noDropdown,
+ .el-submenu__title {
+ &:hover {
+ background-color: $menuHover !important;
+ }
+ }
+
+ .is-active>.el-submenu__title {
+ color: $subMenuActiveText !important;
+ }
+
+ & .nest-menu .el-submenu>.el-submenu__title,
+ & .el-submenu .el-menu-item {
+ min-width: $sideBarWidth !important;
+ background-color: $subMenuBg !important;
+
+ &:hover {
+ background-color: $subMenuHover !important;
+ }
+ }
+ }
+
+ .hideSidebar {
+ .sidebar-container {
+ width: 54px !important;
+ }
+
+ .main-container {
+ margin-left: 54px;
+ }
+
+ .submenu-title-noDropdown {
+ padding: 0 !important;
+ position: relative;
+
+ .el-tooltip {
+ padding: 0 !important;
+
+ .svg-icon {
+ margin-left: 20px;
+ }
+
+ .sub-el-icon {
+ margin-left: 19px;
+ }
+ }
+ }
+
+ .el-submenu {
+ overflow: hidden;
+
+ &>.el-submenu__title {
+ padding: 0 !important;
+
+ .svg-icon {
+ margin-left: 20px;
+ }
+
+ .sub-el-icon {
+ margin-left: 19px;
+ }
+
+ .el-submenu__icon-arrow {
+ display: none;
+ }
+ }
+ }
+
+ .el-menu--collapse {
+ .el-submenu {
+ &>.el-submenu__title {
+ &>span {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+
+ .el-menu--collapse .el-menu .el-submenu {
+ min-width: $sideBarWidth !important;
+ }
+
+ // mobile responsive
+ .mobile {
+ .main-container {
+ margin-left: 0px;
+ }
+
+ .sidebar-container {
+ transition: transform .28s;
+ width: $sideBarWidth !important;
+ }
+
+ &.hideSidebar {
+ .sidebar-container {
+ pointer-events: none;
+ transition-duration: 0.3s;
+ transform: translate3d(-$sideBarWidth, 0, 0);
+ }
+ }
+ }
+
+ .withoutAnimation {
+
+ .main-container,
+ .sidebar-container {
+ transition: none;
+ }
+ }
+}
+
+// when menu collapsed
+.el-menu--vertical {
+ &>.el-menu {
+ .svg-icon {
+ margin-right: 16px;
+ }
+ .sub-el-icon {
+ margin-right: 12px;
+ margin-left: -2px;
+ }
+ }
+
+ .nest-menu .el-submenu>.el-submenu__title,
+ .el-menu-item {
+ &:hover {
+ // you can use $subMenuHover
+ background-color: $menuHover !important;
+ }
+ }
+
+ // the scroll bar appears when the subMenu is too long
+ >.el-menu--popup {
+ max-height: 100vh;
+ overflow-y: auto;
+
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+ }
+}
diff --git a/src/styles/transition.scss b/src/styles/transition.scss
new file mode 100644
index 0000000..4cb27cc
--- /dev/null
+++ b/src/styles/transition.scss
@@ -0,0 +1,48 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+ opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+ transition: all .5s;
+}
+
+.fade-transform-enter {
+ opacity: 0;
+ transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+ opacity: 0;
+ transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+ transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+ opacity: 0;
+ transform: translateX(20px);
+}
+
+.breadcrumb-move {
+ transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+ position: absolute;
+}
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
new file mode 100644
index 0000000..a19c27c
--- /dev/null
+++ b/src/styles/variables.scss
@@ -0,0 +1,35 @@
+// base color
+$blue:#324157;
+$light-blue:#3A71A8;
+$red:#C03639;
+$pink: #E65D6E;
+$green: #30B08F;
+$tiffany: #4AB7BD;
+$yellow:#FEC171;
+$panGreen: #30B08F;
+
+// sidebar
+$menuText:#bfcbd9;
+$menuActiveText:#409EFF;
+$subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951
+
+$menuBg:#304156;
+$menuHover:#263445;
+
+$subMenuBg:#1f2d3d;
+$subMenuHover:#001528;
+
+$sideBarWidth: 210px;
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+ menuText: $menuText;
+ menuActiveText: $menuActiveText;
+ subMenuActiveText: $subMenuActiveText;
+ menuBg: $menuBg;
+ menuHover: $menuHover;
+ subMenuBg: $subMenuBg;
+ subMenuHover: $subMenuHover;
+ sideBarWidth: $sideBarWidth;
+}
diff --git a/src/utils/auth.js b/src/utils/auth.js
new file mode 100644
index 0000000..08a43d6
--- /dev/null
+++ b/src/utils/auth.js
@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'Admin-Token'
+
+export function getToken() {
+ return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+ return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+ return Cookies.remove(TokenKey)
+}
diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js
new file mode 100644
index 0000000..cf5b07a
--- /dev/null
+++ b/src/utils/clipboard.js
@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import Clipboard from 'clipboard'
+
+function clipboardSuccess() {
+ Vue.prototype.$message({
+ message: 'Copy successfully',
+ type: 'success',
+ duration: 1500
+ })
+}
+
+function clipboardError() {
+ Vue.prototype.$message({
+ message: 'Copy failed',
+ type: 'error'
+ })
+}
+
+export default function handleClipboard(text, event) {
+ const clipboard = new Clipboard(event.target, {
+ text: () => text
+ })
+ clipboard.on('success', () => {
+ clipboardSuccess()
+ clipboard.destroy()
+ })
+ clipboard.on('error', () => {
+ clipboardError()
+ clipboard.destroy()
+ })
+ clipboard.onClick(event)
+}
diff --git a/src/utils/error-log.js b/src/utils/error-log.js
new file mode 100644
index 0000000..a7f5b55
--- /dev/null
+++ b/src/utils/error-log.js
@@ -0,0 +1,35 @@
+import Vue from 'vue'
+import store from '@/store'
+import { isString, isArray } from '@/utils/validate'
+import settings from '@/settings'
+
+// you can set in settings.js
+// errorLog:'production' | ['production', 'development']
+const { errorLog: needErrorLog } = settings
+
+function checkNeed() {
+ const env = process.env.NODE_ENV
+ if (isString(needErrorLog)) {
+ return env === needErrorLog
+ }
+ if (isArray(needErrorLog)) {
+ return needErrorLog.includes(env)
+ }
+ return false
+}
+
+if (checkNeed()) {
+ Vue.config.errorHandler = function(err, vm, info, a) {
+ // Don't ask me why I use Vue.nextTick, it just a hack.
+ // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500
+ Vue.nextTick(() => {
+ store.dispatch('errorLog/addErrorLog', {
+ err,
+ vm,
+ info,
+ url: window.location.href
+ })
+ console.error(err, info)
+ })
+ }
+}
diff --git a/src/utils/get-page-title.js b/src/utils/get-page-title.js
new file mode 100644
index 0000000..cab7fd9
--- /dev/null
+++ b/src/utils/get-page-title.js
@@ -0,0 +1,10 @@
+import defaultSettings from '@/settings'
+
+const title = defaultSettings.title || 'Vue Element Admin'
+
+export default function getPageTitle(pageTitle) {
+ if (pageTitle) {
+ return `${pageTitle} - ${title}`
+ }
+ return `${title}`
+}
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 0000000..3225d3c
--- /dev/null
+++ b/src/utils/index.js
@@ -0,0 +1,357 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * Parse the time to string
+ * @param {(Object|string|number)} time
+ * @param {string} cFormat
+ * @returns {string | null}
+ */
+export function parseTime(time, cFormat) {
+ if (arguments.length === 0 || !time) {
+ return null
+ }
+ const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+ let date
+ if (typeof time === 'object') {
+ date = time
+ } else {
+ if ((typeof time === 'string')) {
+ if ((/^[0-9]+$/.test(time))) {
+ // support "1548221490638"
+ time = parseInt(time)
+ } else {
+ // support safari
+ // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
+ time = time.replace(new RegExp(/-/gm), '/')
+ }
+ }
+
+ if ((typeof time === 'number') && (time.toString().length === 10)) {
+ time = time * 1000
+ }
+ date = new Date(time)
+ }
+ const formatObj = {
+ y: date.getFullYear(),
+ m: date.getMonth() + 1,
+ d: date.getDate(),
+ h: date.getHours(),
+ i: date.getMinutes(),
+ s: date.getSeconds(),
+ a: date.getDay()
+ }
+ const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+ const value = formatObj[key]
+ // Note: getDay() returns 0 on Sunday
+ if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
+ return value.toString().padStart(2, '0')
+ })
+ return time_str
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+ if (('' + time).length === 10) {
+ time = parseInt(time) * 1000
+ } else {
+ time = +time
+ }
+ const d = new Date(time)
+ const now = Date.now()
+
+ const diff = (now - d) / 1000
+
+ if (diff < 30) {
+ return '刚刚'
+ } else if (diff < 3600) {
+ // less 1 hour
+ return Math.ceil(diff / 60) + '分钟前'
+ } else if (diff < 3600 * 24) {
+ return Math.ceil(diff / 3600) + '小时前'
+ } else if (diff < 3600 * 24 * 2) {
+ return '1天前'
+ }
+ if (option) {
+ return parseTime(time, option)
+ } else {
+ return (
+ d.getMonth() +
+ 1 +
+ '月' +
+ d.getDate() +
+ '日' +
+ d.getHours() +
+ '时' +
+ d.getMinutes() +
+ '分'
+ )
+ }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function getQueryObject(url) {
+ url = url == null ? window.location.href : url
+ const search = url.substring(url.lastIndexOf('?') + 1)
+ const obj = {}
+ const reg = /([^?&=]+)=([^?&=]*)/g
+ search.replace(reg, (rs, $1, $2) => {
+ const name = decodeURIComponent($1)
+ let val = decodeURIComponent($2)
+ val = String(val)
+ obj[name] = val
+ return rs
+ })
+ return obj
+}
+
+/**
+ * @param {string} input value
+ * @returns {number} output value
+ */
+export function byteLength(str) {
+ // returns the byte length of an utf8 string
+ let s = str.length
+ for (var i = str.length - 1; i >= 0; i--) {
+ const code = str.charCodeAt(i)
+ if (code > 0x7f && code <= 0x7ff) s++
+ else if (code > 0x7ff && code <= 0xffff) s += 2
+ if (code >= 0xDC00 && code <= 0xDFFF) i--
+ }
+ return s
+}
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export function cleanArray(actual) {
+ const newArray = []
+ for (let i = 0; i < actual.length; i++) {
+ if (actual[i]) {
+ newArray.push(actual[i])
+ }
+ }
+ return newArray
+}
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export function param(json) {
+ if (!json) return ''
+ return cleanArray(
+ Object.keys(json).map(key => {
+ if (json[key] === undefined) return ''
+ return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
+ })
+ ).join('&')
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+ const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+ if (!search) {
+ return {}
+ }
+ const obj = {}
+ const searchArr = search.split('&')
+ searchArr.forEach(v => {
+ const index = v.indexOf('=')
+ if (index !== -1) {
+ const name = v.substring(0, index)
+ const val = v.substring(index + 1, v.length)
+ obj[name] = val
+ }
+ })
+ return obj
+}
+
+/**
+ * @param {string} val
+ * @returns {string}
+ */
+export function html2Text(val) {
+ const div = document.createElement('div')
+ div.innerHTML = val
+ return div.textContent || div.innerText
+}
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export function objectMerge(target, source) {
+ if (typeof target !== 'object') {
+ target = {}
+ }
+ if (Array.isArray(source)) {
+ return source.slice()
+ }
+ Object.keys(source).forEach(property => {
+ const sourceProperty = source[property]
+ if (typeof sourceProperty === 'object') {
+ target[property] = objectMerge(target[property], sourceProperty)
+ } else {
+ target[property] = sourceProperty
+ }
+ })
+ return target
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {string} className
+ */
+export function toggleClass(element, className) {
+ if (!element || !className) {
+ return
+ }
+ let classString = element.className
+ const nameIndex = classString.indexOf(className)
+ if (nameIndex === -1) {
+ classString += '' + className
+ } else {
+ classString =
+ classString.substr(0, nameIndex) +
+ classString.substr(nameIndex + className.length)
+ }
+ element.className = classString
+}
+
+/**
+ * @param {string} type
+ * @returns {Date}
+ */
+export function getTime(type) {
+ if (type === 'start') {
+ return new Date().getTime() - 3600 * 1000 * 24 * 90
+ } else {
+ return new Date(new Date().toDateString())
+ }
+}
+
+/**
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {*}
+ */
+export function debounce(func, wait, immediate) {
+ let timeout, args, context, timestamp, result
+
+ const later = function() {
+ // 据上一次触发时间间隔
+ const last = +new Date() - timestamp
+
+ // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
+ if (last < wait && last > 0) {
+ timeout = setTimeout(later, wait - last)
+ } else {
+ timeout = null
+ // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
+ if (!immediate) {
+ result = func.apply(context, args)
+ if (!timeout) context = args = null
+ }
+ }
+ }
+
+ return function(...args) {
+ context = this
+ timestamp = +new Date()
+ const callNow = immediate && !timeout
+ // 如果延时不存在,重新设定延时
+ if (!timeout) timeout = setTimeout(later, wait)
+ if (callNow) {
+ result = func.apply(context, args)
+ context = args = null
+ }
+
+ return result
+ }
+}
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+export function deepClone(source) {
+ if (!source && typeof source !== 'object') {
+ throw new Error('error arguments', 'deepClone')
+ }
+ const targetObj = source.constructor === Array ? [] : {}
+ Object.keys(source).forEach(keys => {
+ if (source[keys] && typeof source[keys] === 'object') {
+ targetObj[keys] = deepClone(source[keys])
+ } else {
+ targetObj[keys] = source[keys]
+ }
+ })
+ return targetObj
+}
+
+/**
+ * @param {Array} arr
+ * @returns {Array}
+ */
+export function uniqueArr(arr) {
+ return Array.from(new Set(arr))
+}
+
+/**
+ * @returns {string}
+ */
+export function createUniqueString() {
+ const timestamp = +new Date() + ''
+ const randomNum = parseInt((1 + Math.random()) * 65536) + ''
+ return (+(randomNum + timestamp)).toString(32)
+}
+
+/**
+ * Check if an element has a class
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export function hasClass(ele, cls) {
+ return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
+}
+
+/**
+ * Add class to element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function addClass(ele, cls) {
+ if (!hasClass(ele, cls)) ele.className += ' ' + cls
+}
+
+/**
+ * Remove class from element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function removeClass(ele, cls) {
+ if (hasClass(ele, cls)) {
+ const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
+ ele.className = ele.className.replace(reg, ' ')
+ }
+}
diff --git a/src/utils/open-window.js b/src/utils/open-window.js
new file mode 100644
index 0000000..1a655d7
--- /dev/null
+++ b/src/utils/open-window.js
@@ -0,0 +1,25 @@
+/**
+ *Created by PanJiaChen on 16/11/29.
+ * @param {Sting} url
+ * @param {Sting} title
+ * @param {Number} w
+ * @param {Number} h
+ */
+export default function openWindow(url, title, w, h) {
+ // Fixes dual-screen position Most browsers Firefox
+ const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
+ const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top
+
+ const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width
+ const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height
+
+ const left = ((width / 2) - (w / 2)) + dualScreenLeft
+ const top = ((height / 2) - (h / 2)) + dualScreenTop
+ const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left)
+
+ // Puts focus on the newWindow
+ if (window.focus) {
+ newWindow.focus()
+ }
+}
+
diff --git a/src/utils/permission.js b/src/utils/permission.js
new file mode 100644
index 0000000..8e2bbad
--- /dev/null
+++ b/src/utils/permission.js
@@ -0,0 +1,21 @@
+import store from '@/store'
+
+/**
+ * @param {Array} value
+ * @returns {Boolean}
+ * @example see @/views/permission/directive.vue
+ */
+export default function checkPermission(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const roles = store.getters && store.getters.roles
+ const permissionRoles = value
+
+ const hasPermission = roles.some(role => {
+ return permissionRoles.includes(role)
+ })
+ return hasPermission
+ } else {
+ console.error(`need roles! Like v-permission="['admin','editor']"`)
+ return false
+ }
+}
diff --git a/src/utils/request.js b/src/utils/request.js
new file mode 100644
index 0000000..2fb95ac
--- /dev/null
+++ b/src/utils/request.js
@@ -0,0 +1,85 @@
+import axios from 'axios'
+import { MessageBox, Message } from 'element-ui'
+import store from '@/store'
+import { getToken } from '@/utils/auth'
+
+// create an axios instance
+const service = axios.create({
+ baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
+ // withCredentials: true, // send cookies when cross-domain requests
+ timeout: 5000 // request timeout
+})
+
+// request interceptor
+service.interceptors.request.use(
+ config => {
+ // do something before request is sent
+
+ if (store.getters.token) {
+ // let each request carry token
+ // ['X-Token'] is a custom headers key
+ // please modify it according to the actual situation
+ config.headers['X-Token'] = getToken()
+ }
+ return config
+ },
+ error => {
+ // do something with request error
+ console.log(error) // for debug
+ return Promise.reject(error)
+ }
+)
+
+// response interceptor
+service.interceptors.response.use(
+ /**
+ * If you want to get http information such as headers or status
+ * Please return response => response
+ */
+
+ /**
+ * Determine the request status by custom code
+ * Here is just an example
+ * You can also judge the status by HTTP Status Code
+ */
+ response => {
+ const res = response.data
+
+ // if the custom code is not 20000, it is judged as an error.
+ if (res.code !== 20000) {
+ Message({
+ message: res.message || 'Error',
+ type: 'error',
+ duration: 5 * 1000
+ })
+
+ // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
+ if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
+ // to re-login
+ MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
+ confirmButtonText: 'Re-Login',
+ cancelButtonText: 'Cancel',
+ type: 'warning'
+ }).then(() => {
+ store.dispatch('user/resetToken').then(() => {
+ location.reload()
+ })
+ })
+ }
+ return Promise.reject(new Error(res.message || 'Error'))
+ } else {
+ return res
+ }
+ },
+ error => {
+ console.log('err' + error) // for debug
+ Message({
+ message: error.message,
+ type: 'error',
+ duration: 5 * 1000
+ })
+ return Promise.reject(error)
+ }
+)
+
+export default service
diff --git a/src/utils/scroll-to.js b/src/utils/scroll-to.js
new file mode 100644
index 0000000..c5d8e04
--- /dev/null
+++ b/src/utils/scroll-to.js
@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+ t /= d / 2
+ if (t < 1) {
+ return c / 2 * t * t + b
+ }
+ t--
+ return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move(amount) {
+ document.documentElement.scrollTop = amount
+ document.body.parentNode.scrollTop = amount
+ document.body.scrollTop = amount
+}
+
+function position() {
+ return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo(to, duration, callback) {
+ const start = position()
+ const change = to - start
+ const increment = 20
+ let currentTime = 0
+ duration = (typeof (duration) === 'undefined') ? 500 : duration
+ var animateScroll = function() {
+ // increment the time
+ currentTime += increment
+ // find the value with the quadratic in-out easing function
+ var val = Math.easeInOutQuad(currentTime, start, change, duration)
+ // move the document.body
+ move(val)
+ // do the animation unless its over
+ if (currentTime < duration) {
+ requestAnimFrame(animateScroll)
+ } else {
+ if (callback && typeof (callback) === 'function') {
+ // the animation is done so lets callback
+ callback()
+ }
+ }
+ }
+ animateScroll()
+}
diff --git a/src/utils/validate.js b/src/utils/validate.js
new file mode 100644
index 0000000..6b3ac41
--- /dev/null
+++ b/src/utils/validate.js
@@ -0,0 +1,87 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+ return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+ const valid_map = ['admin', 'editor']
+ return valid_map.indexOf(str.trim()) >= 0
+}
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+ const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+ return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+ const reg = /^[a-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+ const reg = /^[A-Z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+ const reg = /^[A-Za-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+ const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+ if (typeof str === 'string' || str instanceof String) {
+ return true
+ }
+ return false
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+ if (typeof Array.isArray === 'undefined') {
+ return Object.prototype.toString.call(arg) === '[object Array]'
+ }
+ return Array.isArray(arg)
+}
diff --git a/src/vendor/Export2Excel.js b/src/vendor/Export2Excel.js
new file mode 100644
index 0000000..d8a2af3
--- /dev/null
+++ b/src/vendor/Export2Excel.js
@@ -0,0 +1,220 @@
+/* eslint-disable */
+import { saveAs } from 'file-saver'
+import XLSX from 'xlsx'
+
+function generateArray(table) {
+ var out = [];
+ var rows = table.querySelectorAll('tr');
+ var ranges = [];
+ for (var R = 0; R < rows.length; ++R) {
+ var outRow = [];
+ var row = rows[R];
+ var columns = row.querySelectorAll('td');
+ for (var C = 0; C < columns.length; ++C) {
+ var cell = columns[C];
+ var colspan = cell.getAttribute('colspan');
+ var rowspan = cell.getAttribute('rowspan');
+ var cellValue = cell.innerText;
+ if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;
+
+ //Skip ranges
+ ranges.forEach(function (range) {
+ if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
+ for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);
+ }
+ });
+
+ //Handle Row Span
+ if (rowspan || colspan) {
+ rowspan = rowspan || 1;
+ colspan = colspan || 1;
+ ranges.push({
+ s: {
+ r: R,
+ c: outRow.length
+ },
+ e: {
+ r: R + rowspan - 1,
+ c: outRow.length + colspan - 1
+ }
+ });
+ };
+
+ //Handle Value
+ outRow.push(cellValue !== "" ? cellValue : null);
+
+ //Handle Colspan
+ if (colspan)
+ for (var k = 0; k < colspan - 1; ++k) outRow.push(null);
+ }
+ out.push(outRow);
+ }
+ return [out, ranges];
+};
+
+function datenum(v, date1904) {
+ if (date1904) v += 1462;
+ var epoch = Date.parse(v);
+ return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
+}
+
+function sheet_from_array_of_arrays(data, opts) {
+ var ws = {};
+ var range = {
+ s: {
+ c: 10000000,
+ r: 10000000
+ },
+ e: {
+ c: 0,
+ r: 0
+ }
+ };
+ for (var R = 0; R != data.length; ++R) {
+ for (var C = 0; C != data[R].length; ++C) {
+ if (range.s.r > R) range.s.r = R;
+ if (range.s.c > C) range.s.c = C;
+ if (range.e.r < R) range.e.r = R;
+ if (range.e.c < C) range.e.c = C;
+ var cell = {
+ v: data[R][C]
+ };
+ if (cell.v == null) continue;
+ var cell_ref = XLSX.utils.encode_cell({
+ c: C,
+ r: R
+ });
+
+ if (typeof cell.v === 'number') cell.t = 'n';
+ else if (typeof cell.v === 'boolean') cell.t = 'b';
+ else if (cell.v instanceof Date) {
+ cell.t = 'n';
+ cell.z = XLSX.SSF._table[14];
+ cell.v = datenum(cell.v);
+ } else cell.t = 's';
+
+ ws[cell_ref] = cell;
+ }
+ }
+ if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
+ return ws;
+}
+
+function Workbook() {
+ if (!(this instanceof Workbook)) return new Workbook();
+ this.SheetNames = [];
+ this.Sheets = {};
+}
+
+function s2ab(s) {
+ var buf = new ArrayBuffer(s.length);
+ var view = new Uint8Array(buf);
+ for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
+ return buf;
+}
+
+export function export_table_to_excel(id) {
+ var theTable = document.getElementById(id);
+ var oo = generateArray(theTable);
+ var ranges = oo[1];
+
+ /* original data */
+ var data = oo[0];
+ var ws_name = "SheetJS";
+
+ var wb = new Workbook(),
+ ws = sheet_from_array_of_arrays(data);
+
+ /* add ranges to worksheet */
+ // ws['!cols'] = ['apple', 'banan'];
+ ws['!merges'] = ranges;
+
+ /* add worksheet to workbook */
+ wb.SheetNames.push(ws_name);
+ wb.Sheets[ws_name] = ws;
+
+ var wbout = XLSX.write(wb, {
+ bookType: 'xlsx',
+ bookSST: false,
+ type: 'binary'
+ });
+
+ saveAs(new Blob([s2ab(wbout)], {
+ type: "application/octet-stream"
+ }), "test.xlsx")
+}
+
+export function export_json_to_excel({
+ multiHeader = [],
+ header,
+ data,
+ filename,
+ merges = [],
+ autoWidth = true,
+ bookType = 'xlsx'
+} = {}) {
+ /* original data */
+ filename = filename || 'excel-list'
+ data = [...data]
+ data.unshift(header);
+
+ for (let i = multiHeader.length - 1; i > -1; i--) {
+ data.unshift(multiHeader[i])
+ }
+
+ var ws_name = "SheetJS";
+ var wb = new Workbook(),
+ ws = sheet_from_array_of_arrays(data);
+
+ if (merges.length > 0) {
+ if (!ws['!merges']) ws['!merges'] = [];
+ merges.forEach(item => {
+ ws['!merges'].push(XLSX.utils.decode_range(item))
+ })
+ }
+
+ if (autoWidth) {
+ /*设置worksheet每列的最大宽度*/
+ const colWidth = data.map(row => row.map(val => {
+ /*先判断是否为null/undefined*/
+ if (val == null) {
+ return {
+ 'wch': 10
+ };
+ }
+ /*再判断是否为中文*/
+ else if (val.toString().charCodeAt(0) > 255) {
+ return {
+ 'wch': val.toString().length * 2
+ };
+ } else {
+ return {
+ 'wch': val.toString().length
+ };
+ }
+ }))
+ /*以第一行为初始值*/
+ let result = colWidth[0];
+ for (let i = 1; i < colWidth.length; i++) {
+ for (let j = 0; j < colWidth[i].length; j++) {
+ if (result[j]['wch'] < colWidth[i][j]['wch']) {
+ result[j]['wch'] = colWidth[i][j]['wch'];
+ }
+ }
+ }
+ ws['!cols'] = result;
+ }
+
+ /* add worksheet to workbook */
+ wb.SheetNames.push(ws_name);
+ wb.Sheets[ws_name] = ws;
+
+ var wbout = XLSX.write(wb, {
+ bookType: bookType,
+ bookSST: false,
+ type: 'binary'
+ });
+ saveAs(new Blob([s2ab(wbout)], {
+ type: "application/octet-stream"
+ }), `${filename}.${bookType}`);
+}
diff --git a/src/vendor/Export2Zip.js b/src/vendor/Export2Zip.js
new file mode 100644
index 0000000..db70707
--- /dev/null
+++ b/src/vendor/Export2Zip.js
@@ -0,0 +1,24 @@
+/* eslint-disable */
+import { saveAs } from 'file-saver'
+import JSZip from 'jszip'
+
+export function export_txt_to_zip(th, jsonData, txtName, zipName) {
+ const zip = new JSZip()
+ const txt_name = txtName || 'file'
+ const zip_name = zipName || 'file'
+ const data = jsonData
+ let txtData = `${th}\r\n`
+ data.forEach((row) => {
+ let tempStr = ''
+ tempStr = row.toString()
+ txtData += `${tempStr}\r\n`
+ })
+ zip.file(`${txt_name}.txt`, txtData)
+ zip.generateAsync({
+ type: "blob"
+ }).then((blob) => {
+ saveAs(blob, `${zip_name}.zip`)
+ }, (err) => {
+ alert('导出失败')
+ })
+}
diff --git a/src/views/charts/keyboard.vue b/src/views/charts/keyboard.vue
new file mode 100644
index 0000000..917f8ee
--- /dev/null
+++ b/src/views/charts/keyboard.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/charts/line.vue b/src/views/charts/line.vue
new file mode 100644
index 0000000..fea1497
--- /dev/null
+++ b/src/views/charts/line.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/charts/mix-chart.vue b/src/views/charts/mix-chart.vue
new file mode 100644
index 0000000..c57db75
--- /dev/null
+++ b/src/views/charts/mix-chart.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/clipboard/index.vue b/src/views/clipboard/index.vue
new file mode 100644
index 0000000..4a6bdd1
--- /dev/null
+++ b/src/views/clipboard/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ copy
+
+
+
+
+
+ copy
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/avatar-upload.vue b/src/views/components-demo/avatar-upload.vue
new file mode 100644
index 0000000..5cb20a5
--- /dev/null
+++ b/src/views/components-demo/avatar-upload.vue
@@ -0,0 +1,61 @@
+
+
+
This is based on
+ vue-image-crop-upload .
+ Since I was using only the vue@1 version, and it is not compatible with mockjs at the moment, I modified it myself, and if you are going to use it, it is better to use official version.
+
+
+
+
+
+ Change Avatar
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/back-to-top.vue b/src/views/components-demo/back-to-top.vue
new file mode 100644
index 0000000..df5370b
--- /dev/null
+++ b/src/views/components-demo/back-to-top.vue
@@ -0,0 +1,154 @@
+
+
+
+ When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner
+
+
+ You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally
+
+
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/count-to.vue b/src/views/components-demo/count-to.vue
new file mode 100644
index 0000000..70681df
--- /dev/null
+++ b/src/views/components-demo/count-to.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+
<count-to :start-val='{{ _startVal }}' :end-val='{{ _endVal }}' :duration='{{ _duration }}'
+ :decimals='{{ _decimals }}' :separator='{{ _separator }}' :prefix='{{ _prefix }}' :suffix='{{ _suffix }}'
+ :autoplay=false>
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/dnd-list.vue b/src/views/components-demo/dnd-list.vue
new file mode 100644
index 0000000..e299fa6
--- /dev/null
+++ b/src/views/components-demo/dnd-list.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
diff --git a/src/views/components-demo/drag-dialog.vue b/src/views/components-demo/drag-dialog.vue
new file mode 100644
index 0000000..c815b28
--- /dev/null
+++ b/src/views/components-demo/drag-dialog.vue
@@ -0,0 +1,61 @@
+
+
+
+ open a Drag Dialog
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/drag-kanban.vue b/src/views/components-demo/drag-kanban.vue
new file mode 100644
index 0000000..943be45
--- /dev/null
+++ b/src/views/components-demo/drag-kanban.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/drag-select.vue b/src/views/components-demo/drag-select.vue
new file mode 100644
index 0000000..905ecb9
--- /dev/null
+++ b/src/views/components-demo/drag-select.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+ {{ item }}
+
+
+
+
+
+
diff --git a/src/views/components-demo/dropzone.vue b/src/views/components-demo/dropzone.vue
new file mode 100644
index 0000000..a8c1040
--- /dev/null
+++ b/src/views/components-demo/dropzone.vue
@@ -0,0 +1,31 @@
+
+
+
+ Based on dropzone .
+ Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.
+
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/json-editor.vue b/src/views/components-demo/json-editor.vue
new file mode 100644
index 0000000..85bf383
--- /dev/null
+++ b/src/views/components-demo/json-editor.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/markdown.vue b/src/views/components-demo/markdown.vue
new file mode 100644
index 0000000..25cf3e3
--- /dev/null
+++ b/src/views/components-demo/markdown.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+ Basic:
+
+
+
+
+
+
+ Markdown Mode:
+
+
+
+
+
+
+ Customize Toolbar:
+
+
+
+
+
+
+ I18n:
+
+
+
+
+
+
+ Get HTML
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/mixin.vue b/src/views/components-demo/mixin.vue
new file mode 100644
index 0000000..425cf4f
--- /dev/null
+++ b/src/views/components-demo/mixin.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+ Buttons
+
+
+
+
+ Documentation
+
+
+
+
+ Icons
+
+
+
+
+ Excel
+
+
+
+
+ Table
+
+
+
+
+ Form
+
+
+
+
+ Theme
+
+
+
+
+
+
+
+
+
+
+ Material Design 的input
+
+
+
+
+
+ 标题
+
+
+
+
+
+
+
+
+
+
+ 图片hover效果
+
+
+
+ vue-element-admin
+
+
+
+
+
+
+
+
+ 水波纹 waves v-directive
+
+
+
+ 水波纹效果
+
+
+
+
+
+
+
+
+ hover text
+
+
+
+
+
+
+
+
+
+
+
+
+ Share
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/split-pane.vue b/src/views/components-demo/split-pane.vue
new file mode 100644
index 0000000..7dba353
--- /dev/null
+++ b/src/views/components-demo/split-pane.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/sticky.vue b/src/views/components-demo/sticky.vue
new file mode 100644
index 0000000..f01d088
--- /dev/null
+++ b/src/views/components-demo/sticky.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
+ Platform
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
+ Link
+
+
+
+
+ Url
+
+
+
+
+
+
+
+
+
+
+ publish
+
+
+
+
+
+ Sticky header, When the page is scrolled to the preset position will be sticky on the top.
+
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
+ placeholder
+
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
placeholder
+
+
+
+
+
+
+
diff --git a/src/views/components-demo/tinymce.vue b/src/views/components-demo/tinymce.vue
new file mode 100644
index 0000000..f03389a
--- /dev/null
+++ b/src/views/components-demo/tinymce.vue
@@ -0,0 +1,36 @@
+
+
+
+ Rich text is a core feature of the management backend, but at the same time it is a place with lots of pits. In the process of selecting rich texts, I also took a lot of detours. The common rich texts on the market have been basically used, and I finally chose Tinymce. See the more detailed rich text comparison and introduction.
+ Documentation
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/BarChart.vue b/src/views/dashboard/admin/components/BarChart.vue
new file mode 100644
index 0000000..be0af34
--- /dev/null
+++ b/src/views/dashboard/admin/components/BarChart.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/BoxCard.vue b/src/views/dashboard/admin/components/BoxCard.vue
new file mode 100644
index 0000000..1ec1d37
--- /dev/null
+++ b/src/views/dashboard/admin/components/BoxCard.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+ Vue
+
+
+
+ JavaScript
+
+
+
+ CSS
+
+
+
+ ESLint
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/LineChart.vue b/src/views/dashboard/admin/components/LineChart.vue
new file mode 100644
index 0000000..e654168
--- /dev/null
+++ b/src/views/dashboard/admin/components/LineChart.vue
@@ -0,0 +1,135 @@
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/PanelGroup.vue b/src/views/dashboard/admin/components/PanelGroup.vue
new file mode 100644
index 0000000..589236e
--- /dev/null
+++ b/src/views/dashboard/admin/components/PanelGroup.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/PieChart.vue b/src/views/dashboard/admin/components/PieChart.vue
new file mode 100644
index 0000000..4d2ef32
--- /dev/null
+++ b/src/views/dashboard/admin/components/PieChart.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/RaddarChart.vue b/src/views/dashboard/admin/components/RaddarChart.vue
new file mode 100644
index 0000000..52c8f9f
--- /dev/null
+++ b/src/views/dashboard/admin/components/RaddarChart.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/TodoList/Todo.vue b/src/views/dashboard/admin/components/TodoList/Todo.vue
new file mode 100644
index 0000000..c4b3cae
--- /dev/null
+++ b/src/views/dashboard/admin/components/TodoList/Todo.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/TodoList/index.scss b/src/views/dashboard/admin/components/TodoList/index.scss
new file mode 100644
index 0000000..74ce0d5
--- /dev/null
+++ b/src/views/dashboard/admin/components/TodoList/index.scss
@@ -0,0 +1,320 @@
+.todoapp {
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ color: #4d4d4d;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto ;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-weight: 300;
+ background: #fff;
+ z-index: 1;
+ position: relative;
+ button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ :focus {
+ outline: 0;
+ }
+ .hidden {
+ display: none;
+ }
+ .todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+ }
+ .todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+ }
+ .todoapp input::-moz-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+ }
+ .todoapp input::input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+ }
+ .todoapp h1 {
+ position: absolute;
+ top: -155px;
+ width: 100%;
+ font-size: 100px;
+ font-weight: 100;
+ text-align: center;
+ color: rgba(175, 47, 47, 0.15);
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+ }
+ .new-todo,
+ .edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 18px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ border: 0;
+ color: inherit;
+ padding: 6px;
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ .new-todo {
+ padding: 10px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
+ }
+ .main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+ }
+ .toggle-all {
+ text-align: center;
+ border: none;
+ /* Mobile Safari */
+ opacity: 0;
+ position: absolute;
+ }
+ .toggle-all+label {
+ width: 60px;
+ height: 34px;
+ font-size: 0;
+ position: absolute;
+ top: -52px;
+ left: -13px;
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+ }
+ .toggle-all+label:before {
+ content: '❯';
+ font-size: 22px;
+ color: #e6e6e6;
+ padding: 10px 27px 10px 27px;
+ }
+ .toggle-all:checked+label:before {
+ color: #737373;
+ }
+ .todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+ .todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+ }
+ .todo-list li:last-child {
+ border-bottom: none;
+ }
+ .todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+ }
+ .todo-list li.editing .edit {
+ display: block;
+ width: 506px;
+ padding: 12px 16px;
+ margin: 0 0 0 43px;
+ }
+ .todo-list li.editing .view {
+ display: none;
+ }
+ .todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ /* auto, since non-WebKit browsers doesn't support input styling */
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none;
+ /* Mobile Safari */
+ -webkit-appearance: none;
+ appearance: none;
+ }
+ .todo-list li .toggle {
+ opacity: 0;
+ }
+ .todo-list li .toggle+label {
+ /*
+ Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
+ IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
+ */
+ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
+ background-repeat: no-repeat;
+ background-position: center left;
+ background-size: 36px;
+ }
+ .todo-list li .toggle:checked+label {
+ background-size: 36px;
+ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
+ }
+ .todo-list li label {
+ word-break: break-all;
+ padding: 15px 15px 15px 50px;
+ display: block;
+ line-height: 1.0;
+ font-size: 14px;
+ transition: color 0.4s;
+ }
+ .todo-list li.completed label {
+ color: #d9d9d9;
+ text-decoration: line-through;
+ }
+ .todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #cc9a9a;
+ transition: color 0.2s ease-out;
+ cursor: pointer;
+ }
+ .todo-list li .destroy:hover {
+ color: #af5b5e;
+ }
+ .todo-list li .destroy:after {
+ content: '×';
+ }
+ .todo-list li:hover .destroy {
+ display: block;
+ }
+ .todo-list li .edit {
+ display: none;
+ }
+ .todo-list li.editing:last-child {
+ margin-bottom: -1px;
+ }
+ .footer {
+ color: #777;
+ position: relative;
+ padding: 10px 15px;
+ height: 40px;
+ text-align: center;
+ border-top: 1px solid #e6e6e6;
+ }
+ .footer:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 40px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+ }
+ .todo-count {
+ float: left;
+ text-align: left;
+ }
+ .todo-count strong {
+ font-weight: 300;
+ }
+ .filters {
+ margin: 0;
+ padding: 0;
+ position: relative;
+ z-index: 1;
+ list-style: none;
+ }
+ .filters li {
+ display: inline;
+ }
+ .filters li a {
+ color: inherit;
+ font-size: 12px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ }
+ .filters li a:hover {
+ border-color: rgba(175, 47, 47, 0.1);
+ }
+ .filters li a.selected {
+ border-color: rgba(175, 47, 47, 0.2);
+ }
+ .clear-completed,
+ html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ cursor: pointer;
+ }
+ .clear-completed:hover {
+ text-decoration: underline;
+ }
+ .info {
+ margin: 65px auto 0;
+ color: #bfbfbf;
+ font-size: 10px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-align: center;
+ }
+ .info p {
+ line-height: 1;
+ }
+ .info a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 400;
+ }
+ .info a:hover {
+ text-decoration: underline;
+ }
+ /*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox
+*/
+ @media screen and (-webkit-min-device-pixel-ratio:0) {
+ .toggle-all,
+ .todo-list li .toggle {
+ background: none;
+ }
+ .todo-list li .toggle {
+ height: 40px;
+ }
+ }
+ @media (max-width: 430px) {
+ .footer {
+ height: 50px;
+ }
+ .filters {
+ bottom: 10px;
+ }
+ }
+}
diff --git a/src/views/dashboard/admin/components/TodoList/index.vue b/src/views/dashboard/admin/components/TodoList/index.vue
new file mode 100644
index 0000000..8000d41
--- /dev/null
+++ b/src/views/dashboard/admin/components/TodoList/index.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/TransactionTable.vue b/src/views/dashboard/admin/components/TransactionTable.vue
new file mode 100644
index 0000000..d07b0ed
--- /dev/null
+++ b/src/views/dashboard/admin/components/TransactionTable.vue
@@ -0,0 +1,55 @@
+
+
+
+
+ {{ scope.row.order_no | orderNoFilter }}
+
+
+
+
+ ¥{{ scope.row.price | toThousandFilter }}
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/admin/components/mixins/resize.js b/src/views/dashboard/admin/components/mixins/resize.js
new file mode 100644
index 0000000..234953b
--- /dev/null
+++ b/src/views/dashboard/admin/components/mixins/resize.js
@@ -0,0 +1,55 @@
+import { debounce } from '@/utils'
+
+export default {
+ data() {
+ return {
+ $_sidebarElm: null,
+ $_resizeHandler: null
+ }
+ },
+ mounted() {
+ this.$_resizeHandler = debounce(() => {
+ if (this.chart) {
+ this.chart.resize()
+ }
+ }, 100)
+ this.$_initResizeEvent()
+ this.$_initSidebarResizeEvent()
+ },
+ beforeDestroy() {
+ this.$_destroyResizeEvent()
+ this.$_destroySidebarResizeEvent()
+ },
+ // to fixed bug when cached by keep-alive
+ // https://github.com/PanJiaChen/vue-element-admin/issues/2116
+ activated() {
+ this.$_initResizeEvent()
+ this.$_initSidebarResizeEvent()
+ },
+ deactivated() {
+ this.$_destroyResizeEvent()
+ this.$_destroySidebarResizeEvent()
+ },
+ methods: {
+ // use $_ for mixins properties
+ // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
+ $_initResizeEvent() {
+ window.addEventListener('resize', this.$_resizeHandler)
+ },
+ $_destroyResizeEvent() {
+ window.removeEventListener('resize', this.$_resizeHandler)
+ },
+ $_sidebarResizeHandler(e) {
+ if (e.propertyName === 'width') {
+ this.$_resizeHandler()
+ }
+ },
+ $_initSidebarResizeEvent() {
+ this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
+ this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
+ },
+ $_destroySidebarResizeEvent() {
+ this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
+ }
+ }
+}
diff --git a/src/views/dashboard/admin/index.vue b/src/views/dashboard/admin/index.vue
new file mode 100644
index 0000000..8cb557b
--- /dev/null
+++ b/src/views/dashboard/admin/index.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/editor/index.vue b/src/views/dashboard/editor/index.vue
new file mode 100644
index 0000000..9723bcc
--- /dev/null
+++ b/src/views/dashboard/editor/index.vue
@@ -0,0 +1,74 @@
+
+
+
+
+ Your roles:
+ {{ item }}
+
+
+
+ {{ name }}
+ Editor's Dashboard
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue
new file mode 100644
index 0000000..1720ea8
--- /dev/null
+++ b/src/views/dashboard/index.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/src/views/documentation/index.vue b/src/views/documentation/index.vue
new file mode 100644
index 0000000..09f5951
--- /dev/null
+++ b/src/views/documentation/index.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
diff --git a/src/views/error-log/components/ErrorTestA.vue b/src/views/error-log/components/ErrorTestA.vue
new file mode 100644
index 0000000..52654e0
--- /dev/null
+++ b/src/views/error-log/components/ErrorTestA.vue
@@ -0,0 +1,13 @@
+
+
+
+ {{ a.a }}
+
+
+
+
+
diff --git a/src/views/error-log/components/ErrorTestB.vue b/src/views/error-log/components/ErrorTestB.vue
new file mode 100644
index 0000000..d796bee
--- /dev/null
+++ b/src/views/error-log/components/ErrorTestB.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/src/views/error-log/index.vue b/src/views/error-log/index.vue
new file mode 100644
index 0000000..e999c85
--- /dev/null
+++ b/src/views/error-log/index.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
Please click the bug icon in the upper right corner
+
+ Now the management system are basically the form of the spa, it enhances the user experience, but it also increases the possibility of page problems, a small negligence may lead to the entire page deadlock. Fortunately Vue provides a way to catch handling exceptions, where you can handle errors or report exceptions.
+
+ Document introduction
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/error-page/401.vue b/src/views/error-page/401.vue
new file mode 100644
index 0000000..a52ed23
--- /dev/null
+++ b/src/views/error-page/401.vue
@@ -0,0 +1,99 @@
+
+
+
+ 返回
+
+
+
+
+ Oops!
+
+ gif来源airbnb 页面
+ 你没有权限去该页面
+ 如有不满请联系你领导
+
+ 或者你可以去:
+
+
+ 回首页
+
+
+
+ 随便看看
+
+ 点我看图
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/error-page/404.vue b/src/views/error-page/404.vue
new file mode 100644
index 0000000..1791f55
--- /dev/null
+++ b/src/views/error-page/404.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
OOPS!
+
+
{{ message }}
+
Please check that the URL you entered is correct, or click the button below to return to the homepage.
+
Back to home
+
+
+
+
+
+
+
+
diff --git a/src/views/example/components/ArticleDetail.vue b/src/views/example/components/ArticleDetail.vue
new file mode 100644
index 0000000..157497b
--- /dev/null
+++ b/src/views/example/components/ArticleDetail.vue
@@ -0,0 +1,289 @@
+
+
+
+
+
+
+
+
+
+ Publish
+
+
+ Draft
+
+
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ contentShortLength }}words
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/example/components/Dropdown/Comment.vue b/src/views/example/components/Dropdown/Comment.vue
new file mode 100644
index 0000000..d34b2b9
--- /dev/null
+++ b/src/views/example/components/Dropdown/Comment.vue
@@ -0,0 +1,41 @@
+
+
+
+ {{ !comment_disabled?'Comment: opened':'Comment: closed' }}
+
+
+
+
+
+
+ Close comment
+
+
+ Open comment
+
+
+
+
+
+
+
+
diff --git a/src/views/example/components/Dropdown/Platform.vue b/src/views/example/components/Dropdown/Platform.vue
new file mode 100644
index 0000000..0a52726
--- /dev/null
+++ b/src/views/example/components/Dropdown/Platform.vue
@@ -0,0 +1,46 @@
+
+
+
+ Platfroms({{ platforms.length }})
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+
diff --git a/src/views/example/components/Dropdown/SourceUrl.vue b/src/views/example/components/Dropdown/SourceUrl.vue
new file mode 100644
index 0000000..8f47485
--- /dev/null
+++ b/src/views/example/components/Dropdown/SourceUrl.vue
@@ -0,0 +1,38 @@
+
+
+
+ Link
+
+
+
+
+
+
+ URL
+
+
+
+
+
+
+
+
diff --git a/src/views/example/components/Dropdown/index.js b/src/views/example/components/Dropdown/index.js
new file mode 100644
index 0000000..bc0c171
--- /dev/null
+++ b/src/views/example/components/Dropdown/index.js
@@ -0,0 +1,3 @@
+export { default as CommentDropdown } from './Comment'
+export { default as PlatformDropdown } from './Platform'
+export { default as SourceUrlDropdown } from './SourceUrl'
diff --git a/src/views/example/components/Warning.vue b/src/views/example/components/Warning.vue
new file mode 100644
index 0000000..8d2a7e5
--- /dev/null
+++ b/src/views/example/components/Warning.vue
@@ -0,0 +1,13 @@
+
+
+ Creating and editing pages cannot be cached by keep-alive because keep-alive include does not currently support
+ caching based on routes, so it is currently cached based on component name. If you want to achieve a similar caching
+ effect, you can use a browser caching scheme such as localStorage. Or do not use keep-alive include to cache all
+ pages directly. See details
+ Document
+
+
+
diff --git a/src/views/example/create.vue b/src/views/example/create.vue
new file mode 100644
index 0000000..f28ce28
--- /dev/null
+++ b/src/views/example/create.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/src/views/example/edit.vue b/src/views/example/edit.vue
new file mode 100644
index 0000000..87b6126
--- /dev/null
+++ b/src/views/example/edit.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/src/views/example/list.vue b/src/views/example/list.vue
new file mode 100644
index 0000000..7cdc4ac
--- /dev/null
+++ b/src/views/example/list.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+ {{ scope.row.id }}
+
+
+
+
+
+ {{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}
+
+
+
+
+
+ {{ scope.row.author }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+
+
+
+ {{ row.title }}
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/excel/components/AutoWidthOption.vue b/src/views/excel/components/AutoWidthOption.vue
new file mode 100644
index 0000000..9050e65
--- /dev/null
+++ b/src/views/excel/components/AutoWidthOption.vue
@@ -0,0 +1,34 @@
+
+
+ Cell Auto-Width:
+
+
+ True
+
+
+ False
+
+
+
+
+
+
diff --git a/src/views/excel/components/BookTypeOption.vue b/src/views/excel/components/BookTypeOption.vue
new file mode 100644
index 0000000..af9fed3
--- /dev/null
+++ b/src/views/excel/components/BookTypeOption.vue
@@ -0,0 +1,39 @@
+
+
+ Book Type:
+
+
+
+
+
+
+
diff --git a/src/views/excel/components/FilenameOption.vue b/src/views/excel/components/FilenameOption.vue
new file mode 100644
index 0000000..7509223
--- /dev/null
+++ b/src/views/excel/components/FilenameOption.vue
@@ -0,0 +1,27 @@
+
+
+ Filename:
+
+
+
+
+
diff --git a/src/views/excel/export-excel.vue b/src/views/excel/export-excel.vue
new file mode 100644
index 0000000..b4d7500
--- /dev/null
+++ b/src/views/excel/export-excel.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+ {{ scope.$index }}
+
+
+
+
+ {{ scope.row.title }}
+
+
+
+
+ {{ scope.row.author }}
+
+
+
+
+ {{ scope.row.pageviews }}
+
+
+
+
+
+ {{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/views/excel/merge-header.vue b/src/views/excel/merge-header.vue
new file mode 100644
index 0000000..4be0227
--- /dev/null
+++ b/src/views/excel/merge-header.vue
@@ -0,0 +1,101 @@
+
+
+
+ Export
+
+
+
+
+ {{ scope.$index }}
+
+
+
+
+
+ {{ scope.row.title }}
+
+
+
+
+ {{ scope.row.author }}
+
+
+
+
+ {{ scope.row.pageviews }}
+
+
+
+
+
+
+ {{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}
+
+
+
+
+
+
+
+
diff --git a/src/views/excel/select-excel.vue b/src/views/excel/select-excel.vue
new file mode 100644
index 0000000..05b85fb
--- /dev/null
+++ b/src/views/excel/select-excel.vue
@@ -0,0 +1,107 @@
+
+
+
+
+ Export Selected Items
+
+
+ Documentation
+
+
+
+
+
+ {{ scope.$index }}
+
+
+
+
+ {{ scope.row.title }}
+
+
+
+
+ {{ scope.row.author }}
+
+
+
+
+ {{ scope.row.pageviews }}
+
+
+
+
+
+ {{ scope.row.display_time }}
+
+
+
+
+
+
+
diff --git a/src/views/excel/upload-excel.vue b/src/views/excel/upload-excel.vue
new file mode 100644
index 0000000..1772b7f
--- /dev/null
+++ b/src/views/excel/upload-excel.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/guide/index.vue b/src/views/guide/index.vue
new file mode 100644
index 0000000..4897c8f
--- /dev/null
+++ b/src/views/guide/index.vue
@@ -0,0 +1,36 @@
+
+
+
+ The guide page is useful for some people who entered the project for the first time. You can briefly introduce the
+ features of the project. Demo is based on
+ driver.js.
+
+
+ Show Guide
+
+
+
+
+
diff --git a/src/views/guide/steps.js b/src/views/guide/steps.js
new file mode 100644
index 0000000..283c672
--- /dev/null
+++ b/src/views/guide/steps.js
@@ -0,0 +1,53 @@
+const steps = [
+ {
+ element: '#hamburger-container',
+ popover: {
+ title: 'Hamburger',
+ description: 'Open && Close sidebar',
+ position: 'bottom'
+ }
+ },
+ {
+ element: '#breadcrumb-container',
+ popover: {
+ title: 'Breadcrumb',
+ description: 'Indicate the current page location',
+ position: 'bottom'
+ }
+ },
+ {
+ element: '#header-search',
+ popover: {
+ title: 'Page Search',
+ description: 'Page search, quick navigation',
+ position: 'left'
+ }
+ },
+ {
+ element: '#screenfull',
+ popover: {
+ title: 'Screenfull',
+ description: 'Set the page into fullscreen',
+ position: 'left'
+ }
+ },
+ {
+ element: '#size-select',
+ popover: {
+ title: 'Switch Size',
+ description: 'Switch the system size',
+ position: 'left'
+ }
+ },
+ {
+ element: '#tags-view-container',
+ popover: {
+ title: 'Tags view',
+ description: 'The history of the page you visited',
+ position: 'bottom'
+ },
+ padding: 0
+ }
+]
+
+export default steps
diff --git a/src/views/icons/element-icons.js b/src/views/icons/element-icons.js
new file mode 100644
index 0000000..9ea4d63
--- /dev/null
+++ b/src/views/icons/element-icons.js
@@ -0,0 +1,3 @@
+const elementIcons = ['platform-eleme', 'eleme', 'delete-solid', 'delete', 's-tools', 'setting', 'user-solid', 'user', 'phone', 'phone-outline', 'more', 'more-outline', 'star-on', 'star-off', 's-goods', 'goods', 'warning', 'warning-outline', 'question', 'info', 'remove', 'circle-plus', 'success', 'error', 'zoom-in', 'zoom-out', 'remove-outline', 'circle-plus-outline', 'circle-check', 'circle-close', 's-help', 'help', 'minus', 'plus', 'check', 'close', 'picture', 'picture-outline', 'picture-outline-round', 'upload', 'upload2', 'download', 'camera-solid', 'camera', 'video-camera-solid', 'video-camera', 'message-solid', 'bell', 's-cooperation', 's-order', 's-platform', 's-fold', 's-unfold', 's-operation', 's-promotion', 's-home', 's-release', 's-ticket', 's-management', 's-open', 's-shop', 's-marketing', 's-flag', 's-comment', 's-finance', 's-claim', 's-custom', 's-opportunity', 's-data', 's-check', 's-grid', 'menu', 'share', 'd-caret', 'caret-left', 'caret-right', 'caret-bottom', 'caret-top', 'bottom-left', 'bottom-right', 'back', 'right', 'bottom', 'top', 'top-left', 'top-right', 'arrow-left', 'arrow-right', 'arrow-down', 'arrow-up', 'd-arrow-left', 'd-arrow-right', 'video-pause', 'video-play', 'refresh', 'refresh-right', 'refresh-left', 'finished', 'sort', 'sort-up', 'sort-down', 'rank', 'loading', 'view', 'c-scale-to-original', 'date', 'edit', 'edit-outline', 'folder', 'folder-opened', 'folder-add', 'folder-remove', 'folder-delete', 'folder-checked', 'tickets', 'document-remove', 'document-delete', 'document-copy', 'document-checked', 'document', 'document-add', 'printer', 'paperclip', 'takeaway-box', 'search', 'monitor', 'attract', 'mobile', 'scissors', 'umbrella', 'headset', 'brush', 'mouse', 'coordinate', 'magic-stick', 'reading', 'data-line', 'data-board', 'pie-chart', 'data-analysis', 'collection-tag', 'film', 'suitcase', 'suitcase-1', 'receiving', 'collection', 'files', 'notebook-1', 'notebook-2', 'toilet-paper', 'office-building', 'school', 'table-lamp', 'house', 'no-smoking', 'smoking', 'shopping-cart-full', 'shopping-cart-1', 'shopping-cart-2', 'shopping-bag-1', 'shopping-bag-2', 'sold-out', 'sell', 'present', 'box', 'bank-card', 'money', 'coin', 'wallet', 'discount', 'price-tag', 'news', 'guide', 'male', 'female', 'thumb', 'cpu', 'link', 'connection', 'open', 'turn-off', 'set-up', 'chat-round', 'chat-line-round', 'chat-square', 'chat-dot-round', 'chat-dot-square', 'chat-line-square', 'message', 'postcard', 'position', 'turn-off-microphone', 'microphone', 'close-notification', 'bangzhu', 'time', 'odometer', 'crop', 'aim', 'switch-button', 'full-screen', 'copy-document', 'mic', 'stopwatch', 'medal-1', 'medal', 'trophy', 'trophy-1', 'first-aid-kit', 'discover', 'place', 'location', 'location-outline', 'location-information', 'add-location', 'delete-location', 'map-location', 'alarm-clock', 'timer', 'watch-1', 'watch', 'lock', 'unlock', 'key', 'service', 'mobile-phone', 'bicycle', 'truck', 'ship', 'basketball', 'football', 'soccer', 'baseball', 'wind-power', 'light-rain', 'lightning', 'heavy-rain', 'sunrise', 'sunrise-1', 'sunset', 'sunny', 'cloudy', 'partly-cloudy', 'cloudy-and-sunny', 'moon', 'moon-night', 'dish', 'dish-1', 'food', 'chicken', 'fork-spoon', 'knife-fork', 'burger', 'tableware', 'sugar', 'dessert', 'ice-cream', 'hot-water', 'water-cup', 'coffee-cup', 'cold-drink', 'goblet', 'goblet-full', 'goblet-square', 'goblet-square-full', 'refrigerator', 'grape', 'watermelon', 'cherry', 'apple', 'pear', 'orange', 'coffee', 'ice-tea', 'ice-drink', 'milk-tea', 'potato-strips', 'lollipop', 'ice-cream-square', 'ice-cream-round']
+
+export default elementIcons
diff --git a/src/views/icons/index.vue b/src/views/icons/index.vue
new file mode 100644
index 0000000..4c483db
--- /dev/null
+++ b/src/views/icons/index.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+ {{ generateIconCode(item) }}
+
+
+
+ {{ item }}
+
+
+
+
+
+
+
+
+
+
+ {{ generateElementIconCode(item) }}
+
+
+
+ {{ item }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/icons/svg-icons.js b/src/views/icons/svg-icons.js
new file mode 100644
index 0000000..1e3c66d
--- /dev/null
+++ b/src/views/icons/svg-icons.js
@@ -0,0 +1,10 @@
+const req = require.context('../../icons/svg', false, /\.svg$/)
+const requireAll = requireContext => requireContext.keys()
+
+const re = /\.\/(.*)\.svg/
+
+const svgIcons = requireAll(req).map(i => {
+ return i.match(re)[1]
+})
+
+export default svgIcons
diff --git a/src/views/login/auth-redirect.vue b/src/views/login/auth-redirect.vue
new file mode 100644
index 0000000..7df8934
--- /dev/null
+++ b/src/views/login/auth-redirect.vue
@@ -0,0 +1,15 @@
+
diff --git a/src/views/login/components/SocialSignin.vue b/src/views/login/components/SocialSignin.vue
new file mode 100644
index 0000000..e9bf4f2
--- /dev/null
+++ b/src/views/login/components/SocialSignin.vue
@@ -0,0 +1,72 @@
+
+
+
+
+ WeChat
+
+
+
+ QQ
+
+
+
+
+
+
+
diff --git a/src/views/login/index.vue b/src/views/login/index.vue
new file mode 100644
index 0000000..2590640
--- /dev/null
+++ b/src/views/login/index.vue
@@ -0,0 +1,324 @@
+
+
+
+
+
+
Login Form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Login
+
+
+
+ Username : admin
+ Password : any
+
+
+ Username : editor
+ Password : any
+
+
+
+ Or connect with
+
+
+
+
+
+ Can not be simulated on local, so please combine you own business simulation! ! !
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/nested/menu1/index.vue b/src/views/nested/menu1/index.vue
new file mode 100644
index 0000000..30cb670
--- /dev/null
+++ b/src/views/nested/menu1/index.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/views/nested/menu1/menu1-1/index.vue b/src/views/nested/menu1/menu1-1/index.vue
new file mode 100644
index 0000000..27e173a
--- /dev/null
+++ b/src/views/nested/menu1/menu1-1/index.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/views/nested/menu1/menu1-2/index.vue b/src/views/nested/menu1/menu1-2/index.vue
new file mode 100644
index 0000000..0c86276
--- /dev/null
+++ b/src/views/nested/menu1/menu1-2/index.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue b/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue
new file mode 100644
index 0000000..f87d88f
--- /dev/null
+++ b/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue b/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue
new file mode 100644
index 0000000..d88789f
--- /dev/null
+++ b/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/views/nested/menu1/menu1-3/index.vue b/src/views/nested/menu1/menu1-3/index.vue
new file mode 100644
index 0000000..f7cd073
--- /dev/null
+++ b/src/views/nested/menu1/menu1-3/index.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/views/nested/menu2/index.vue b/src/views/nested/menu2/index.vue
new file mode 100644
index 0000000..19dd48f
--- /dev/null
+++ b/src/views/nested/menu2/index.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/views/pdf/content.js b/src/views/pdf/content.js
new file mode 100644
index 0000000..e62b1a2
--- /dev/null
+++ b/src/views/pdf/content.js
@@ -0,0 +1,58 @@
+const title = 'Plans for the Next Iteration of Vue.js'
+
+const content = `Last week atVue.js London I gave a brief sneak peek of what’s coming in the next major version of Vue. This post provides an in-depth overview of the plan.
+
+Why a new majorversion?
+Vue 2.0 was releasedexactly two years ago (how time flies!). During this period, the core has remained backwards compatible with five minor releases. We’ve accumulated a number of ideas that would bring improvements, but they were held off because they would result in breaking changes. At the same time, the JavaScript ecosystem and the language itself has been evolving rapidly. There are greatly improved tools that could enhance our workflow, and many new language features that could unlock simpler, more complete, and more efficient solutions to the problems Vue is trying to solve. What’s more exciting is that we are seeing ES2015 support becoming a baseline for all major evergreen browsers. Vue 3.0 aims to leverage these new language features to make Vue core smaller, faster, and more powerful.
+Vue 3.0 is currently in prototyping phase, and we have already implemented a runtime close to feature-parity with 2.x.Many of the items listed below are either already implemented, or confirmed to be feasible. Ones that are not yet implemented or still in exploration phase are marked with a *.
+The Details
+High-Level APIChanges
+TL;DR: Everything except render function API and scoped-slots syntax will either remain the same or can be made 2.x compatible via a compatibility build.
+Since it’s a new major, there is going to be some breaking changes. However, we take backwards compatibility seriously, so we want to start communicating these changes as soon as possible. Here’s the currently planned public API changes:
+Template syntax will remain 99% the same. There may be small tweaks in scoped slots syntax, but other than that we have no plans to change anything else for templates. 3.0 will support class-based components natively, with the aim to provide an API that is pleasant to use in native ES2015 without requiring any transpilation or stage-x features. Most current options will have a reasonable mapping in the class-based API. Stage-x features such as class fields and decorators can still be used optionally to enhance the authoring experience. In addition, the API is designed with TypeScript type inference in mind. The 3.x codebase will itself be written in TypeScript, and providing improved TypeScript support. (That said, usage of TypeScript in an application is still entirely optional.) The 2.x object-based component format will still be supported by internally transforming the object to a corresponding class. Mixins will still be supported.* Top level APIs will likely receive an overhaul to avoid globally mutating the Vue runtime when installing plugins. Instead, plugins will be applied and scoped to a component tree. This will make it easier to test components that rely on specific plugins, and also make it possible to mount multiple Vue applications on the same page with different plugins, but using the same Vue runtime.* Functional components can finally be plain functions —however, async components will now need to be explicitly created via a helper function. The part that will receive the most changes is the Virtual DOM format used in render functions. We are currently collecting feedback from major library authors and will be sharing more details as we are more confident of the changes, but as long as you don’t heavily rely on hand-written (non-JSX) render functions in your app, upgrading should be a reasonably straightforward process.
+Source Code Architecture
+TL;DR: better decoupled internal modules, TypeScript, and a codebase that is easier to contribute to.
+We are re-writing 3.0 from the ground up for a cleaner and more maintainable architecture, in particular trying to make it easier to contribute to. We are breaking some internal functionalities into individual packages in order to isolate the scope of complexity. For example, the observer module will become its own package, with its own public API and tests. Note this does not affect framework-level API— you will not have to manually import individual bits from multiple packages in order to use Vue. Instead, the final Vue package is assembled using these internal packages.
+The codebase is also now written in TypeScript. Although this will make proficiency in TypeScript a pre-requisite for contributing to the new codebase, we believe the type information and IDE support will actually make it easier for a new contributor to make meaningful contributions.
+Decoupling the observer and scheduler into separate packages also allows us to easily experiment with alternative implementations of these parts. For example, we can implement an IE11 compatible observer implementation with the same API, or an alternative scheduler that leveragesrequestIdleCallbackto yield to the browser during long updates.*
+
+Observation Mechanism
+TL;DR: more complete, precise, efficient and debuggable reactivity tracking & API for creating observables.
+3.0 will ship with a Proxy-based observer implementation that provides reactivity tracking with full language coverage. This eliminates a number of limitations of Vue 2’s current implementation based onObject.defineProperty:
+The new observer also features the following:
+Easily understand why a component is re-rendering
+
+Other Runtime Improvements
+TL;DR: smaller, faster, tree-shakable features, fragments & portals, custom renderer API.
+Compiler Improvements*
+TL;DR: tree-shaking friendly output, more AOT optimizations, parser with better error info and source map support.
+IE11 Support*
+TL;DR: it will be supported, but in a separate build with the same reactivity limitations of Vue 2.x.
+The new codebase currently targets evergreen browsers only and assumes baseline native ES2015 support. But alas, we know a lot of our users still need to support IE11 for the foreseeable future. Most of the ES2015 features used can be transpiled / polyfilled for IE11, with the exception for Proxies. Our plan is to implement an alternative observer with the same API, but using the good old ES5Object.definePropertyAPI. A separate build of Vue 3.x will be distributed using this observer implementation. However, this build will be subject to the same change detection caveats of Vue 2.x and thus not fully compatible with the “modern” build of 3.x. We are aware that this imposes some inconvenience for library authors as they will need to be aware of compatibility for two different builds, but we will make sure to provide clear guidelines on this when we reach that stage.
+How Do We GetThere
+First of all, although we are announcing it today, we do not have a definitive timeline yet. What we do know at the moment is the steps we will be taking to get there:
+1. Internal Feedback for the Runtime Prototype
+This is the phase we are in right now. Currently, we already have a working runtime prototype that includes the new observer, Virtual DOM and component implementation. We have invited a group of authors of influential community projects to provide feedback for the internal changes, and would like to make sure they are comfortable with the changes before moving forward. We want to ensure that important libraries in the ecosystem will be ready at the same time when we release 3.0, so that users relying on those projects can upgrade easily.
+2. Public Feedback viaRFCs
+Once we gain a certain level of confidence in the new design, for each breaking change we will be opening a dedicated RFC issue which includes:
+We will anticipate public feedback from the wider community to help us consolidate these ideas.
+3. Introduce Compatible Features in 2.x &2.x-next
+We are not forgetting about 2.x! In fact, we plan to use 2.x to progressively accustom users to the new changes. We will be gradually introducing confirmed API changes into 2.x via opt-in adaptors, and 2.x-next will allow users to try out the new Proxy-based observer.
+The last minor release in 2.x will become LTS and continue to receive bug and security fixes for 18 months when 3.0 is released.
+4. AlphaPhase
+Next, we will finish up the compiler and server-side rendering parts of 3.0 and start making alpha releases. These will mostly be for stability testing purposes in small greenfield apps.
+5. BetaPhase
+During beta phase, our main goal is updating support libraries and tools like Vue Router, Vuex, Vue CLI, Vue DevTools and make sure they work smoothly with the new core. We will also be working with major library authors from the community to help them get ready for 3.0.
+6. RCPhase
+Once we consider the API and codebase stable, we will enter RC phase with API freeze. During this phase we will also work on a “compat build”: a build of 3.0 that includes compatibility layers for 2.x API. This build will also ship with a flag you can turn on to emit deprecation warnings for 2.x API usage in your app. The compat build can be used as a guide to upgrade your app to 3.0.
+7. IE11build
+The last task before the final release will be the IE11 compatibility build as mentioned above.
+8. FinalRelease
+In all honesty, we don’t know when this will happen yet, but likely in 2019. Again, we care more about shipping something that is solid and stable rather than hitting specific dates. There is a lot of work to be done, but we are excited for what’s coming next!
`
+
+const data = {
+ title,
+ content
+}
+
+export default data
diff --git a/src/views/pdf/download.vue b/src/views/pdf/download.vue
new file mode 100644
index 0000000..a348c69
--- /dev/null
+++ b/src/views/pdf/download.vue
@@ -0,0 +1,201 @@
+
+
+
+
+ {{ article.title }}
+
+
+
+ This article is from Evan You on
medium
+
+
+
+
+
+
+
+
diff --git a/src/views/pdf/index.vue b/src/views/pdf/index.vue
new file mode 100644
index 0000000..86278b3
--- /dev/null
+++ b/src/views/pdf/index.vue
@@ -0,0 +1,13 @@
+
+
+
+ Here we use window.print() to implement the feature of downloading PDF.
+
+
+
+ Click to download PDF
+
+
+
+
+
diff --git a/src/views/permission/components/SwitchRoles.vue b/src/views/permission/components/SwitchRoles.vue
new file mode 100644
index 0000000..9fabbe1
--- /dev/null
+++ b/src/views/permission/components/SwitchRoles.vue
@@ -0,0 +1,32 @@
+
+
+
+ Your roles: {{ roles }}
+
+ Switch roles:
+
+
+
+
+
+
+
+
diff --git a/src/views/permission/directive.vue b/src/views/permission/directive.vue
new file mode 100644
index 0000000..4a49792
--- /dev/null
+++ b/src/views/permission/directive.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+ Only
+ admin can see this
+
+
+ v-permission="['admin']"
+
+
+
+
+
+ Only
+ editor can see this
+
+
+ v-permission="['editor']"
+
+
+
+
+
+ Both
+ admin and
+ editor can see this
+
+
+ v-permission="['admin','editor']"
+
+
+
+
+
+
+ In some cases, using v-permission will have no effect. For example: Element-UI's Tab component or el-table-column and other scenes that dynamically render dom. You can only do this with v-if.
+ e.g.
+
+
+
+
+ Admin can see this
+
+ v-if="checkPermission(['admin'])"
+
+
+
+
+ Editor can see this
+
+ v-if="checkPermission(['editor'])"
+
+
+
+
+ Both admin or editor can see this
+
+ v-if="checkPermission(['admin','editor'])"
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/permission/page.vue b/src/views/permission/page.vue
new file mode 100644
index 0000000..5291a78
--- /dev/null
+++ b/src/views/permission/page.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/src/views/permission/role.vue b/src/views/permission/role.vue
new file mode 100644
index 0000000..cd7ecd4
--- /dev/null
+++ b/src/views/permission/role.vue
@@ -0,0 +1,270 @@
+
+
+
New Role
+
+
+
+
+ {{ scope.row.key }}
+
+
+
+
+ {{ scope.row.name }}
+
+
+
+
+ {{ scope.row.description }}
+
+
+
+
+ Edit
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+ Confirm
+
+
+
+
+
+
+
+
diff --git a/src/views/profile/components/Account.vue b/src/views/profile/components/Account.vue
new file mode 100644
index 0000000..9f2e386
--- /dev/null
+++ b/src/views/profile/components/Account.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+ Update
+
+
+
+
+
diff --git a/src/views/profile/components/Activity.vue b/src/views/profile/components/Activity.vue
new file mode 100644
index 0000000..dd5db3a
--- /dev/null
+++ b/src/views/profile/components/Activity.vue
@@ -0,0 +1,185 @@
+
+
+
+
+
+
Iron Man
+
Shared publicly - 7:30 PM today
+
+
+ Lorem ipsum represents a long-held tradition for designers,
+ typographers and the like. Some people hate it and argue for
+ its demise, but others ignore the hate as they create awesome
+ tools to help create filler text for everyone from bacon lovers
+ to Charlie Sheen fans.
+
+
+
+
+
+ Share
+
+
+
+
+
+ Like
+
+
+
+
+
+
+
+
Captain American
+
Sent you a message - yesterday
+
+
+ Lorem ipsum represents a long-held tradition for designers,
+ typographers and the like. Some people hate it and argue for
+ its demise, but others ignore the hate as they create awesome
+ tools to help create filler text for everyone from bacon lovers
+ to Charlie Sheen fans.
+
+
+
+
+
+ Share
+
+
+
+
+
+ Like
+
+
+
+
+
+
+
+
Spider Man
+
Posted 4 photos - 2 days ago
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/profile/components/Timeline.vue b/src/views/profile/components/Timeline.vue
new file mode 100644
index 0000000..ba90b3d
--- /dev/null
+++ b/src/views/profile/components/Timeline.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+ {{ item.title }}
+ {{ item.content }}
+
+
+
+
+
+
+
diff --git a/src/views/profile/components/UserCard.vue b/src/views/profile/components/UserCard.vue
new file mode 100644
index 0000000..2476f04
--- /dev/null
+++ b/src/views/profile/components/UserCard.vue
@@ -0,0 +1,134 @@
+
+
+
+ About me
+
+
+
+
+
+ Hello
+ {{ user.role }}
+
+
+
+
{{ user.name }}
+
{{ user.role | uppercaseFirst }}
+
+
+
+
+
+
+
+
+ JS in Computer Science from the University of Technology
+
+
+
+
+
+
+
+
+ Vue
+
+
+
+ JavaScript
+
+
+
+ Css
+
+
+
+ ESLint
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/profile/index.vue b/src/views/profile/index.vue
new file mode 100644
index 0000000..87e4f94
--- /dev/null
+++ b/src/views/profile/index.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/qiniu/upload.vue b/src/views/qiniu/upload.vue
new file mode 100644
index 0000000..9dc9aed
--- /dev/null
+++ b/src/views/qiniu/upload.vue
@@ -0,0 +1,41 @@
+
+
+
+
+ 将文件拖到此处,或点击上传
+
+
+
+
+
diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue
new file mode 100644
index 0000000..db4c1d6
--- /dev/null
+++ b/src/views/redirect/index.vue
@@ -0,0 +1,12 @@
+
diff --git a/src/views/tab/components/TabPane.vue b/src/views/tab/components/TabPane.vue
new file mode 100644
index 0000000..3fb1439
--- /dev/null
+++ b/src/views/tab/components/TabPane.vue
@@ -0,0 +1,103 @@
+
+
+
+
+ {{ scope.row.id }}
+
+
+
+
+
+ {{ scope.row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}
+
+
+
+
+
+ {{ row.title }}
+ {{ row.type }}
+
+
+
+
+
+ {{ scope.row.author }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.pageviews }}
+
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+
+
+
+
diff --git a/src/views/tab/index.vue b/src/views/tab/index.vue
new file mode 100644
index 0000000..2a35fa5
--- /dev/null
+++ b/src/views/tab/index.vue
@@ -0,0 +1,57 @@
+
+
+ mounted times :{{ createdTimes }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/table/complex-table.vue b/src/views/table/complex-table.vue
new file mode 100644
index 0000000..295c5fc
--- /dev/null
+++ b/src/views/table/complex-table.vue
@@ -0,0 +1,379 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search
+
+
+ Add
+
+
+ Export
+
+
+ reviewer
+
+
+
+
+
+
+ {{ row.id }}
+
+
+
+
+ {{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}
+
+
+
+
+ {{ row.title }}
+ {{ row.type | typeFilter }}
+
+
+
+
+ {{ row.author }}
+
+
+
+
+ {{ row.reviewer }}
+
+
+
+
+
+
+
+
+
+ {{ row.pageviews }}
+ 0
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+
+
+ Edit
+
+
+ Publish
+
+
+ Draft
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/table/drag-table.vue b/src/views/table/drag-table.vue
new file mode 100644
index 0000000..3b5c890
--- /dev/null
+++ b/src/views/table/drag-table.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+ {{ row.id }}
+
+
+
+
+
+ {{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}
+
+
+
+
+
+ {{ row.title }}
+
+
+
+
+
+ {{ row.author }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.pageviews }}
+
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+
+
+
+
+
+
+
+ The default order : {{ oldList }}
+
+
+ The after dragging order : {{ newList }}
+
+
+
+
+
+
+
+
+
diff --git a/src/views/table/dynamic-table/components/FixedThead.vue b/src/views/table/dynamic-table/components/FixedThead.vue
new file mode 100644
index 0000000..c3deb92
--- /dev/null
+++ b/src/views/table/dynamic-table/components/FixedThead.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+ apple
+
+
+ banana
+
+
+ orange
+
+
+
+
+
+
+
+
+ {{ scope.row[fruit] }}
+
+
+
+
+
+
+
+
diff --git a/src/views/table/dynamic-table/components/UnfixedThead.vue b/src/views/table/dynamic-table/components/UnfixedThead.vue
new file mode 100644
index 0000000..831b070
--- /dev/null
+++ b/src/views/table/dynamic-table/components/UnfixedThead.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+ apple
+
+
+ banana
+
+
+ orange
+
+
+
+
+
+
+
+
+ {{ scope.row[fruit] }}
+
+
+
+
+
+
+
diff --git a/src/views/table/dynamic-table/index.vue b/src/views/table/dynamic-table/index.vue
new file mode 100644
index 0000000..5a4dd36
--- /dev/null
+++ b/src/views/table/dynamic-table/index.vue
@@ -0,0 +1,24 @@
+
+
+
+ Fixed header, sorted by header order,
+
+
+
+
+ Not fixed header, sorted by click order
+
+
+
+
+
+
+
diff --git a/src/views/table/inline-edit-table.vue b/src/views/table/inline-edit-table.vue
new file mode 100644
index 0000000..31e0065
--- /dev/null
+++ b/src/views/table/inline-edit-table.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+ {{ row.id }}
+
+
+
+
+
+ {{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}
+
+
+
+
+
+ {{ row.author }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.status }}
+
+
+
+
+
+
+
+
+
+ cancel
+
+
+ {{ row.title }}
+
+
+
+
+
+
+ Ok
+
+
+ Edit
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/theme/index.vue b/src/views/theme/index.vue
new file mode 100644
index 0000000..0af7711
--- /dev/null
+++ b/src/views/theme/index.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
Change Theme :
+
+
+ Tips: It is different from the theme-pick on the navbar is two different skinning methods, each with different application scenarios. Refer to the documentation for details.
+
+
+
+
+
+
+ Primary
+
+
+ Success
+
+
+ Info
+
+
+ Warning
+
+
+ Danger
+
+
+
+
+
+
+
+
+ Search
+
+
+ Upload
+
+
+
+
+
+
+ {{ tag.name }}
+
+
+
+
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/zip/index.vue b/src/views/zip/index.vue
new file mode 100644
index 0000000..412608f
--- /dev/null
+++ b/src/views/zip/index.vue
@@ -0,0 +1,77 @@
+
+
+
+
+ Export Zip
+
+
+
+
+ {{ scope.$index }}
+
+
+
+
+ {{ scope.row.title }}
+
+
+
+
+ {{ scope.row.author }}
+
+
+
+
+ {{ scope.row.pageviews }}
+
+
+
+
+
+ {{ scope.row.display_time }}
+
+
+
+
+
+
+
diff --git a/tests/unit/.eslintrc.js b/tests/unit/.eslintrc.js
new file mode 100644
index 0000000..958d51b
--- /dev/null
+++ b/tests/unit/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = {
+ env: {
+ jest: true
+ }
+}
diff --git a/tests/unit/components/Hamburger.spec.js b/tests/unit/components/Hamburger.spec.js
new file mode 100644
index 0000000..01ea303
--- /dev/null
+++ b/tests/unit/components/Hamburger.spec.js
@@ -0,0 +1,18 @@
+import { shallowMount } from '@vue/test-utils'
+import Hamburger from '@/components/Hamburger/index.vue'
+describe('Hamburger.vue', () => {
+ it('toggle click', () => {
+ const wrapper = shallowMount(Hamburger)
+ const mockFn = jest.fn()
+ wrapper.vm.$on('toggleClick', mockFn)
+ wrapper.find('.hamburger').trigger('click')
+ expect(mockFn).toBeCalled()
+ })
+ it('prop isActive', () => {
+ const wrapper = shallowMount(Hamburger)
+ wrapper.setProps({ isActive: true })
+ expect(wrapper.contains('.is-active')).toBe(true)
+ wrapper.setProps({ isActive: false })
+ expect(wrapper.contains('.is-active')).toBe(false)
+ })
+})
diff --git a/tests/unit/components/SvgIcon.spec.js b/tests/unit/components/SvgIcon.spec.js
new file mode 100644
index 0000000..31467a9
--- /dev/null
+++ b/tests/unit/components/SvgIcon.spec.js
@@ -0,0 +1,22 @@
+import { shallowMount } from '@vue/test-utils'
+import SvgIcon from '@/components/SvgIcon/index.vue'
+describe('SvgIcon.vue', () => {
+ it('iconClass', () => {
+ const wrapper = shallowMount(SvgIcon, {
+ propsData: {
+ iconClass: 'test'
+ }
+ })
+ expect(wrapper.find('use').attributes().href).toBe('#icon-test')
+ })
+ it('className', () => {
+ const wrapper = shallowMount(SvgIcon, {
+ propsData: {
+ iconClass: 'test'
+ }
+ })
+ expect(wrapper.classes().length).toBe(1)
+ wrapper.setProps({ className: 'test' })
+ expect(wrapper.classes().includes('test')).toBe(true)
+ })
+})
diff --git a/tests/unit/utils/formatTime.spec.js b/tests/unit/utils/formatTime.spec.js
new file mode 100644
index 0000000..d07e414
--- /dev/null
+++ b/tests/unit/utils/formatTime.spec.js
@@ -0,0 +1,29 @@
+import { formatTime } from '@/utils/index.js'
+describe('Utils:formatTime', () => {
+ const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
+ const retrofit = 5 * 1000
+
+ it('ten digits timestamp', () => {
+ expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
+ })
+ it('test now', () => {
+ expect(formatTime(+new Date() - 1)).toBe('刚刚')
+ })
+ it('less two minute', () => {
+ expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
+ })
+ it('less two hour', () => {
+ expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
+ })
+ it('less one day', () => {
+ expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
+ })
+ it('more than one day', () => {
+ expect(formatTime(d)).toBe('7月13日17时54分')
+ })
+ it('format', () => {
+ expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
+ expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
+ expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
+ })
+})
diff --git a/tests/unit/utils/param2Obj.spec.js b/tests/unit/utils/param2Obj.spec.js
new file mode 100644
index 0000000..e106ed8
--- /dev/null
+++ b/tests/unit/utils/param2Obj.spec.js
@@ -0,0 +1,14 @@
+import { param2Obj } from '@/utils/index.js'
+describe('Utils:param2Obj', () => {
+ const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95'
+
+ it('param2Obj test', () => {
+ expect(param2Obj(url)).toEqual({
+ name: 'bill',
+ age: '29',
+ sex: '1',
+ field: window.btoa('test'),
+ key: '测试'
+ })
+ })
+})
diff --git a/tests/unit/utils/parseTime.spec.js b/tests/unit/utils/parseTime.spec.js
new file mode 100644
index 0000000..3e138d0
--- /dev/null
+++ b/tests/unit/utils/parseTime.spec.js
@@ -0,0 +1,37 @@
+import { parseTime } from '@/utils/index.js'
+
+describe('Utils:parseTime', () => {
+ const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
+ it('timestamp', () => {
+ expect(parseTime(d)).toBe('2018-07-13 17:54:01')
+ })
+
+ it('timestamp string', () => {
+ expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01')
+ })
+
+ it('ten digits timestamp', () => {
+ expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
+ })
+ it('new Date', () => {
+ expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
+ })
+ it('format', () => {
+ expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
+ expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
+ expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
+ })
+ it('get the day of the week', () => {
+ expect(parseTime(d, '{a}')).toBe('五') // 星期五
+ })
+ it('get the day of the week', () => {
+ expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
+ })
+ it('empty argument', () => {
+ expect(parseTime()).toBeNull()
+ })
+
+ it('null', () => {
+ expect(parseTime(null)).toBeNull()
+ })
+})
diff --git a/tests/unit/utils/validate.spec.js b/tests/unit/utils/validate.spec.js
new file mode 100644
index 0000000..ef2efe6
--- /dev/null
+++ b/tests/unit/utils/validate.spec.js
@@ -0,0 +1,28 @@
+import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets } from '@/utils/validate.js'
+describe('Utils:validate', () => {
+ it('validUsername', () => {
+ expect(validUsername('admin')).toBe(true)
+ expect(validUsername('editor')).toBe(true)
+ expect(validUsername('xxxx')).toBe(false)
+ })
+ it('validURL', () => {
+ expect(validURL('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
+ expect(validURL('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
+ expect(validURL('github.com/PanJiaChen/vue-element-admin')).toBe(false)
+ })
+ it('validLowerCase', () => {
+ expect(validLowerCase('abc')).toBe(true)
+ expect(validLowerCase('Abc')).toBe(false)
+ expect(validLowerCase('123abc')).toBe(false)
+ })
+ it('validUpperCase', () => {
+ expect(validUpperCase('ABC')).toBe(true)
+ expect(validUpperCase('Abc')).toBe(false)
+ expect(validUpperCase('123ABC')).toBe(false)
+ })
+ it('validAlphabets', () => {
+ expect(validAlphabets('ABC')).toBe(true)
+ expect(validAlphabets('Abc')).toBe(true)
+ expect(validAlphabets('123aBC')).toBe(false)
+ })
+})
diff --git a/vue.config.js b/vue.config.js
new file mode 100644
index 0000000..33a6348
--- /dev/null
+++ b/vue.config.js
@@ -0,0 +1,124 @@
+'use strict'
+const path = require('path')
+const defaultSettings = require('./src/settings.js')
+
+function resolve(dir) {
+ return path.join(__dirname, dir)
+}
+
+const name = defaultSettings.title || 'vue Element Admin' // page title
+
+// If your port is set to 80,
+// use administrator privileges to execute the command line.
+// For example, Mac: sudo npm run
+// You can change the port by the following method:
+// port = 9527 npm run dev OR npm run dev --port = 9527
+const port = process.env.port || process.env.npm_config_port || 9527 // dev port
+
+// All configuration item explanations can be find in https://cli.vuejs.org/config/
+module.exports = {
+ /**
+ * You will need to set publicPath if you plan to deploy your site under a sub path,
+ * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
+ * then publicPath should be set to "/bar/".
+ * In most cases please use '/' !!!
+ * Detail: https://cli.vuejs.org/config/#publicpath
+ */
+ publicPath: '/',
+ outputDir: 'dist',
+ assetsDir: 'static',
+ lintOnSave: process.env.NODE_ENV === 'development',
+ productionSourceMap: false,
+ devServer: {
+ port: port,
+ open: true,
+ overlay: {
+ warnings: false,
+ errors: true
+ },
+ before: require('./mock/mock-server.js')
+ },
+ configureWebpack: {
+ // provide the app's title in webpack's name field, so that
+ // it can be accessed in index.html to inject the correct title.
+ name: name,
+ resolve: {
+ alias: {
+ '@': resolve('src')
+ }
+ }
+ },
+ chainWebpack(config) {
+ // it can improve the speed of the first screen, it is recommended to turn on preload
+ // it can improve the speed of the first screen, it is recommended to turn on preload
+ config.plugin('preload').tap(() => [
+ {
+ rel: 'preload',
+ // to ignore runtime.js
+ // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
+ fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
+ include: 'initial'
+ }
+ ])
+
+ // when there are many pages, it will cause too many meaningless requests
+ config.plugins.delete('prefetch')
+
+ // set svg-sprite-loader
+ config.module
+ .rule('svg')
+ .exclude.add(resolve('src/icons'))
+ .end()
+ config.module
+ .rule('icons')
+ .test(/\.svg$/)
+ .include.add(resolve('src/icons'))
+ .end()
+ .use('svg-sprite-loader')
+ .loader('svg-sprite-loader')
+ .options({
+ symbolId: 'icon-[name]'
+ })
+ .end()
+
+ config
+ .when(process.env.NODE_ENV !== 'development',
+ config => {
+ config
+ .plugin('ScriptExtHtmlWebpackPlugin')
+ .after('html')
+ .use('script-ext-html-webpack-plugin', [{
+ // `runtime` must same as runtimeChunk name. default is `runtime`
+ inline: /runtime\..*\.js$/
+ }])
+ .end()
+ config
+ .optimization.splitChunks({
+ chunks: 'all',
+ cacheGroups: {
+ libs: {
+ name: 'chunk-libs',
+ test: /[\\/]node_modules[\\/]/,
+ priority: 10,
+ chunks: 'initial' // only package third parties that are initially dependent
+ },
+ elementUI: {
+ name: 'chunk-elementUI', // split elementUI into a single package
+ priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
+ test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
+ },
+ commons: {
+ name: 'chunk-commons',
+ test: resolve('src/components'), // can customize your rules
+ minChunks: 3, // minimum common number
+ priority: 5,
+ reuseExistingChunk: true
+ }
+ }
+ })
+ // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
+ config.optimization.runtimeChunk('single')
+ }
+ )
+ }
+}