diff --git a/.env.development b/.env.development index de583d09..d52f15d9 100644 --- a/.env.development +++ b/.env.development @@ -2,4 +2,4 @@ ENV = 'development' # base api -VUE_APP_BASE_API = '/dev-api' +VUE_APP_BASE_API = 'http://localhost:18080/api' diff --git a/.env.production b/.env.production index 80c81030..9027c8a5 100644 --- a/.env.production +++ b/.env.production @@ -2,5 +2,4 @@ ENV = 'production' # base api -VUE_APP_BASE_API = '/prod-api' - +VUE_APP_BASE_API = 'http://localhost:18080/api' diff --git a/.env.staging b/.env.staging index a8793a09..c4f2e632 100644 --- a/.env.staging +++ b/.env.staging @@ -4,5 +4,4 @@ NODE_ENV = production ENV = 'staging' # base api -VUE_APP_BASE_API = '/stage-api' - +VUE_APP_BASE_API = 'http://localhost:18080/api' diff --git a/src/api/user.js b/src/api/user.js index b8b8741c..8d2d0951 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -2,23 +2,15 @@ import request from '@/utils/request' export function login(data) { return request({ - url: '/vue-element-admin/user/login', + url: '/admin/auth/login', method: 'post', data }) } -export function getInfo(token) { +export function getMenu() { return request({ - url: '/vue-element-admin/user/info', - method: 'get', - params: { token } - }) -} - -export function logout() { - return request({ - url: '/vue-element-admin/user/logout', - method: 'post' + url: '/admin/auth/menu', + method: 'get' }) } diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue index 37bc1e69..0af0fce0 100644 --- a/src/layout/components/Navbar.vue +++ b/src/layout/components/Navbar.vue @@ -21,23 +21,15 @@ + {{ name }} - - Profile + + 后台首页 - - Dashboard - - - Github - - - Docs - - Log Out + 退出登录 @@ -67,7 +59,8 @@ export default { ...mapGetters([ 'sidebar', 'avatar', - 'device' + 'device', + 'name' ]) }, methods: { @@ -153,10 +146,16 @@ export default { border-radius: 10px; } + .user-name { + margin-left: 8px; + font-size: 14px; + color: #303133; + } + .el-icon-caret-bottom { cursor: pointer; - position: absolute; - right: -20px; + position: static; + margin-left: 6px; top: 25px; font-size: 12px; } diff --git a/src/layout/components/ParentView.vue b/src/layout/components/ParentView.vue new file mode 100644 index 00000000..5b35704f --- /dev/null +++ b/src/layout/components/ParentView.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/src/main.js b/src/main.js index b5fa135d..6f0d2cea 100644 --- a/src/main.js +++ b/src/main.js @@ -20,19 +20,6 @@ import './utils/error-log' // error log import * as filters from './filters' // global filters -/** - * If you don't want to use mock-server - * you want to use MockJs for mock api - * you can execute: mockXHR() - * - * Currently MockJs will be used in the production environment, - * please remove it before going online ! ! ! - */ -if (process.env.NODE_ENV === 'production') { - const { mockXHR } = require('../mock') - mockXHR() -} - Vue.use(Element, { size: Cookies.get('size') || 'medium', // set element-ui default size locale: enLang // 如果使用中文,无需设置,请删除 diff --git a/src/permission.js b/src/permission.js index ff5eaad2..02cc25cf 100644 --- a/src/permission.js +++ b/src/permission.js @@ -32,12 +32,10 @@ router.beforeEach(async(to, from, next) => { next() } else { try { - // get user info - // note: roles must be a object array! such as: ['admin'] or ,['developer','editor'] - const { roles } = await store.dispatch('user/getInfo') + const userInfo = await store.dispatch('user/getInfo') - // generate accessible routes map based on roles - const accessRoutes = await store.dispatch('permission/generateRoutes', roles) + // generate accessible routes map based on backend menus + const accessRoutes = await store.dispatch('permission/generateRoutes', userInfo.menus) // dynamically add accessible routes router.addRoutes(accessRoutes) diff --git a/src/router/index.js b/src/router/index.js index 2be959d2..5e976557 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,44 +1,12 @@ import Vue from 'vue' import Router from 'vue-router' +import Layout from '@/layout' +import { rootRedirectRoute } from './root-route' Vue.use(Router) -/* Layout */ -import Layout from '@/layout' - -/* Router Modules */ -import componentsRouter from './modules/components' -import chartsRouter from './modules/charts' -import tableRouter from './modules/table' -import nestedRouter from './modules/nested' - -/** - * Note: sub-menu only appear when route children.length >= 1 - * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html - * - * hidden: true if set true, item will not show in the sidebar(default is false) - * alwaysShow: true if set true, will always show the root menu - * if not set alwaysShow, when item has more than one children route, - * it will becomes nested mode, otherwise not show the root menu - * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb - * name:'router-name' the name is used by (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 = [ + rootRedirectRoute, { path: '/redirect', component: Layout, @@ -69,336 +37,19 @@ export const constantRoutes = [ 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 + router.matcher = newRouter.matcher } export default router diff --git a/src/router/root-route.js b/src/router/root-route.js new file mode 100644 index 00000000..77576310 --- /dev/null +++ b/src/router/root-route.js @@ -0,0 +1,5 @@ +export const rootRedirectRoute = { + path: '/', + redirect: '/dashboard', + hidden: true +} diff --git a/src/settings.js b/src/settings.js index 1ebc7f29..4cc47849 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1,5 +1,5 @@ module.exports = { - title: 'Vue Element Admin', + title: '竞赛管理后台', /** * @type {boolean} true | false diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js index aeb5ee53..7b564bb1 100644 --- a/src/store/modules/permission.js +++ b/src/store/modules/permission.js @@ -1,37 +1,44 @@ -import { asyncRoutes, constantRoutes } from '@/router' +import Layout from '@/layout' +import ParentView from '@/layout/components/ParentView' +import { constantRoutes } from '@/router' +import { buildBackendRouteTree } from '@/utils/backend-menu' -/** - * 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 - } +const viewLoaders = { + 'views/dashboard/index': () => import('@/views/dashboard/index') } -/** - * Filter asynchronous routing tables by recursion - * @param routes asyncRoutes - * @param roles - */ -export function filterAsyncRoutes(routes, roles) { - const res = [] +function resolveView(componentKey) { + if (componentKey === 'Layout') { + return Layout + } + if (componentKey === 'ParentView') { + return ParentView + } + return viewLoaders[componentKey] || (() => import('@/views/backend-page/index')) +} - routes.forEach(route => { - const tmp = { ...route } - if (hasPermission(roles, tmp)) { - if (tmp.children) { - tmp.children = filterAsyncRoutes(tmp.children, roles) - } - res.push(tmp) +function hydrateRoutes(routes) { + return routes.map(route => { + const record = { + ...route, + component: resolveView(route.componentKey) } - }) - return res + if (record.meta) { + record.meta = { + ...record.meta, + componentKey: route.componentKey + } + } + + delete record.componentKey + + if (record.children && record.children.length) { + record.children = hydrateRoutes(record.children) + } + + return record + }) } const state = { @@ -47,14 +54,11 @@ const mutations = { } const actions = { - generateRoutes({ commit }, roles) { + generateRoutes({ commit }, menus) { return new Promise(resolve => { - let accessedRoutes - if (roles.includes('admin')) { - accessedRoutes = asyncRoutes || [] - } else { - accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) - } + const accessedRoutes = hydrateRoutes(buildBackendRouteTree(menus)).concat([ + { path: '*', redirect: '/404', hidden: true } + ]) commit('SET_ROUTES', accessedRoutes) resolve(accessedRoutes) }) diff --git a/src/store/modules/user.js b/src/store/modules/user.js index 78009412..21f41fae 100644 --- a/src/store/modules/user.js +++ b/src/store/modules/user.js @@ -1,6 +1,7 @@ -import { login, logout, getInfo } from '@/api/user' +import { login, getMenu } from '@/api/user' import { getToken, setToken, removeToken } from '@/utils/auth' -import router, { resetRouter } from '@/router' +import { resetRouter } from '@/router' +import { buildUserProfileFromToken } from '@/utils/admin-auth' const state = { token: getToken(), @@ -47,25 +48,22 @@ const actions = { // get user info getInfo({ commit, state }) { return new Promise((resolve, reject) => { - getInfo(state.token).then(response => { - const { data } = response + getMenu().then(response => { + const profile = buildUserProfileFromToken(state.token) + const menus = Array.isArray(response.data) ? response.data : [] - if (!data) { + if (!profile.roles || profile.roles.length <= 0) { 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) + commit('SET_ROLES', profile.roles) + commit('SET_NAME', profile.name) + commit('SET_AVATAR', profile.avatar) + commit('SET_INTRODUCTION', profile.introduction) + resolve({ + ...profile, + menus + }) }).catch(error => { reject(error) }) @@ -73,22 +71,17 @@ const actions = { }, // 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) - }) + logout({ commit, dispatch }) { + return new Promise(resolve => { + commit('SET_TOKEN', '') + commit('SET_ROLES', []) + commit('SET_NAME', '') + commit('SET_AVATAR', '') + commit('SET_INTRODUCTION', '') + removeToken() + resetRouter() + dispatch('tagsView/delAllViews', null, { root: true }) + resolve() }) }, @@ -97,29 +90,12 @@ const actions = { return new Promise(resolve => { commit('SET_TOKEN', '') commit('SET_ROLES', []) + commit('SET_NAME', '') + commit('SET_AVATAR', '') + commit('SET_INTRODUCTION', '') 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 }) } } diff --git a/src/utils/admin-auth.js b/src/utils/admin-auth.js new file mode 100644 index 00000000..fd75b963 --- /dev/null +++ b/src/utils/admin-auth.js @@ -0,0 +1,37 @@ +const DEFAULT_AVATAR = 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif' + +export function parseJwtPayload(token) { + if (!token || typeof token !== 'string') { + return null + } + + const parts = token.split('.') + if (parts.length < 2) { + return null + } + + try { + const payload = parts[1] + .replace(/-/g, '+') + .replace(/_/g, '/') + .padEnd(Math.ceil(parts[1].length / 4) * 4, '=') + + return JSON.parse(window.atob(payload)) + } catch (error) { + return null + } +} + +export function buildUserProfileFromToken(token) { + const payload = parseJwtPayload(token) || {} + const roleCode = payload.roleCode || '' + + return { + name: payload.username || '', + avatar: DEFAULT_AVATAR, + introduction: payload.roleName || '', + roles: roleCode ? [roleCode] : [], + roleCode, + roleName: payload.roleName || '' + } +} diff --git a/src/utils/auth.js b/src/utils/auth.js index 08a43d6e..15b2e624 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -1,6 +1,6 @@ import Cookies from 'js-cookie' -const TokenKey = 'Admin-Token' +const TokenKey = 'admin_token' export function getToken() { return Cookies.get(TokenKey) diff --git a/src/utils/backend-menu.js b/src/utils/backend-menu.js new file mode 100644 index 00000000..8d030b2f --- /dev/null +++ b/src/utils/backend-menu.js @@ -0,0 +1,83 @@ +function trimSlashes(value) { + return String(value || '').replace(/^\/+|\/+$/g, '') +} + +function ensureAbsolutePath(value) { + const normalized = trimSlashes(value) + return normalized ? `/${normalized}` : '/' +} + +function relativePath(fullPath, parentPath) { + const full = ensureAbsolutePath(fullPath) + const parent = ensureAbsolutePath(parentPath) + + if (full === parent) { + return '' + } + + if (full.startsWith(`${parent}/`)) { + return trimSlashes(full.slice(parent.length)) + } + + return trimSlashes(full.split('/').pop()) +} + +function buildRouteName(menu, suffix = '') { + const base = trimSlashes(menu.routePath).replace(/[^\w]+/g, '_') || `menu_${menu.id}` + return `${base}${suffix}_${menu.id}` +} + +function buildMeta(menu) { + return { + title: menu.menuName || 'Untitled', + icon: menu.icon || 'menu' + } +} + +function buildLeafRoute(menu, parentPath, useIndexPath = false) { + return { + path: useIndexPath ? '' : relativePath(menu.routePath, parentPath), + componentKey: menu.component || 'backend-placeholder', + name: buildRouteName(menu, '_view'), + meta: buildMeta(menu) + } +} + +function buildChildRoute(menu, parentPath) { + const children = Array.isArray(menu.children) ? menu.children : [] + + if (!children.length) { + return buildLeafRoute(menu, parentPath, false) + } + + return { + path: relativePath(menu.routePath, parentPath), + componentKey: 'ParentView', + name: buildRouteName(menu, '_group'), + redirect: ensureAbsolutePath(children[0].routePath), + meta: buildMeta(menu), + alwaysShow: true, + children: children.map(child => buildChildRoute(child, menu.routePath)) + } +} + +function buildRootRoute(menu) { + const rootPath = ensureAbsolutePath(menu.routePath) + const children = Array.isArray(menu.children) ? menu.children : [] + + return { + path: rootPath, + componentKey: 'Layout', + name: buildRouteName(menu, '_root'), + redirect: children.length ? ensureAbsolutePath(children[0].routePath) : rootPath, + meta: buildMeta(menu), + alwaysShow: children.length > 0, + children: children.length + ? children.map(child => buildChildRoute(child, menu.routePath)) + : [buildLeafRoute(menu, menu.routePath, true)] + } +} + +export function buildBackendRouteTree(menus) { + return (Array.isArray(menus) ? menus : []).map(buildRootRoute) +} diff --git a/src/utils/request.js b/src/utils/request.js index 2fb95ac0..99537859 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -7,19 +7,15 @@ import { getToken } from '@/utils/auth' 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 + timeout: 15000 // 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() + const token = store.getters.token || getToken() + if (token) { + config.headers.Authorization = `Bearer ${token}` } return config }, @@ -45,7 +41,18 @@ service.interceptors.response.use( response => { const res = response.data - // if the custom code is not 20000, it is judged as an error. + if (typeof res.success === 'boolean') { + if (!res.success) { + Message({ + message: res.message || 'Error', + type: 'error', + duration: 5 * 1000 + }) + return Promise.reject(new Error(res.message || 'Error')) + } + return res + } + if (res.code !== 20000) { Message({ message: res.message || 'Error', @@ -72,9 +79,22 @@ service.interceptors.response.use( } }, error => { - console.log('err' + error) // for debug + const status = error.response && error.response.status + + if (status === 401) { + MessageBox.confirm('登录状态已失效,可以取消停留在当前页,或重新登录', '登录失效', { + confirmButtonText: '重新登录', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + store.dispatch('user/resetToken').then(() => { + location.reload() + }) + }).catch(() => {}) + } + Message({ - message: error.message, + message: (error.response && error.response.data && error.response.data.message) || error.message, type: 'error', duration: 5 * 1000 }) diff --git a/src/views/backend-page/index.vue b/src/views/backend-page/index.vue new file mode 100644 index 00000000..e8543d3d --- /dev/null +++ b/src/views/backend-page/index.vue @@ -0,0 +1,27 @@ + + + + + {{ title }} + + 当前菜单已接入现有后端动态菜单。 + 路由:{{ $route.path }} + 组件映射:{{ componentKey }} + 该页面可继续替换为对应业务页面实现。 + + + + + diff --git a/src/views/dashboard/admin/index.vue b/src/views/dashboard/admin/index.vue index 8cb557be..d2f60758 100644 --- a/src/views/dashboard/admin/index.vue +++ b/src/views/dashboard/admin/index.vue @@ -1,124 +1,160 @@ - - - - - - - - - - - - - - + + + + + ADMIN HOME + 竞赛管理后台已接入现有认证与菜单系统 + + 当前登录账号为 {{ name }},角色为 {{ roleLabel }}。 + 登录后使用后端返回的 JWT 完成鉴权,并根据 `/api/admin/auth/menu` 动态生成左侧导航。 + + + 登录接口:`/api/admin/auth/login` + 菜单接口:`/api/admin/auth/menu` + + - - - - - - - - - + + + 已加载菜单 + {{ menuItems.length }} + 来自后端动态菜单,不再依赖 mock 数据。 + - - - - - - - - - - - + + + 当前可访问菜单 + + + + + {{ item.title }} + {{ item.path }} + + + + diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index 1720ea8f..18a3bc5e 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -5,27 +5,15 @@ diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 25906405..e3980d1d 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -1,9 +1,9 @@ - - Login Form + 竞赛管理后台 + vue-element-admin 已接入现有管理端认证与动态菜单 @@ -13,7 +13,7 @@ - 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/tests/unit/router.spec.js b/tests/unit/router.spec.js new file mode 100644 index 00000000..9c591aae --- /dev/null +++ b/tests/unit/router.spec.js @@ -0,0 +1,9 @@ +import { rootRedirectRoute } from '@/router/root-route' + +describe('router constants', () => { + it('redirects the root path to dashboard', () => { + expect(rootRedirectRoute.path).toBe('/') + expect(rootRedirectRoute.redirect).toBe('/dashboard') + expect(rootRedirectRoute.hidden).toBe(true) + }) +}) diff --git a/tests/unit/utils/admin-auth.spec.js b/tests/unit/utils/admin-auth.spec.js new file mode 100644 index 00000000..9ab2a3a4 --- /dev/null +++ b/tests/unit/utils/admin-auth.spec.js @@ -0,0 +1,38 @@ +import { buildUserProfileFromToken, parseJwtPayload } from '@/utils/admin-auth' + +function createJwt(payload) { + const encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64url') + return `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode(payload)}.signature` +} + +describe('admin auth utils', () => { + it('parses jwt payload from backend admin token', () => { + const token = createJwt({ + username: 'admin_user', + roleCode: 'SUPER_ADMIN', + roleName: 'Super Admin' + }) + + expect(parseJwtPayload(token)).toEqual({ + username: 'admin_user', + roleCode: 'SUPER_ADMIN', + roleName: 'Super Admin' + }) + }) + + it('builds user profile from backend admin token', () => { + const token = createJwt({ + username: 'admin_user', + roleCode: 'SUPER_ADMIN', + roleName: 'Super Admin' + }) + + expect(buildUserProfileFromToken(token)).toMatchObject({ + name: 'admin_user', + introduction: 'Super Admin', + roleCode: 'SUPER_ADMIN', + roleName: 'Super Admin', + roles: ['SUPER_ADMIN'] + }) + }) +}) diff --git a/tests/unit/utils/backend-menu.spec.js b/tests/unit/utils/backend-menu.spec.js new file mode 100644 index 00000000..1b66d224 --- /dev/null +++ b/tests/unit/utils/backend-menu.spec.js @@ -0,0 +1,63 @@ +import { buildBackendRouteTree } from '@/utils/backend-menu' + +describe('backend menu utils', () => { + it('maps a root backend menu into a layout route with index child', () => { + const routes = buildBackendRouteTree([ + { + id: 1, + menuName: 'Dashboard', + routePath: '/dashboard', + component: 'views/dashboard/index', + icon: 'dashboard', + children: [] + } + ]) + + expect(routes).toHaveLength(1) + expect(routes[0]).toMatchObject({ + path: '/dashboard', + componentKey: 'Layout', + redirect: '/dashboard' + }) + expect(routes[0].children[0]).toMatchObject({ + path: '', + componentKey: 'views/dashboard/index', + meta: { + title: 'Dashboard', + icon: 'dashboard' + } + }) + }) + + it('maps nested backend menus into relative child routes', () => { + const routes = buildBackendRouteTree([ + { + id: 2, + menuName: 'Contest', + routePath: '/contest', + component: 'layout/router-view', + icon: 'trophy', + children: [ + { + id: 3, + menuName: 'List', + routePath: '/contest/list', + component: 'views/contest/list', + icon: 'list', + children: [] + } + ] + } + ]) + + expect(routes[0]).toMatchObject({ + path: '/contest', + redirect: '/contest/list', + alwaysShow: true + }) + expect(routes[0].children[0]).toMatchObject({ + path: 'list', + componentKey: 'views/contest/list' + }) + }) +}) diff --git a/vue.config.js b/vue.config.js index 33a63483..75ac826c 100644 --- a/vue.config.js +++ b/vue.config.js @@ -13,7 +13,7 @@ const name = defaultSettings.title || 'vue Element Admin' // page title // 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 +const port = process.env.port || process.env.npm_config_port || 9528 // dev port // All configuration item explanations can be find in https://cli.vuejs.org/config/ module.exports = { @@ -35,8 +35,7 @@ module.exports = { overlay: { warnings: false, errors: true - }, - before: require('./mock/mock-server.js') + } }, configureWebpack: { // provide the app's title in webpack's name field, so that
当前菜单已接入现有后端动态菜单。
路由:{{ $route.path }}
组件映射:{{ componentKey }}
该页面可继续替换为对应业务页面实现。
+ 当前登录账号为 {{ name }},角色为 {{ roleLabel }}。 + 登录后使用后端返回的 JWT 完成鉴权,并根据 `/api/admin/auth/menu` 动态生成左侧导航。 +
vue-element-admin 已接入现有管理端认证与动态菜单