fix: 完整支持新路由结构

This commit is contained in:
chansee97 2025-09-07 00:47:56 +08:00
parent ab72ff1bdd
commit 423e79f64d
25 changed files with 359 additions and 427 deletions

View File

@ -45,6 +45,10 @@ export function deleteMenu(id: number) {
* *
* GET /menu/selectTree * GET /menu/selectTree
*/ */
export function getMenuOptions() { export function getMenuOptions(excludePermissions?: boolean) {
return request.Get<Api.Response<Entity.TreeNode[]>>('/menu/options') return request.Get<Api.Response<Entity.TreeNode[]>>('/menu/options', {
params: {
excludePermissions,
},
})
} }

View File

@ -39,13 +39,13 @@ const options = computed(() => {
return routeStore.rowRoutes.filter((item) => { return routeStore.rowRoutes.filter((item) => {
const conditions = [ const conditions = [
t(`route.${String(item.name)}`, item.title || item.name)?.includes(searchValue.value), t(`${String(item.i18nKey)}`, item.title)?.includes(searchValue.value),
item.path?.includes(searchValue.value), item.path?.includes(searchValue.value),
] ]
return conditions.some(condition => !item.hide && condition) return conditions.some(condition => !item.menuVisible && condition)
}).map((item) => { }).map((item) => {
return { return {
label: t(`route.${String(item.name)}`, item.title || item.name), label: t(`${String(item.i18nKey)}`, item.title),
value: item.path, value: item.path,
icon: item.icon, icon: item.icon,
} }

View File

@ -24,7 +24,7 @@ const emit = defineEmits<{
> >
<div class="flex-center gap-2 text-nowrap"> <div class="flex-center gap-2 text-nowrap">
<nova-icon :icon="route.meta.icon" /> <nova-icon :icon="route.meta.icon" />
<span>{{ $t(`route.${String(route.name)}`, route.meta.title) }}</span> <span>{{ $t(`${String(route.meta.i18nKey)}`, route.meta.title) }}</span>
<button <button
v-if="closable" v-if="closable"
type="button" type="button"

View File

@ -11,8 +11,8 @@ export function setupRouterGuard(router: Router) {
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
// 判断是否是外链,如果是直接打开网页并拦截跳转 // 判断是否是外链,如果是直接打开网页并拦截跳转
if (to.meta.href) { if (to.meta.isLink) {
window.open(to.meta.href) window.open(to.meta.linkPath)
next(false) // 取消当前导航 next(false) // 取消当前导航
return return
} }
@ -23,7 +23,7 @@ export function setupRouterGuard(router: Router) {
const isLogin = Boolean(local.get('accessToken')) const isLogin = Boolean(local.get('accessToken'))
// 处理根路由重定向 // 处理根路由重定向
if (to.name === 'root') { if (to.path === '/') {
if (isLogin) { if (isLogin) {
// 已登录,重定向到首页 // 已登录,重定向到首页
next({ path: import.meta.env.VITE_HOME_PATH, replace: true }) next({ path: import.meta.env.VITE_HOME_PATH, replace: true })
@ -35,22 +35,19 @@ export function setupRouterGuard(router: Router) {
return return
} }
// 如果是login路由直接放行 // 如果用户未登录,重定向到登录页
if (to.name === 'login') { if (!isLogin) {
// login页面不需要任何认证检查直接放行
}
// 如果路由明确设置了requiresAuth为false直接放行
else if (to.meta.requiresAuth === false) {
// 明确设置为false的路由直接放行
// 继续执行后面的逻辑
}
// 如果路由设置了requiresAuth为true且用户未登录重定向到登录页
else if (to.meta.requiresAuth === true && !isLogin) {
const redirect = to.name === 'not-found' ? undefined : to.fullPath const redirect = to.name === 'not-found' ? undefined : to.fullPath
next({ path: '/login', query: { redirect } }) next({ path: '/login', query: { redirect } })
return return
} }
// 如果用户已登录且访问login页面重定向到首页
if (to.name === 'login' && isLogin) {
next({ path: '/' })
return
}
// 判断路由有无进行初始化 // 判断路由有无进行初始化
if (!routeStore.isInitAuthRoute && to.name !== 'login') { if (!routeStore.isInitAuthRoute && to.name !== 'login') {
try { try {
@ -76,17 +73,11 @@ export function setupRouterGuard(router: Router) {
} }
} }
// 如果用户已登录且访问login页面重定向到首页
if (to.name === 'login' && isLogin) {
next({ path: '/' })
return
}
next() next()
}) })
router.beforeResolve((to) => { router.beforeResolve((to) => {
// 设置菜单高亮 // 设置菜单高亮
routeStore.setActiveMenu(to.meta.activeMenu ?? to.fullPath) routeStore.setActiveMenu(to.meta.activePath ?? to.fullPath)
// 添加tabs // 添加tabs
tabStore.addTab(to) tabStore.addTab(to)
// 设置高亮标签; // 设置高亮标签;

View File

@ -1,4 +1,5 @@
import type { App } from 'vue' import type { App } from 'vue'
import type { RouteRecord } from 'vue-router'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import { setupRouterGuard } from './guard' import { setupRouterGuard } from './guard'
import { routes } from './routes.inner' import { routes } from './routes.inner'
@ -6,7 +7,7 @@ import { routes } from './routes.inner'
const { VITE_ROUTE_MODE = 'hash', VITE_BASE_URL } = import.meta.env const { VITE_ROUTE_MODE = 'hash', VITE_BASE_URL } = import.meta.env
export const router = createRouter({ export const router = createRouter({
history: VITE_ROUTE_MODE === 'hash' ? createWebHashHistory(VITE_BASE_URL) : createWebHistory(VITE_BASE_URL), history: VITE_ROUTE_MODE === 'hash' ? createWebHashHistory(VITE_BASE_URL) : createWebHistory(VITE_BASE_URL),
routes, routes: routes as unknown as RouteRecord[],
}) })
// 安装vue路由 // 安装vue路由
export async function installRouter(app: App) { export async function installRouter(app: App) {

View File

@ -1,17 +1,42 @@
import type { RouteRecordRaw } from 'vue-router' import Layout from '@/layouts/index.vue'
/* 页面中的一些固定路由,错误页等 */ /* 页面中的一些固定路由,错误页等 */
export const routes: RouteRecordRaw[] = [ export const routes = [
{ {
path: '/', path: '/appRoot',
name: 'root', name: 'appRoot',
redirect: import.meta.env.VITE_HOME_PATH,
component: Layout,
meta: {
title: '',
icon: 'icon-park-outline:home',
},
children: [ children: [
{
path: '/user-center',
name: 'userCenter',
component: () => import('@/views/build-in/user-center/index.vue'),
meta: {
title: '个人中心',
icon: 'carbon:user-avatar-filled-alt',
},
},
{
path: '/home',
name: 'home',
component: () => import('@/views/build-in/home/index.vue'),
meta: {
title: '首页',
icon: 'icon-park-outline:analysis',
pinTab: true,
},
},
], ],
}, },
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: () => import('@/views/build-in/login/index.vue'), // 注意这里要带上 文件后缀.vue component: () => import('@/views/build-in/login/index.vue'),
meta: { meta: {
title: '登录', title: '登录',
withoutTab: true, withoutTab: true,

View File

@ -1,14 +1,4 @@
export const staticRoutes: Entity.Menu[] = [ export const staticRoutes: Entity.Menu[] = [
{
path: '/home',
title: '概览',
icon: 'icon-park-outline:analysis',
pinTab: true,
menuType: 'page',
component: '/build-in/home/index.vue',
id: 1,
parentId: 0,
},
{ {
path: '/multi', path: '/multi',
title: '多级菜单演示', title: '多级菜单演示',
@ -30,7 +20,7 @@ export const staticRoutes: Entity.Menu[] = [
path: '/multi/multi-2/detail', path: '/multi/multi-2/detail',
title: '菜单详情页', title: '菜单详情页',
icon: 'icon-park-outline:list', icon: 'icon-park-outline:list',
menuVisible: true, menuVisible: false,
activePath: '/multi/multi-2', activePath: '/multi/multi-2',
menuType: 'page', menuType: 'page',
component: '/demo/multi/multi-2/detail/index.vue', component: '/demo/multi/multi-2/detail/index.vue',
@ -50,6 +40,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '多级菜单3-1', title: '多级菜单3-1',
icon: 'icon-park-outline:list', icon: 'icon-park-outline:list',
component: '/demo/multi/multi-3/multi-4/index.vue', component: '/demo/multi/multi-3/multi-4/index.vue',
menuType: 'page',
id: 20201, id: 20201,
parentId: 202, parentId: 202,
}, },
@ -66,6 +57,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '卡片列表', title: '卡片列表',
icon: 'icon-park-outline:view-grid-list', icon: 'icon-park-outline:view-grid-list',
component: '/demo/list/card-list/index.vue', component: '/demo/list/card-list/index.vue',
menuType: 'page',
id: 302, id: 302,
parentId: 3, parentId: 3,
}, },
@ -74,6 +66,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '拖拽列表', title: '拖拽列表',
icon: 'icon-park-outline:menu-fold', icon: 'icon-park-outline:menu-fold',
component: '/demo/list/draggable-list/index.vue', component: '/demo/list/draggable-list/index.vue',
menuType: 'page',
id: 303, id: 303,
parentId: 3, parentId: 3,
}, },
@ -90,6 +83,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '请求示例', title: '请求示例',
icon: 'icon-park-outline:international', icon: 'icon-park-outline:international',
component: '/demo/fetch/index.vue', component: '/demo/fetch/index.vue',
menuType: 'page',
id: 401, id: 401,
parentId: 4, parentId: 4,
}, },
@ -98,6 +92,7 @@ export const staticRoutes: Entity.Menu[] = [
title: 'ECharts', title: 'ECharts',
icon: 'icon-park-outline:chart-proportion', icon: 'icon-park-outline:chart-proportion',
component: '/demo/echarts/index.vue', component: '/demo/echarts/index.vue',
menuType: 'page',
id: 402, id: 402,
parentId: 4, parentId: 4,
}, },
@ -107,6 +102,7 @@ export const staticRoutes: Entity.Menu[] = [
icon: 'carbon:map', icon: 'carbon:map',
keepAlive: true, keepAlive: true,
component: '/demo/map/index.vue', component: '/demo/map/index.vue',
menuType: 'page',
id: 403, id: 403,
parentId: 4, parentId: 4,
}, },
@ -123,6 +119,7 @@ export const staticRoutes: Entity.Menu[] = [
title: 'MarkDown', title: 'MarkDown',
icon: 'ri:markdown-line', icon: 'ri:markdown-line',
component: '/demo/editor/md/index.vue', component: '/demo/editor/md/index.vue',
menuType: 'page',
id: 40401, id: 40401,
parentId: 404, parentId: 404,
}, },
@ -131,6 +128,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '富文本', title: '富文本',
icon: 'icon-park-outline:edit-one', icon: 'icon-park-outline:edit-one',
component: '/demo/editor/rich/index.vue', component: '/demo/editor/rich/index.vue',
menuType: 'page',
id: 40402, id: 40402,
parentId: 404, parentId: 404,
}, },
@ -139,6 +137,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '剪贴板', title: '剪贴板',
icon: 'icon-park-outline:clipboard', icon: 'icon-park-outline:clipboard',
component: '/demo/clipboard/index.vue', component: '/demo/clipboard/index.vue',
menuType: 'page',
id: 405, id: 405,
parentId: 4, parentId: 4,
}, },
@ -147,6 +146,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '图标', title: '图标',
icon: 'local:cool', icon: 'local:cool',
component: '/demo/icons/index.vue', component: '/demo/icons/index.vue',
menuType: 'page',
id: 406, id: 406,
parentId: 4, parentId: 4,
}, },
@ -155,6 +155,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '二维码', title: '二维码',
icon: 'icon-park-outline:two-dimensional-code', icon: 'icon-park-outline:two-dimensional-code',
component: '/demo/qr-code/index.vue', component: '/demo/qr-code/index.vue',
menuType: 'page',
id: 407, id: 407,
parentId: 4, parentId: 4,
}, },
@ -163,6 +164,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '省市区联动', title: '省市区联动',
icon: 'icon-park-outline:add-subset', icon: 'icon-park-outline:add-subset',
component: '/demo/cascader/index.vue', component: '/demo/cascader/index.vue',
menuType: 'page',
id: 408, id: 408,
parentId: 4, parentId: 4,
}, },
@ -171,6 +173,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '字典示例', title: '字典示例',
icon: 'icon-park-outline:book-one', icon: 'icon-park-outline:book-one',
component: '/demo/dict/index.vue', component: '/demo/dict/index.vue',
menuType: 'page',
id: 409, id: 409,
parentId: 4, parentId: 4,
}, },
@ -187,6 +190,7 @@ export const staticRoutes: Entity.Menu[] = [
title: 'Vue', title: 'Vue',
icon: 'logos:vue', icon: 'logos:vue',
component: '/demo/documents/vue/index.vue', component: '/demo/documents/vue/index.vue',
menuType: 'page',
id: 501, id: 501,
parentId: 5, parentId: 5,
}, },
@ -195,25 +199,30 @@ export const staticRoutes: Entity.Menu[] = [
title: 'Vite', title: 'Vite',
icon: 'logos:vitejs', icon: 'logos:vitejs',
component: '/demo/documents/vite/index.vue', component: '/demo/documents/vite/index.vue',
menuType: 'page',
id: 502, id: 502,
parentId: 5, parentId: 5,
}, },
{ {
path: 'https://vueuse.org/guide/', path: '/VueUse',
title: 'VueUse外链', title: 'VueUse外链',
icon: 'logos:vueuse', icon: 'logos:vueuse',
isLink: true,
id: 503, id: 503,
parentId: 5, parentId: 5,
isLink: true,
linkPath: 'https://vueuse.org/guide/',
menuType: 'page',
}, },
{ {
path: 'https://nova-admin-docs.netlify.app/', path: '/Nova',
title: 'Nova docs', title: 'Nova docs(外链)',
icon: 'local:logo', icon: 'local:logo',
isLink: true,
id: 504, id: 504,
parentId: 5, parentId: 5,
isLink: true,
linkPath: 'https://nova-admin-docs.netlify.app/',
menuType: 'page',
}, },
{ {
path: '/public', path: '/public',
@ -222,6 +231,8 @@ export const staticRoutes: Entity.Menu[] = [
isLink: true, isLink: true,
id: 505, id: 505,
parentId: 5, parentId: 5,
linkPath: '/public',
menuType: 'page',
}, },
{ {
path: '/permission', path: '/permission',
@ -236,6 +247,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '权限示例', title: '权限示例',
icon: 'icon-park-outline:right-user', icon: 'icon-park-outline:right-user',
component: '/demo/permission/permission/index.vue', component: '/demo/permission/permission/index.vue',
menuType: 'page',
id: 601, id: 601,
parentId: 6, parentId: 6,
}, },
@ -244,6 +256,7 @@ export const staticRoutes: Entity.Menu[] = [
title: 'super可见', title: 'super可见',
icon: 'icon-park-outline:wrong-user', icon: 'icon-park-outline:wrong-user',
component: '/demo/permission/just-super/index.vue', component: '/demo/permission/just-super/index.vue',
menuType: 'page',
id: 602, id: 602,
parentId: 6, parentId: 6,
}, },
@ -260,6 +273,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '用户设置', title: '用户设置',
icon: 'icon-park-outline:every-user', icon: 'icon-park-outline:every-user',
component: '/system/user/index.vue', component: '/system/user/index.vue',
menuType: 'page',
id: 701, id: 701,
parentId: 7, parentId: 7,
}, },
@ -268,6 +282,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '角色设置', title: '角色设置',
icon: 'icon-park-outline:every-user', icon: 'icon-park-outline:every-user',
component: '/system/role/index.vue', component: '/system/role/index.vue',
menuType: 'page',
id: 702, id: 702,
parentId: 7, parentId: 7,
}, },
@ -276,6 +291,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '字典设置', title: '字典设置',
icon: 'icon-park-outline:book-one', icon: 'icon-park-outline:book-one',
component: '/system/dict/index.vue', component: '/system/dict/index.vue',
menuType: 'page',
id: 703, id: 703,
parentId: 7, parentId: 7,
}, },
@ -284,6 +300,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '菜单设置', title: '菜单设置',
icon: 'icon-park-outline:application-menu', icon: 'icon-park-outline:application-menu',
component: '/system/menu/index.vue', component: '/system/menu/index.vue',
menuType: 'page',
id: 704, id: 704,
parentId: 7, parentId: 7,
}, },
@ -292,6 +309,7 @@ export const staticRoutes: Entity.Menu[] = [
title: '部门管理', title: '部门管理',
icon: 'icon-park-outline:application-menu', icon: 'icon-park-outline:application-menu',
component: '/system/dept/index.vue', component: '/system/dept/index.vue',
menuType: 'page',
id: 705, id: 705,
parentId: 7, parentId: 7,
}, },
@ -300,17 +318,8 @@ export const staticRoutes: Entity.Menu[] = [
title: '关于', title: '关于',
icon: 'icon-park-outline:info', icon: 'icon-park-outline:info',
component: '/demo/about/index.vue', component: '/demo/about/index.vue',
menuType: 'page',
id: 8, id: 8,
parentId: 0, parentId: 0,
}, },
{
path: '/user-center',
title: '个人中心',
menuVisible: true,
icon: 'carbon:user-avatar-filled-alt',
component: '/build-in/user-center/index.vue',
id: 999,
parentId: 0,
},
] ]

View File

@ -5,7 +5,7 @@ import { useRouteStore } from './router'
import { useTabStore } from './tab' import { useTabStore } from './tab'
interface AuthStatus { interface AuthStatus {
userInfo: Entity.User | Record<string, any> userInfo: Entity.User
roles: string[] roles: string[]
permissions: string[] permissions: string[]
} }
@ -65,7 +65,13 @@ export const useAuthStore = defineStore('auth-store', {
const { data } = await fetchLogin(loginData) const { data } = await fetchLogin(loginData)
// 处理登录信息 // 处理登录信息
await this.handleLoginInfo(data) try {
await this.handleLoginInfo(data)
}
catch (error) {
console.error('Failed to handle login info:', error)
throw error
}
// 更新用户信息 // 更新用户信息
await this.updataUserInfo() await this.updataUserInfo()

View File

@ -1,88 +1,66 @@
/// <reference path="../../typings/global.d.ts" />
import type { MenuOption } from 'naive-ui' import type { MenuOption } from 'naive-ui'
import type { RouteRecordRaw } from 'vue-router'
import { usePermission } from '@/hooks'
import Layout from '@/layouts/index.vue'
import { $t, renderIcon } from '@/utils' import { $t, renderIcon } from '@/utils'
import { clone, min, omit, pick } from 'radash' import { clone, isEmpty, min, pick } from 'radash'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import arrayToTree from 'array-to-tree' import arrayToTree from 'array-to-tree'
const metaFields: AppRoute.MetaKeys[] const metaFields: (keyof Entity.Menu)[]
= ['title', 'icon', 'requiresAuth', 'roles', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType'] = ['title', 'icon', 'keepAlive', 'activePath', 'tabVisible', 'pinTab', 'menuType', 'linkPath', 'isLink', 'i18nKey']
function standardizedRoutes(route: AppRoute.RowRoute[]) {
return clone(route).map((i) => {
const route = omit(i, metaFields)
Reflect.set(route, 'meta', pick(i, metaFields))
return route
}) as AppRoute.Route[]
}
export function createRoutes(routes: AppRoute.RowRoute[]) {
const { hasPermission } = usePermission()
export function createRoutes(routes: Entity.Menu[]) {
// Structure the meta field // Structure the meta field
let resultRouter = standardizedRoutes(routes) const routerWithMeta: App.RouteRecord[] = clone(routes).map((i) => {
const meta = pick(i, metaFields)
// Route permission filtering return {
resultRouter = resultRouter.filter(i => hasPermission(i.meta.roles)) name: `${i.title}_${i.id}`,
path: i.path,
component: i.component,
meta,
id: i.id,
parentId: i.parentId || 0,
}
})
// Generate routes, no need to import files for those with redirect // Generate routes, no need to import files for those with redirect
const modules = import.meta.glob('@/views/**/*.vue') const modules = import.meta.glob('@/views/**/*.vue')
resultRouter = resultRouter.map((item: AppRoute.Route) => { let resultRouter = routerWithMeta.map((item) => {
if (item.component && !item.redirect) if (item.component)
item.component = modules[`/src/views${item.component}`] item.component = modules[`/src/views${item.component}`]
return item return item
}) })
// Generate route tree // Generate route tree
resultRouter = arrayToTree(resultRouter, { resultRouter = arrayToTree(resultRouter, {
parentProperty: 'parentId', parentProperty: 'parentId',
}) as AppRoute.Route[] })
const appRootRoute: RouteRecordRaw = {
path: '/appRoot',
name: 'appRoot',
redirect: import.meta.env.VITE_HOME_PATH,
component: Layout,
meta: {
title: '',
icon: 'icon-park-outline:home',
},
children: [],
}
// Set the correct redirect path for the route // Set the correct redirect path for the route
setRedirect(resultRouter) setRedirect(resultRouter)
// Insert the processed route into the root route return resultRouter
appRootRoute.children = resultRouter as unknown as RouteRecordRaw[]
return appRootRoute
} }
// Generate an array of route names that need to be kept alive // Generate an array of route names that need to be kept alive
export function generateCacheRoutes(routes: AppRoute.RowRoute[]) { export function generateCacheRoutes(routes: Entity.Menu[]) {
return routes return routes
.filter(i => i.keepAlive) .filter(i => i.keepAlive)
.map(i => i.name) .map(i => i.title)
} }
function setRedirect(routes: AppRoute.Route[]) { function setRedirect(routes: App.RouteRecord[]) {
routes.forEach((route) => { routes.forEach((route) => {
if (route.children) { if (route.children) {
if (!route.redirect) { if (!route.redirect) {
// Filter out a collection of child elements that are not hidden // Filter out a collection of child elements that are not hidden
const visibleChilds = route.children.filter(child => !child.meta.hide) const visibleChilds = route.children.filter(child => !child.meta?.menuVisible)
// Redirect page to the path of the first child element by default // Redirect page to the path of the first child element by default
let target = visibleChilds[0] let target = visibleChilds[0]
// Filter out pages with the order attribute // Filter out pages with the order attribute
const orderChilds = visibleChilds.filter(child => child.meta.order) const orderChilds = visibleChilds.filter(child => !isEmpty(child.meta.sort))
if (orderChilds.length > 0) if (orderChilds.length > 0)
target = min(orderChilds, i => i.meta.order!) as AppRoute.Route target = min(orderChilds, i => i.meta.sort!)!
if (target) if (target)
route.redirect = target.path route.redirect = target.path
@ -94,31 +72,36 @@ function setRedirect(routes: AppRoute.Route[]) {
} }
/* 生成侧边菜单的数据 */ /* 生成侧边菜单的数据 */
export function createMenus(userRoutes: AppRoute.RowRoute[]) { export function createMenus(userRoutes: Entity.Menu[]): MenuOption[] {
const resultMenus = standardizedRoutes(userRoutes) const menus = transformAuthRoutesToMenus(userRoutes)
// filter menus that do not need to be displayed
const visibleMenus = resultMenus.filter(route => !route.meta.hide)
// generate side menu // generate side menu
return arrayToTree(transformAuthRoutesToMenus(visibleMenus), { return arrayToTree(menus, {
parentProperty: 'parentId', parentProperty: 'parentId',
}) })
} }
// render the returned routing table as a sidebar // render the returned routing table as a sidebar
function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) { function transformAuthRoutesToMenus(userRoutes: Entity.Menu[]) {
const { hasPermission } = usePermission() const homeRoute: Entity.Menu = {
id: 9999999999999,
parentId: 0,
path: '/home',
title: '首页',
icon: 'icon-park-outline:analysis',
menuVisible: true,
menuType: 'page',
}
userRoutes.unshift(homeRoute)
return userRoutes return userRoutes
// Filter out side menus without permission // filter menus that do not need to be displayed
.filter(i => hasPermission(i.meta.roles)) .filter(route => route.menuVisible !== false)
// Sort the menu according to the order size // Sort the menu according to the order size
.sort((a, b) => { .sort((a, b) => {
if (a.meta && a.meta.order && b.meta && b.meta.order) if (a.sort && b.sort)
return a.meta.order - b.meta.order return a.sort - b.sort
else if (a.meta && a.meta.order) else if (a.sort)
return -1 return -1
else if (b.meta && b.meta.order) else if (b.sort)
return 1 return 1
else return 0 else return 0
}) })
@ -128,7 +111,7 @@ function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
id: item.id, id: item.id,
parentId: item.parentId, parentId: item.parentId,
label: label:
(!item.meta.menuType || item.meta.menuType === 'page') (item.menuType !== 'directory')
? () => ? () =>
h( h(
RouterLink, RouterLink,
@ -137,11 +120,11 @@ function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
path: item.path, path: item.path,
}, },
}, },
{ default: () => $t(`route.${String(item.name)}`, item.meta.title) }, { default: () => $t(String(item.i18nKey), item.title) },
) )
: () => $t(`route.${String(item.name)}`, item.meta.title), : () => $t(String(item.i18nKey), item.title),
key: item.path, key: item.path,
icon: item.meta.icon ? renderIcon(item.meta.icon) : undefined, icon: item.icon ? renderIcon(item.icon) : undefined,
} }
return target return target
}) })

View File

@ -41,9 +41,6 @@ export const useRouteStore = defineStore('route-store', {
try { try {
// Get user's route // Get user's route
const { data } = await fetchUserMenus() const { data } = await fetchUserMenus()
if (!data) {
throw new Error('Failed to fetch user routes')
}
return data return data
} }
@ -59,7 +56,6 @@ export const useRouteStore = defineStore('route-store', {
}, },
async initAuthRoute() { async initAuthRoute() {
this.isInitAuthRoute = false this.isInitAuthRoute = false
try { try {
// Initialize route information // Initialize route information
const rowRoutes = await this.initRouteInfo() const rowRoutes = await this.initRouteInfo()
@ -72,7 +68,10 @@ export const useRouteStore = defineStore('route-store', {
// Generate actual route and insert // Generate actual route and insert
const routes = createRoutes(rowRoutes) const routes = createRoutes(rowRoutes)
router.addRoute(routes) // Add each route as a child of appRoot
routes.forEach((route) => {
router.addRoute('appRoot', route as any)
})
// Generate side menu // Generate side menu
this.menus = createMenus(rowRoutes) this.menus = createMenus(rowRoutes)

View File

@ -9,7 +9,7 @@ namespace Entity {
/** /**
* *
*/ */
component?: string component?: string | (() => Promise<unknown>)
/** /**
* *
*/ */
@ -33,7 +33,7 @@ namespace Entity {
/** /**
* *
*/ */
menuType?: MenuType menuType: MenuType
/** /**
* ID * ID
*/ */
@ -50,6 +50,10 @@ namespace Entity {
* *
*/ */
perms?: string perms?: string
/**
*
*/
linkPath?: string
/** /**
* *
*/ */

View File

@ -48,9 +48,13 @@ declare namespace NaiveUI {
type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning' type ThemeColor = 'default' | 'error' | 'primary' | 'info' | 'success' | 'warning'
} }
declare namespace Storage { declare namespace App {
type lang = 'zhCN' | 'enUS'
interface Session { interface Session {
dict: DictMap dict: {
[key: string]: Entity.Dict[]
}
} }
interface Local { interface Local {
@ -65,12 +69,13 @@ declare namespace Storage {
/* 存储当前语言 */ /* 存储当前语言 */
lang: App.lang lang: App.lang
} }
}
declare namespace App { interface RouteRecord {
type lang = 'zhCN' | 'enUS' name: string
} path: string
redirect?: string
interface DictMap { component?: string | (() => Promise<unknown>)
[key: string]: Entity.Dict[] meta: Entity.Menu
children?: RouteRecord[]
}
} }

View File

@ -1,76 +0,0 @@
declare namespace AppRoute {
type MenuType = 'directory' | 'page' | 'permission'
/** 单个路由所携带的meta标识 */
interface RouteMeta {
/* 页面标题,通常必选。 */
title: string
/* 图标,一般配合菜单使用 */
icon?: string
/* 是否需要登录权限。 */
requiresAuth?: boolean
/* 可以访问的角色 */
roles?: Entity.RoleType[]
/* 是否开启页面缓存 */
keepAlive?: boolean
/* 菜单显示状态 - 适配新菜单实体 */
menuVisible?: boolean
/* 菜单排序 - 适配新菜单实体 */
sort?: number
/* 是否为外链 - 适配新菜单实体 */
isLink?: boolean
/* 高亮菜单路径 - 适配新菜单实体 */
activePath?: string
/* 标签栏显示状态 - 适配新菜单实体 */
tabVisible?: boolean
/** 当前路由是否会被固定在Tab中,用于一些常驻页面 */
pinTab?: boolean
/** 当前路由在左侧菜单是目录还是页面,不设置默认为page */
menuType?: MenuType
/* 以下字段保持向后兼容 */
/* 有些路由我们并不想在菜单中显示,比如某些编辑页面。 */
hide?: boolean
/* 菜单排序。 */
order?: number
/* 嵌套外链 */
href?: string
/** 当前路由不在左侧菜单显示,但需要高亮某个菜单的情况 */
activeMenu?: string
/** 当前路由是否会被添加到Tab中 */
withoutTab?: boolean
}
type MetaKeys = keyof RouteMeta
interface baseRoute {
/** 路由名称(路由唯一标识) */
name: string
/** 路由路径 */
path: string
/** 路由重定向 */
redirect?: string
/* 页面组件地址 */
component?: string | null
/* 路由id */
id: number
/* 父级路由id顶级页面为null */
parentId: number | null
}
/** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */
type RowRoute = RouteMeta & baseRoute
/**
*
*/
interface Route extends baseRoute {
/** 子路由 */
children?: Route[]
/* 页面组件 */
component: any
/** 路由描述 */
meta: RouteMeta
}
}

View File

@ -1,5 +1,6 @@
import 'vue-router' import 'vue-router'
declare module 'vue-router' { declare module 'vue-router' {
interface RouteMeta extends AppRoute.RouteMeta {} interface RouteMeta extends Entity.Menu {
}
} }

View File

@ -7,7 +7,7 @@ interface StorageData<T> {
/** /**
* LocalStorage部分操作 * LocalStorage部分操作
*/ */
function createLocalStorage<T extends Storage.Local>() { function createLocalStorage<T extends App.Local>() {
// 默认缓存期限为7天 // 默认缓存期限为7天
function set<K extends keyof T>(key: K, value: T[K], expire: number = 60 * 60 * 24 * 7) { function set<K extends keyof T>(key: K, value: T[K], expire: number = 60 * 60 * 24 * 7) {
@ -52,7 +52,7 @@ function createLocalStorage<T extends Storage.Local>() {
* sessionStorage部分操作 * sessionStorage部分操作
*/ */
function createSessionStorage<T extends Storage.Session>() { function createSessionStorage<T extends App.Session>() {
function set<K extends keyof T>(key: K, value: T[K]) { function set<K extends keyof T>(key: K, value: T[K]) {
const json = JSON.stringify(value) const json = JSON.stringify(value)
window.sessionStorage.setItem(`${STORAGE_PREFIX}${String(key)}`, json) window.sessionStorage.setItem(`${STORAGE_PREFIX}${String(key)}`, json)

View File

@ -7,7 +7,7 @@ const dictStore = useDictStore()
const selectedDictType = ref('') const selectedDictType = ref('')
const selectedDictValue = ref('') const selectedDictValue = ref('')
const dictTypeOptions = ref<Array<{ label: string; value: string }>>([]) const dictTypeOptions = ref<Array<{ label: string, value: string }>>([])
const displayData = ref<Entity.DictData[] | Record<string, any>>([]) const displayData = ref<Entity.DictData[] | Record<string, any>>([])
// 使 useDict hook // 使 useDict hook
@ -18,7 +18,7 @@ const dictUtils = computed(() => {
enumMap: ref({}), enumMap: ref({}),
valueMap: ref({}), valueMap: ref({}),
labelMap: ref({}), labelMap: ref({}),
options: ref([]) options: ref([]),
} }
} }
return useDict(selectedDictType.value) return useDict(selectedDictType.value)
@ -81,7 +81,8 @@ function showOptions() {
// 使 enumMap // 使 enumMap
const dictLabel = computed(() => { const dictLabel = computed(() => {
if (!selectedDictValue.value || !dictUtils.value.enumMap.value) return '--' if (!selectedDictValue.value || !dictUtils.value.enumMap.value)
return '--'
return dictUtils.value.enumMap.value[selectedDictValue.value] || '--' return dictUtils.value.enumMap.value[selectedDictValue.value] || '--'
}) })
@ -104,19 +105,19 @@ function removeCache() {
<n-card title="字典演示"> <n-card title="字典演示">
<n-flex vertical> <n-flex vertical>
<n-flex align="center"> <n-flex align="center">
<n-select <n-select
v-model:value="selectedDictType" v-model:value="selectedDictType"
:options="dictTypeOptions" :options="dictTypeOptions"
placeholder="请选择字典类型" placeholder="请选择字典类型"
@update:value="changeSelect" @update:value="changeSelect"
/> />
<n-select <n-select
v-model:value="selectedDictValue" v-model:value="selectedDictValue"
:options="dictUtils.options.value" :options="dictUtils.options.value"
placeholder="请选择字典项" placeholder="请选择字典项"
/> />
</n-flex> </n-flex>
<n-flex> <n-flex>
<n-button @click="showRawData"> <n-button @click="showRawData">
显示原始数据 (rawData) 显示原始数据 (rawData)
@ -134,36 +135,62 @@ function removeCache() {
显示选项格式 (options) 显示选项格式 (options)
</n-button> </n-button>
</n-flex> </n-flex>
<n-flex> <n-flex>
<n-button @click="clearCache" type="warning"> <n-button type="warning" @click="clearCache">
清理所有缓存 清理所有缓存
</n-button> </n-button>
<n-button @click="removeCache" type="error" :disabled="!selectedDictType"> <n-button type="error" :disabled="!selectedDictType" @click="removeCache">
移除当前字典缓存 移除当前字典缓存
</n-button> </n-button>
</n-flex> </n-flex>
<pre class="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-96"> <pre class="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-96">
{{ JSON.stringify(displayData, null, 2) }}</pre> {{ JSON.stringify(displayData, null, 2) }}</pre>
<n-flex align="center" v-if="selectedDictValue"> <n-flex v-if="selectedDictValue" align="center">
<n-text>选中值: {{ selectedDictValue }}</n-text> <n-text>选中值: {{ selectedDictValue }}</n-text>
<n-text type="info"> <n-text type="info">
对应标签: {{ dictLabel }} 对应标签: {{ dictLabel }}
</n-text> </n-text>
</n-flex> </n-flex>
<n-divider /> <n-divider />
<n-text depth="3">useDict Hook 使用说明</n-text> <n-text depth="3">
useDict Hook 使用说明
</n-text>
<n-ul> <n-ul>
<n-li><n-text code>useDict(dictType)</n-text> - 使用字典类型获取字典工具对象</n-li> <n-li>
<n-li><n-text code>rawData</n-text> - 原始字典数据数组 Entity.DictData[]</n-li> <n-text code>
<n-li><n-text code>enumMap</n-text> - 枚举映射 { value: name }</n-li> useDict(dictType)
<n-li><n-text code>valueMap</n-text> - 值映射 { value: dictData }</n-li> </n-text> - 使用字典类型获取字典工具对象
<n-li><n-text code>labelMap</n-text> - 标签映射 { name: dictData }</n-li> </n-li>
<n-li><n-text code>options</n-text> - 选项数组 [{ label: name, value }]</n-li> <n-li>
<n-text code>
rawData
</n-text> - 原始字典数据数组 Entity.DictData[]
</n-li>
<n-li>
<n-text code>
enumMap
</n-text> - 枚举映射 { value: name }
</n-li>
<n-li>
<n-text code>
valueMap
</n-text> - 值映射 { value: dictData }
</n-li>
<n-li>
<n-text code>
labelMap
</n-text> - 标签映射 { name: dictData }
</n-li>
<n-li>
<n-text code>
options
</n-text> - 选项数组 [{ label: name, value }]
</n-li>
<n-li>字典数据会自动缓存60分钟支持手动清理缓存</n-li> <n-li>字典数据会自动缓存60分钟支持手动清理缓存</n-li>
<n-li>推荐使用 useDict hook 而不是直接调用 API store</n-li> <n-li>推荐使用 useDict hook 而不是直接调用 API store</n-li>
</n-ul> </n-ul>

View File

@ -6,7 +6,7 @@ const { modifyTab } = useTabStore()
const { fullPath, query } = useRoute() const { fullPath, query } = useRoute()
modifyTab(fullPath, (target) => { modifyTab(fullPath, (target) => {
target.meta.title = `详情页${query.id}` target.meta.title = `详情页${query.id || ''}`
}) })
</script> </script>

View File

@ -5,13 +5,13 @@ const router = useRouter()
<template> <template>
<n-card class="h-130vh"> <n-card class="h-130vh">
这个页面包含了一个不在侧边菜单的详情页面 这个页面包含了一个不在侧边菜单的详情页面
<n-button @click="router.push({ path: '/multi/multi2/detail', query: { id: 1 } })"> <n-button @click="router.push({ path: '/multi/multi-2/detail', query: { id: 1 } })">
跳转详情子页1 跳转详情子页1
</n-button> </n-button>
<n-button @click="router.push({ path: '/multi/multi2/detail', query: { id: 2 } })"> <n-button @click="router.push({ path: '/multi/multi-2/detail', query: { id: 2 } })">
跳转详情子页2 跳转详情子页2
</n-button> </n-button>
<n-button @click="router.push({ path: '/multi/multi2/detail', query: { id: 3 } })"> <n-button @click="router.push({ path: '/multi/multi-2/detail', query: { id: 3 } })">
跳转详情子页3 跳转详情子页3
</n-button> </n-button>
</n-card> </n-card>

View File

@ -5,8 +5,6 @@ import { useAuthStore } from '@/store'
const authStore = useAuthStore() const authStore = useAuthStore()
const { hasPermission } = usePermission() const { hasPermission } = usePermission()
const roleList: Entity.RoleType[] = ['super', 'admin', 'user']
function toggleUserRole(role: Entity.RoleType) { function toggleUserRole(role: Entity.RoleType) {
authStore.login(role, '123456') authStore.login(role, '123456')
} }
@ -14,7 +12,7 @@ function toggleUserRole(role: Entity.RoleType) {
<template> <template>
<n-card title="权限示例"> <n-card title="权限示例">
<n-h1> 当前权限{{ authStore.userInfo!.roles.map(r => r.roleName) }}</n-h1> <n-h1> 当前权限{{ authStore.userInfo.roles.map((r: Entity.Role) => r.roleName) }}</n-h1>
<n-button-group> <n-button-group>
<n-button v-for="item in roleList" :key="item" type="default" @click="toggleUserRole(item)"> <n-button v-for="item in roleList" :key="item" type="default" @click="toggleUserRole(item)">
{{ item }} {{ item }}
@ -22,10 +20,10 @@ function toggleUserRole(role: Entity.RoleType) {
</n-button-group> </n-button-group>
<n-h2>v-permission 指令用法</n-h2> <n-h2>v-permission 指令用法</n-h2>
<n-space> <n-space>
<n-button v-permission="['super']"> <n-button v-role="['super']">
仅super可见 仅super可见
</n-button> </n-button>
<n-button v-permission="['admin']"> <n-button v-role="['admin']">
admin可见 admin可见
</n-button> </n-button>
</n-space> </n-space>

View File

@ -36,7 +36,6 @@ export function createMenuColumns(actions: MenuColumnActions): DataTableColumns<
{ {
title: '菜单名称', title: '菜单名称',
key: 'title', key: 'title',
width: 400,
}, },
{ {
title: '图标', title: '图标',

View File

@ -1,43 +0,0 @@
<template>
<pro-input
title="国际化标识Key"
path="i18nKey"
placeholder="Eg: system.user"
/>
<pro-field
title="菜单图标"
path="icon"
>
<template #input="{ inputProps }">
<icon-select
:value="inputProps.value"
@update:value="inputProps.onUpdateValue"
/>
</template>
</pro-field>
<pro-input
required
title="路由路径"
tooltip="目录类型的路由路径,如:/system"
path="path"
class="col-span-2"
placeholder="Eg: /system"
/>
<pro-digit
title="排序"
tooltip="数字越小,同级中越靠前"
path="sort"
/>
<pro-switch
title="启用"
path="status"
:field-props="{
checkedValue: 0,
uncheckedValue: 1,
}"
/>
<pro-switch
title="菜单可见"
path="menuVisible"
/>
</template>

View File

@ -2,9 +2,6 @@
import { useBoolean } from '@/hooks' import { useBoolean } from '@/hooks'
import { createMenu, getMenuById, getMenuOptions, updateMenu } from '@/api' import { createMenu, getMenuById, getMenuOptions, updateMenu } from '@/api'
import { createProModalForm } from 'pro-naive-ui' import { createProModalForm } from 'pro-naive-ui'
import DirectoryForm from './DirectoryForm.vue'
import PageForm from './PageForm.vue'
import PermissionForm from './PermissionForm.vue'
interface Props { interface Props {
modalName?: string modalName?: string
@ -49,21 +46,8 @@ const modalTitle = computed(() => {
const treeData = ref<Entity.TreeNode[]>([]) const treeData = ref<Entity.TreeNode[]>([])
//
const formComponents = {
directory: DirectoryForm,
page: PageForm,
permission: PermissionForm,
}
// 使
const currentFormComponent = computed(() => {
const menuType = modalForm.values.value?.menuType || 'directory'
return formComponents[menuType as keyof typeof formComponents]
})
async function openModal(type: ModalType = 'add', data?: Partial<Entity.Menu>) { async function openModal(type: ModalType = 'add', data?: Partial<Entity.Menu>) {
getMenuOptions().then((res) => { getMenuOptions(true).then((res) => {
treeData.value = res.data treeData.value = res.data
}) })
@ -76,6 +60,14 @@ async function openModal(type: ModalType = 'add', data?: Partial<Entity.Menu>) {
modalForm.values.value.parentId = data.id modalForm.values.value.parentId = data.id
modalForm.values.value.path = `${data.path}/` modalForm.values.value.path = `${data.path}/`
} }
if (data?.menuType === 'directory') {
modalForm.values.value.menuType = 'page'
}
if (data?.menuType === 'page') {
modalForm.values.value.menuType = 'permission'
}
}, },
async edit() { async edit() {
if (!data?.id) if (!data?.id)
@ -123,6 +115,9 @@ async function submitModal(filedValues: Partial<Entity.Menu>) {
} }
} }
function handlePathChange(path: string) {
modalForm.values.value.component = `${path}/index.vue`
}
defineExpose({ defineExpose({
openModal, openModal,
}) })
@ -163,15 +158,119 @@ defineExpose({
/> />
<pro-input <pro-input
required required
title="标题" title="名称"
path="title" path="title"
/> />
<component :is="currentFormComponent" /> <pro-input
v-if="modalForm.values.value.menuType !== 'permission'"
title="国际化标识Key"
path="i18nKey"
placeholder="Eg: system.user"
/>
<pro-field
v-if="modalForm.values.value.menuType !== 'permission'"
title="菜单图标"
path="icon"
>
<template #input="{ inputProps }">
<icon-select
:value="inputProps.value"
@update:value="inputProps.onUpdateValue"
/>
</template>
</pro-field>
<pro-input
v-if="modalForm.values.value.menuType !== 'permission'"
required
title="路由路径"
tooltip="页面路由路径,与组件路径对应"
path="path"
class="col-span-2"
placeholder="Eg: /system/user"
@change="handlePathChange"
/>
<pro-input
v-if="modalForm.values.value.menuType === 'page'"
required
title="组件路径"
tooltip="页面组件的文件路径"
path="component"
class="col-span-2"
placeholder="Eg: /system/user/index.vue"
/>
<pro-digit
title="排序"
tooltip="数字越小,同级中越靠前, 默认为0"
path="sort"
/>
<pro-input
title="权限标识"
tooltip="需与后端装饰器一致,如@RequirePermissions('system:user:list')"
path="perms"
placeholder="Eg: system:user:list"
/>
<pro-switch
title="启用"
path="status"
:field-props="{
checkedValue: 0,
uncheckedValue: 1,
}"
/>
<pro-textarea <pro-textarea
title="备注" title="备注"
path="remark" path="remark"
class="col-span-2" class="col-span-2"
/> />
<n-collapse v-if="modalForm.values.value.menuType === 'page'" class="col-span-2">
<n-collapse-item>
<template #header>
<icon-park-outline-setting class="mr-2" />
高级设置
</template>
<div class="grid grid-cols-2">
<pro-input
title="高亮菜单路径"
tooltip="当前路由不在侧边菜单显示,但需要高亮为某个菜单"
path="activePath"
class="col-span-2"
placeholder="Eg: /system/user"
/>
<pro-switch
title="菜单可见"
path="menuVisible"
/>
<pro-switch
title="标签栏可见"
path="tabVisible"
/>
<pro-switch
title="页面缓存"
tooltip="开启配置后,切换页面数据不会清空"
path="keepAlive"
/>
<pro-switch
title="常驻标签栏"
path="pinTab"
/>
<pro-switch
title="跳转外链"
tooltip="开启配置后,点击菜单会跳转到外链地址"
path="isLink"
/>
<pro-input
v-if="modalForm.values.value.isLink"
required
title="外链地址"
tooltip="开启跳转外链配置后,点击菜单会跳转到外链地址"
path="linkPath"
class="col-span-2"
placeholder="Eg: https://www.baidu.com"
/>
</div>
</n-collapse-item>
</n-collapse>
</div> </div>
</pro-modal-form> </pro-modal-form>
</template> </template>

View File

@ -1,83 +0,0 @@
<template>
<pro-input
title="国际化标识Key"
path="i18nKey"
placeholder="Eg: system.user"
/>
<pro-field
title="菜单图标"
path="icon"
>
<template #input="{ inputProps }">
<icon-select
:value="inputProps.value"
@update:value="inputProps.onUpdateValue"
/>
</template>
</pro-field>
<pro-input
required
title="路由路径"
tooltip="页面路由路径,与组件路径对应"
path="path"
class="col-span-2"
placeholder="Eg: /system/user"
@update:value="$emit('path', $event)"
/>
<pro-input
title="高亮菜单路径"
tooltip="当前路由不在侧边菜单显示,但需要高亮为某个菜单"
path="activePath"
class="col-span-2"
placeholder="Eg: /system/user"
/>
<pro-input
required
title="组件路径"
tooltip="页面组件的文件路径"
path="component"
class="col-span-2"
placeholder="Eg: /system/user/index.vue"
/>
<pro-digit
title="排序"
tooltip="数字越小,同级中越靠前"
path="sort"
/>
<pro-input
title="权限标识"
tooltip="后端装饰器一致,如@RequirePermissions('system:user:list')"
path="perms"
placeholder="Eg: system:user:list"
/>
<pro-switch
title="启用"
path="status"
:field-props="{
checkedValue: 0,
uncheckedValue: 1,
}"
/>
<pro-switch
title="菜单可见"
path="menuVisible"
/>
<pro-switch
title="标签栏可见"
path="tabVisible"
/>
<pro-switch
title="页面缓存"
tooltip="开启配置后,切换页面数据不会清空"
path="keepAlive"
/>
<pro-switch
title="常驻标签栏"
path="pinTab"
/>
<pro-switch
title="外链菜单"
tooltip="开启配置后,点击菜单会跳转到外链地址"
path="isLink"
/>
</template>

View File

@ -1,22 +0,0 @@
<template>
<pro-input
required
title="权限标识"
tooltip="后端装饰器一致,如@RequirePermissions('system:user:add')"
path="perms"
placeholder="Eg: system:user:add"
/>
<pro-digit
title="排序"
tooltip="数字越小,同级中越靠前"
path="sort"
/>
<pro-switch
title="启用"
path="status"
:field-props="{
checkedValue: 0,
uncheckedValue: 1,
}"
/>
</template>

View File

@ -26,5 +26,10 @@
"isolatedModules": true, "isolatedModules": true,
"skipLibCheck": true "skipLibCheck": true
}, },
"include": [
"./**/*.ts",
"./**/*.tsx",
"./**/*.vue"
],
"exclude": ["node_modules", "eslint.config.js"] "exclude": ["node_modules", "eslint.config.js"]
} }