feat(projects): new router system [新的路由系统]

This commit is contained in:
Soybean 2022-11-08 01:14:59 +08:00
parent 40c1e13b50
commit c7b6a3fbec
54 changed files with 1328 additions and 759 deletions

View File

@ -65,27 +65,6 @@
![](https://s2.loli.net/2022/06/07/rRSG6mEZpujOACT.png)
## 开发计划
- [x] 引入 ECharts 替换 AntV G2Plot
- [x] 图表示例ECharts、AntV G2
- [x] 多页签:支持 query、hash 等参数,同一页面支持多个 Tab
- [x] 缓存主题配置
- [x] 精简版(新分支 thin)
- [ ] v0.9.7 表单、表格示例(ing...)
- [ ] v0.9.8 可修改的 KeepAlive 的页面缓存和全局 Tab 组件 store 重构
- [ ] v0.9.9 全局 Iframe 组件
- [ ] v1.0 示例页面完善
- [ ] v1.0 版本文档
- [ ] element-plus 版本
- [ ] i18n 国际化
- [ ] 其他 UI 版本
- [ ] soybean-admin cli 工具(选择不同 UI)
- [ ] soybean-admin 后台服务 java 版: [soybean-admin-java](https://github.com/honghuangdc/soybean-admin-java)
- [ ] soybean-admin 后台服务 go 版: [soybean-admin-go](https://github.com/honghuangdc/soybean-admin-go)
- [ ] soybean-admin 后台服务 nodejs 版: [soybean-admin-nestjs](https://github.com/honghuangdc/soybean-admin-nestjs)
- [ ] 前端可视化创建路由页面
## 安装使用
- 环境配置

View File

@ -8,7 +8,7 @@ const apis: MockMethod[] = [
response: (options: Service.MockOption): Service.MockServiceResult => {
const { userId = undefined } = options.body;
const routeHomeName: AuthRoute.RouteKey = 'dashboard_analysis';
const routeHomeName: AuthRoute.LastDegreeRouteKey = 'dashboard_analysis';
const role = userModel.find(item => item.userId === userId)?.userRole || 'user';

View File

@ -49,7 +49,8 @@
"esno": "esno",
"cleanup": "esno ./scripts/cleanup.ts",
"update-pkg": "ncu --deep -u",
"update-version": "bumpp package.json",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"release": "standard-version",
"prepare": "husky install"
},
"dependencies": {
@ -86,7 +87,7 @@
"@iconify/json": "^2.1.133",
"@iconify/vue": "^4.0.0",
"@soybeanjs/cli": "^0.1.2",
"@soybeanjs/router-page": "0.2.0",
"@soybeanjs/router-page": "1.0.3",
"@tauri-apps/cli": "^1.1.1",
"@types/bmapgl": "^0.0.5",
"@types/crypto-js": "^4.1.1",
@ -97,7 +98,7 @@
"@unocss/vite": "^0.46.3",
"@vitejs/plugin-vue": "^3.2.0",
"@vitejs/plugin-vue-jsx": "^2.1.0",
"bumpp": "^8.2.1",
"conventional-changelog": "^3.1.25",
"cross-env": "^7.0.3",
"eslint": "^8.27.0",
"eslint-config-soybeanjs-vue": "^0.1.2",
@ -108,6 +109,7 @@
"rimraf": "^3.0.2",
"rollup-plugin-visualizer": "^5.8.3",
"sass": "^1.56.0",
"standard-version": "^9.5.0",
"typescript": "4.8.4",
"unplugin-icons": "^0.14.13",
"unplugin-vue-components": "0.22.8",

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
import { useAppStore, useThemeStore } from '@/store';
type LayoutMode = 'vertical' | 'horizontal';
type LayoutHeaderProps = Record<EnumType.ThemeLayoutMode, GlobalHeaderProps>;
type LayoutHeaderProps = Record<EnumType.ThemeLayoutMode, App.GlobalHeaderProps>;
export function useBasicLayout() {
const app = useAppStore();

View File

@ -30,8 +30,10 @@ export enum EnumDataType {
undefined = '[object Undefined]',
object = '[object Object]',
array = '[object Array]',
function = '[object Function]',
date = '[object Date]',
regexp = '[object RegExp]',
promise = '[object Promise]',
set = '[object Set]',
map = '[object Map]',
file = '[object File]'

View File

@ -42,7 +42,7 @@ const routeStore = useRouteStore();
const { routerPush } = useRouterPush();
const breadcrumbs = computed(() =>
getBreadcrumbByRouteKey(route.name as string, routeStore.menus as GlobalMenuOption[], routePath('root'))
getBreadcrumbByRouteKey(route.name as string, routeStore.menus as App.GlobalMenuOption[], routePath('root'))
);
function dropdownSelect(key: string) {

View File

@ -28,11 +28,11 @@ const routeStore = useRouteStore();
const theme = useThemeStore();
const { routerPush } = useRouterPush();
const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
const menus = computed(() => routeStore.menus as App.GlobalMenuOption[]);
const activeKey = computed(() => (route.meta?.activeMenu ? route.meta.activeMenu : route.name) as string);
function handleUpdateMenu(_key: string, item: MenuOption) {
const menuItem = item as GlobalMenuOption;
const menuItem = item as App.GlobalMenuOption;
routerPush(menuItem.routePath);
}
</script>

View File

@ -38,7 +38,7 @@
defineOptions({ name: 'MessageList' });
interface Props {
list?: Message.List[];
list?: App.MessageList[];
}
withDefaults(defineProps<Props>(), {

View File

@ -62,7 +62,7 @@ const { bool: loading, setBool: setLoading } = useBoolean();
const currentTab = ref(0);
const tabData = ref<Message.Tab[]>([
const tabData = ref<App.MessageTab[]>([
{
key: 1,
name: '通知',

View File

@ -39,11 +39,11 @@ defineOptions({ name: 'GlobalHeader' });
interface Props {
/** 显示logo */
showLogo: GlobalHeaderProps['showLogo'];
showLogo: App.GlobalHeaderProps['showLogo'];
/** 显示头部菜单 */
showHeaderMenu: GlobalHeaderProps['showHeaderMenu'];
showHeaderMenu: App.GlobalHeaderProps['showHeaderMenu'];
/** 显示菜单折叠按钮 */
showMenuCollapse: GlobalHeaderProps['showMenuCollapse'];
showMenuCollapse: App.GlobalHeaderProps['showMenuCollapse'];
}
defineProps<Props>();

View File

@ -42,7 +42,7 @@ interface Props {
/** 菜单抽屉可见性 */
visible: boolean;
/** 子菜单数据 */
menus: GlobalMenuOption[];
menus: App.GlobalMenuOption[];
}
const props = defineProps<Props>();
@ -59,7 +59,7 @@ const activeKey = computed(() => (route.meta?.activeMenu ? route.meta.activeMenu
const expandedKeys = ref<string[]>([]);
function handleUpdateMenu(_key: string, item: MenuOption) {
const menuItem = item as GlobalMenuOption;
const menuItem = item as App.GlobalMenuOption;
routerPush(menuItem.routePath);
}

View File

@ -84,7 +84,7 @@ function resetFirstDegreeMenus() {
}
const activeChildMenus = computed(() => {
const menus: GlobalMenuOption[] = [];
const menus: App.GlobalMenuOption[] = [];
routeStore.menus.some(item => {
const flag = item.routeName === activeParentRouteName.value && Boolean(item.children?.length);
if (flag) {

View File

@ -31,13 +31,13 @@ const theme = useThemeStore();
const routeStore = useRouteStore();
const { routerPush } = useRouterPush();
const menus = computed(() => routeStore.menus as GlobalMenuOption[]);
const menus = computed(() => routeStore.menus as App.GlobalMenuOption[]);
const activeKey = computed(() => (route.meta?.activeMenu ? route.meta.activeMenu : route.name) as string);
const expandedKeys = ref<string[]>([]);
function handleUpdateMenu(_key: string, item: MenuOption) {
const menuItem = item as GlobalMenuOption;
const menuItem = item as App.GlobalMenuOption;
routerPush(menuItem.routePath);
}

View File

@ -18,7 +18,7 @@ export async function createDynamicRouteGuard(
if (!route.isInitAuthRoute) {
// 未登录情况下直接回到登录页,登录成功后再加载权限路由
if (!isLogin) {
const toName = to.name as AuthRoute.RouteKey;
const toName = to.name as AuthRoute.AllRouteKey;
if (route.isValidConstantRoute(toName) && !to.meta.requiresAuth) {
next();
} else {
@ -30,19 +30,19 @@ export async function createDynamicRouteGuard(
await route.initAuthRoute();
if (to.name === routeName('not-found-page')) {
// 动态路由没有加载导致被not-found-page路由捕获,等待权限路由加载好了,回到之前的路由
if (to.name === routeName('not-found')) {
// 动态路由没有加载导致被not-found路由捕获,等待权限路由加载好了,回到之前的路由
// 若路由是从根路由重定向过来的,重新回到根路由
const ROOT_ROUTE_NAME: AuthRoute.RouteKey = 'root';
const ROOT_ROUTE_NAME: AuthRoute.AllRouteKey = 'root';
const path = to.redirectedFrom?.name === ROOT_ROUTE_NAME ? '/' : to.fullPath;
next({ path, replace: true, query: to.query, hash: to.hash });
return false;
}
}
// 权限路由已经加载,仍然未找到,重定向到not-found
if (to.name === routeName('not-found-page')) {
next({ name: routeName('not-found'), replace: true });
// 权限路由已经加载,仍然未找到,重定向到404
if (to.name === routeName('not-found')) {
next({ name: routeName('404'), replace: true });
return false;
}

View File

@ -61,7 +61,7 @@ export async function createPermissionGuard(
// 登录状态进入需要登录权限的页面,无权限,重定向到无权限页面
isLogin && needLogin && !hasPermission,
() => {
next({ name: routeName('no-permission') });
next({ name: routeName('403') });
}
]
];

View File

@ -1,6 +1,7 @@
import type { App } from 'vue';
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
import { transformAuthRoutesToVueRoutes, transformRouteNameToRoutePath } from '@/utils';
import { transformRouteNameToRoutePath } from '@/utils';
import { transformAuthRouteToVueRoutes } from '@/utils/router/transform';
import { constantRoutes } from './routes';
import { scrollBehavior } from './helpers';
import { createRouterGuard } from './guard';
@ -9,7 +10,7 @@ const { VITE_HASH_ROUTE = 'N', VITE_BASE_URL } = import.meta.env;
export const router = createRouter({
history: VITE_HASH_ROUTE === 'Y' ? createWebHashHistory(VITE_BASE_URL) : createWebHistory(VITE_BASE_URL),
routes: transformAuthRoutesToVueRoutes(constantRoutes),
routes: transformAuthRouteToVueRoutes(constantRoutes),
scrollBehavior
});
@ -21,9 +22,9 @@ export async function setupRouter(app: App) {
}
/** 路由名称 */
export const routeName = (key: AuthRoute.RouteKey) => key;
export const routeName = (key: AuthRoute.AllRouteKey) => key;
/** 路由路径 */
export const routePath = (key: Exclude<AuthRoute.RouteKey, 'not-found-page'>) => transformRouteNameToRoutePath(key);
export const routePath = (key: Exclude<AuthRoute.AllRouteKey, 'not-found'>) => transformRouteNameToRoutePath(key);
export * from './routes';
export * from './modules';

View File

@ -1,4 +1,4 @@
const about: AuthRoutes.Route = {
const about: AuthRoute.Route = {
name: 'about',
path: '/about',
component: 'self',

View File

@ -39,8 +39,8 @@ export const constantRoutes: AuthRoute.Route[] = [
}
},
{
name: 'no-permission',
path: '/no-permission',
name: '403',
path: '/403',
component: 'self',
meta: {
title: '无权限',
@ -48,8 +48,8 @@ export const constantRoutes: AuthRoute.Route[] = [
}
},
{
name: 'not-found',
path: '/not-found',
name: '404',
path: '/404',
component: 'self',
meta: {
title: '未找到',
@ -57,8 +57,8 @@ export const constantRoutes: AuthRoute.Route[] = [
}
},
{
name: 'service-error',
path: '/service-error',
name: '500',
path: '/500',
component: 'self',
meta: {
title: '服务器错误',
@ -67,7 +67,7 @@ export const constantRoutes: AuthRoute.Route[] = [
},
// 匹配无效路径的路由
{
name: 'not-found-page',
name: 'not-found',
path: '/:pathMatch(.*)*',
component: 'blank',
meta: {

View File

@ -7,12 +7,11 @@ import {
getConstantRouteNames,
getUserInfo,
transformAuthRouteToMenu,
transformAuthRouteToVueRoute,
transformAuthRoutesToSearchMenus,
transformAuthRoutesToVueRoutes,
transformAuthRouteToSearchMenus,
transformRouteNameToRoutePath,
transformRoutePathToRouteName
} from '@/utils';
import { transformAuthRouteToVueRoutes, transformAuthRouteToVueRoute } from '@/utils/router/transform';
import { useAuthStore } from '../auth';
import { useTabStore } from '../tab';
@ -26,9 +25,9 @@ interface RouteState {
/** 是否初始化了权限路由 */
isInitAuthRoute: boolean;
/** 路由首页name(前端静态路由时生效,后端动态路由该值会被后端返回的值覆盖) */
routeHomeName: AuthRoute.RouteKey;
routeHomeName: AuthRoute.AllRouteKey;
/** 菜单 */
menus: GlobalMenuOption[];
menus: App.GlobalMenuOption[];
/** 搜索的菜单 */
searchMenus: AuthRoute.Route[];
/** 缓存的路由名称 */
@ -54,7 +53,7 @@ export const useRouteStore = defineStore('route-store', {
resetRoutes() {
const routes = router.getRoutes();
routes.forEach(route => {
const name: AuthRoute.RouteKey = (route.name || 'root') as AuthRoute.RouteKey;
const name = (route.name || 'root') as AuthRoute.AllRouteKey;
if (!this.isConstantRoute(name)) {
router.removeRoute(name);
}
@ -64,7 +63,7 @@ export const useRouteStore = defineStore('route-store', {
*
* @param name
*/
isConstantRoute(name: AuthRoute.RouteKey) {
isConstantRoute(name: AuthRoute.AllRouteKey) {
const constantRouteNames = getConstantRouteNames(constantRoutes);
return constantRouteNames.includes(name);
},
@ -72,8 +71,8 @@ export const useRouteStore = defineStore('route-store', {
*
* @param name
*/
isValidConstantRoute(name: AuthRoute.RouteKey) {
const NOT_FOUND_PAGE_NAME: AuthRoute.RouteKey = 'not-found-page';
isValidConstantRoute(name: AuthRoute.AllRouteKey) {
const NOT_FOUND_PAGE_NAME: AuthRoute.NotFoundRouteKey = 'not-found';
const constantRouteNames = getConstantRouteNames(constantRoutes);
return constantRouteNames.includes(name) && name !== NOT_FOUND_PAGE_NAME;
},
@ -81,11 +80,11 @@ export const useRouteStore = defineStore('route-store', {
*
* @param routes -
*/
handleAuthRoutes(routes: AuthRoute.Route[]) {
(this.menus as GlobalMenuOption[]) = transformAuthRouteToMenu(routes);
this.searchMenus = transformAuthRoutesToSearchMenus(routes);
handleAuthRoute(routes: AuthRoute.Route[]) {
(this.menus as App.GlobalMenuOption[]) = transformAuthRouteToMenu(routes);
this.searchMenus = transformAuthRouteToSearchMenus(routes);
const vueRoutes = transformAuthRoutesToVueRoutes(routes);
const vueRoutes = transformAuthRouteToVueRoutes(routes);
vueRoutes.forEach(route => {
router.addRoute(route);
@ -94,12 +93,12 @@ export const useRouteStore = defineStore('route-store', {
this.cacheRoutes = getCacheRoutes(vueRoutes);
},
/** 动态路由模式下:更新根路由的重定向 */
handleUpdateRootRedirect(routeKey: AuthRoute.RouteKey) {
if (routeKey === 'root' || routeKey === 'not-found-page') {
throw new Error('routeKey的值不能为root或者not-found-page');
handleUpdateRootRedirect(routeKey: AuthRoute.AllRouteKey) {
if (routeKey === 'root' || routeKey === 'not-found') {
throw new Error('routeKey的值不能为root或者not-found');
}
const rootRoute: AuthRoute.Route = { ...ROOT_ROUTE, redirect: transformRouteNameToRoutePath(routeKey) };
const rootRouteName: AuthRoute.RouteKey = 'root';
const rootRouteName: AuthRoute.AllRouteKey = 'root';
router.removeRoute(rootRouteName);
const rootVueRoute = transformAuthRouteToVueRoute(rootRoute)[0];
router.addRoute(rootVueRoute);
@ -111,14 +110,14 @@ export const useRouteStore = defineStore('route-store', {
if (data) {
this.routeHomeName = data.home;
this.handleUpdateRootRedirect(data.home);
this.handleAuthRoutes(data.routes);
this.handleAuthRoute(data.routes);
}
},
/** 初始化静态路由 */
async initStaticRoute() {
const auth = useAuthStore();
const routes = filterAuthRoutesByUserPermission(staticRoutes, auth.userInfo.userRole);
this.handleAuthRoutes(routes);
this.handleAuthRoute(routes);
},
/** 初始化权限路由 */
async initAuthRoute() {

View File

@ -9,7 +9,7 @@ import { getLocal, setLocal } from '@/utils';
export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocationNormalizedLoaded) {
const fullPath = hasFullPath(route) ? route.fullPath : route.path;
const tabRoute: GlobalTabRoute = {
const tabRoute: App.GlobalTabRoute = {
name: route.name,
fullPath,
meta: route.meta,
@ -26,7 +26,7 @@ export function getTabRouteByVueRoute(route: RouteRecordNormalized | RouteLocati
* @param tabs -
* @param fullPath -
*/
export function getIndexInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) {
export function getIndexInTabRoutes(tabs: App.GlobalTabRoute[], fullPath: string) {
return tabs.findIndex(tab => tab.fullPath === fullPath);
}
@ -35,7 +35,7 @@ export function getIndexInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) {
* @param tabs -
* @param fullPath -
*/
export function isInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) {
export function isInTabRoutes(tabs: App.GlobalTabRoute[], fullPath: string) {
return getIndexInTabRoutes(tabs, fullPath) > -1;
}
@ -44,7 +44,7 @@ export function isInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) {
* @param tabs -
* @param routeName -
*/
export function getIndexInTabRoutesByRouteName(tabs: GlobalTabRoute[], routeName: string) {
export function getIndexInTabRoutesByRouteName(tabs: App.GlobalTabRoute[], routeName: string) {
return tabs.findIndex(tab => tab.name === routeName);
}
@ -59,14 +59,14 @@ function hasFullPath(
}
/** 缓存多页签数据 */
export function setTabRoutes(data: GlobalTabRoute[]) {
export function setTabRoutes(data: App.GlobalTabRoute[]) {
setLocal(EnumStorageKey['multi-tab-routes'], data);
}
/** 获取缓存的多页签数据 */
export function getTabRoutes() {
const routes: GlobalTabRoute[] = [];
const data = getLocal<GlobalTabRoute[]>(EnumStorageKey['multi-tab-routes']);
const routes: App.GlobalTabRoute[] = [];
const data = getLocal<App.GlobalTabRoute[]>(EnumStorageKey['multi-tab-routes']);
if (data) {
const defaultTabRoutes = data.map(item => ({
...item,

View File

@ -14,9 +14,9 @@ import {
interface TabState {
/** 多页签数据 */
tabs: GlobalTabRoute[];
tabs: App.GlobalTabRoute[];
/** 多页签首页 */
homeTab: GlobalTabRoute;
homeTab: App.GlobalTabRoute;
/** 当前激活状态的页签(路由fullPath) */
activeTab: string;
}
@ -213,7 +213,7 @@ export const useTabStore = defineStore('tab-store', {
iniTabStore(currentRoute: RouteLocationNormalizedLoaded) {
const theme = useThemeStore();
const tabs: GlobalTabRoute[] = theme.tab.isCache ? getTabRoutes() : [];
const tabs: App.GlobalTabRoute[] = theme.tab.isCache ? getTabRoutes() : [];
const hasHome = getIndexInTabRoutesByRouteName(tabs, this.homeTab.name as string) > -1;
if (!hasHome && this.homeTab.name !== 'root') {

View File

@ -18,7 +18,7 @@ declare namespace ApiRoute {
/** 动态路由 */
routes: AuthRoute.Route[];
/** 路由首页对应的key */
home: AuthRoute.RouteKey;
home: AuthRoute.AllRouteKey;
}
}

View File

@ -34,7 +34,7 @@ interface ImportMetaEnv {
*/
readonly VITE_AUTH_ROUTE_MODE: 'static' | 'dynamic';
/** 路由首页的路径 */
readonly VITE_ROUTE_HOME_PATH: Exclude<AuthRoute.RoutePath, '/' | '/not-found-page' | '/:pathMatch(.*)*'>;
readonly VITE_ROUTE_HOME_PATH: AuthRoute.RoutePath;
/** iconify图标作为组件的前缀 */
readonly VITE_ICON_PREFFIX: string;
/**

182
src/typings/route.d.ts vendored
View File

@ -1,74 +1,22 @@
/** 权限路由相关类型 */
declare namespace AuthRoute {
/** 多级路由分割符号 */
type RouteSplitMark = '_';
/** 根路由路径 */
type RootRoutePath = '/';
/** 路由的key */
type RouteKey =
// 固定的路由
| 'root' // 根路由
| 'login'
| 'not-found'
| 'no-permission'
| 'service-error'
| 'constant-page'
| 'not-found-page' // 捕获无效path的路由
// 自定义路由
| 'dashboard'
| 'dashboard_analysis'
| 'dashboard_workbench'
| 'document'
| 'document_vue'
| 'document_vite'
| 'document_naive'
| 'document_project'
| 'document_project-link'
| 'component'
| 'component_button'
| 'component_card'
| 'component_table'
| 'plugin'
| 'plugin_map'
| 'plugin_video'
| 'plugin_editor'
| 'plugin_editor_quill'
| 'plugin_editor_markdown'
| 'plugin_copy'
| 'plugin_icon'
| 'plugin_print'
| 'plugin_swiper'
| 'plugin_charts'
| 'plugin_charts_echarts'
| 'plugin_charts_antv'
| 'auth-demo'
| 'auth-demo_permission'
| 'auth-demo_super'
| 'function'
| 'function_tab'
| 'function_tab-detail'
| 'function_tab-multi-detail'
| 'exception'
| 'exception_403'
| 'exception_404'
| 'exception_500'
| 'multi-menu'
| 'multi-menu_first'
| 'multi-menu_first_second'
| 'multi-menu_first_second-new'
| 'multi-menu_first_second-new_third'
| 'management'
| 'management_user'
| 'management_role'
| 'management_auth'
| 'management_route'
| 'about';
/** 捕获无效路由的路由路径 */
type NotFoundRoutePath = '/:pathMatch(.*)*';
/** 路由的path */
type RoutePath =
| '/'
| Exclude<KeyToPath<RouteKey>, '/root' | '/not-found-page'>
| SingleRouteParentPath
| '/:pathMatch(.*)*';
type RootRouteKey = RouterPage.RootRouteKey;
type NotFoundRouteKey = RouterPage.NotFoundRouteKey;
type RouteKey = RouterPage.RouteKey;
type LastDegreeRouteKey = RouterPage.LastDegreeRouteKey;
type AllRouteKey = RouteKey | RootRouteKey | NotFoundRouteKey;
/** 路由路径 */
type RoutePath<K extends AllRouteKey = AllRouteKey> = AuthRouteUtils.GetRoutePath<K>;
/**
*
@ -77,16 +25,16 @@ declare namespace AuthRoute {
* - multi - ()
* - self - 使()
*/
type RouteComponent = 'basic' | 'blank' | 'multi' | 'self';
type RouteComponentType = 'basic' | 'blank' | 'multi' | 'self';
/** 路由描述 */
interface RouteMeta {
/** 路由标题(可用来作document.title或者菜单的名称) */
title: string;
/** 路由的动态路径(需要动态路径的页面需要将path添加进范型参数) */
dynamicPath?: PathToDynamicPath<'/login'>;
dynamicPath?: AuthRouteUtils.GetDynamicPath<'/login'>;
/** 作为单级路由的父级路由布局组件 */
singleLayout?: Extract<RouteComponent, 'basic' | 'blank'>;
singleLayout?: Extract<RouteComponentType, 'basic' | 'blank'>;
/** 需要登录权限 */
requiresAuth?: boolean;
/**
@ -96,7 +44,7 @@ declare namespace AuthRoute {
permissions?: Auth.RoleType[];
/** 缓存页面 */
keepAlive?: boolean;
/** 菜单和面包屑对应的图标(iconify图标名称) */
/** 菜单和面包屑对应的图标 */
icon?: string;
/** 使用本地svg作为的菜单和面包屑对应的图标(assets/svg-icon文件夹的的svg文件名) */
localIcon?: string;
@ -114,14 +62,14 @@ declare namespace AuthRoute {
multi?: boolean;
}
/** 单个路由的类型结构(动态路由模式:后端返回此类型结构的路由) */
interface Route {
type Route<K extends AllRouteKey = AllRouteKey> = K extends AllRouteKey
? {
/** 路由名称(路由唯一标识) */
name: RouteKey;
name: K;
/** 路由路径 */
path: RoutePath;
path: AuthRouteUtils.GetRoutePath<K>;
/** 路由重定向 */
redirect?: RoutePath;
redirect?: AuthRouteUtils.GetRoutePath;
/**
*
* - basic: 基础布局
@ -129,47 +77,71 @@ declare namespace AuthRoute {
* - multi: 多级路由布局()
* - self: 作为子路由使()
*/
component?: RouteComponent;
component?: RouteComponentType;
/** 子路由 */
children?: Route[];
/** 路由描述 */
meta: RouteMeta;
/** 路由属性 */
props?: boolean | Record<string, any> | ((to: any) => Record<string, any>);
}
} & Omit<import('vue-router').RouteRecordRaw, 'name' | 'path' | 'redirect' | 'component' | 'children' | 'meta'>
: never;
/** 前端导入的路由模块 */
type RouteModule = Record<string, { default: AuthRoute.Route }>;
type RouteModule = Record<string, { default: Route }>;
}
/** 单独一级路由的key (单独路由需要添加一个父级路由用于应用布局组件) */
declare namespace AuthRouteUtils {
/** 路由key层级分割符 */
type RouteKeySplitMark = '_';
/** 路由path层级分割符 */
type RoutePathSplitMark = '/';
/** 空白字符串 */
type BlankString = '';
/** key转换成path */
type KeyToPath<K extends string> = K extends `${infer _Left}${RouteKeySplitMark}${RouteKeySplitMark}${infer _Right}`
? never
: K extends `${infer Left}${RouteKeySplitMark}${infer Right}`
? Left extends BlankString
? never
: Right extends BlankString
? never
: KeyToPath<`${Left}${RoutePathSplitMark}${Right}`>
: `${RoutePathSplitMark}${K}`;
/** 根据路由key获取路由路径 */
type GetRoutePath<K extends AuthRoute.AllRouteKey = AuthRoute.AllRouteKey> = K extends AuthRoute.AllRouteKey
? K extends AuthRoute.RootRouteKey
? AuthRoute.RootRoutePath
: K extends AuthRoute.NotFoundRouteKey
? AuthRoute.NotFoundRoutePath
: KeyToPath<K>
: never;
/** 获取一级路由(有子路由的一级路由和没有子路由的路由) */
type GetFirstDegreeRouteKey<K extends AuthRoute.RouteKey = AuthRoute.RouteKey> =
K extends `${infer _Left}${RouteKeySplitMark}${infer _Right}` ? never : K;
/** 获取有子路由的一级路由 */
type GetFirstDegreeRouteKeyWithChildren<K extends AuthRoute.RouteKey = AuthRoute.RouteKey> =
K extends `${infer Left}${RouteKeySplitMark}${infer _Right}` ? Left : never;
/** 单级路由的key (单级路由需要添加一个父级路由用于应用布局组件) */
type SingleRouteKey = Exclude<
GetSingleRouteKey<RouteKey>,
GetRouteFirstParentKey<RouteKey> | 'root' | 'not-found-page'
GetFirstDegreeRouteKey,
GetFirstDegreeRouteKeyWithChildren | AuthRoute.RootRouteKey | AuthRoute.NotFoundRouteKey
>;
/** 单独路由父级路由key */
type SingleRouteParentKey = `${SingleRouteKey}-parent`;
/** 单独路由父级路由path */
type SingleRouteParentPath = KeyToPath<SingleRouteParentKey>;
/** 路由key转换路由path */
type KeyToPath<Key extends string> = Key extends `${infer Left}_${infer Right}`
? KeyToPath<`${Left}/${Right}`>
: `/${Key}`;
/** 路由path转换动态路径 */
type PathToDynamicPath<Path extends RoutePath> =
| `${Path}/:${string}`
| `${Path}/:${string}(${string})`
| `${Path}/:${string}(${string})?`;
/** 获取一级路由(包括有子路由的一级路由) */
type GetSingleRouteKey<Key extends RouteKey> = Key extends `${infer _Left}${RouteSplitMark}${infer _Right}`
? never
: Key;
/** 获取子路由的一级父路由 */
type GetRouteFirstParentKey<Key extends RouteKey> = Key extends `${infer Left}${RouteSplitMark}${infer _Right}`
? Left
: never;
/** 获取路由动态路径 */
type GetDynamicPath<P extends AuthRoute.RoutePath> =
| `${P}/:${string}`
| `${P}/:${string}(${string})`
| `${P}/:${string}(${string})?`;
}

View File

@ -1,200 +0,0 @@
declare namespace AuthRoutes {
/** 路由key */
type RouteKey = ConstantRouteKey | AuthRouteKey;
/** 根路由key */
type RootRouteKey = 'root';
/** 捕获无效路由的路由key */
type RouteCaptureKey = 'not-found-page';
/** 固定的路由key */
type ConstantRouteKey = RootRouteKey | 'login' | 'not-found' | 'no-permission' | 'service-error' | RouteCaptureKey;
/** 权限路由key */
type AuthRouteKey =
| 'dashboard'
| 'dashboard_analysis'
| 'dashboard_workbench'
| 'document'
| 'document_vue'
| 'document_vue-new'
| 'document_vite'
| 'document_naive'
| 'document_project'
| 'component'
| 'component_button'
| 'component_card'
| 'component_table'
| 'plugin'
| 'plugin_map'
| 'plugin_video'
| 'plugin_editor'
| 'plugin_editor_quill'
| 'plugin_editor_markdown'
| 'plugin_copy'
| 'plugin_icon'
| 'plugin_print'
| 'plugin_swiper'
| 'plugin_charts'
| 'plugin_charts_echarts'
| 'plugin_charts_antv'
| 'auth-demo'
| 'auth-demo_permission'
| 'auth-demo_super'
| 'function'
| 'function_tab'
| 'function_tab-detail'
| 'function_tab-multi-detail'
| 'exception'
| 'exception_403'
| 'exception_404'
| 'exception_500'
| 'multi-menu'
| 'multi-menu_first'
| 'multi-menu_first_second'
| 'multi-menu_first_second-new'
| 'multi-menu_first_second-new_third'
| 'management'
| 'management_user'
| 'management_role'
| 'management_auth'
| 'management_route'
| 'about';
/** 根路由路径 */
type RootRoutePath = '/';
/** 捕获无效路由的路由路径 */
type RouteCapturePath = '/:pathMatch(.*)*';
/** 路由路径 */
type RoutePath<K extends RouteKey = RouteKey> = AuthRouteUtils.GetRoutePath<K>;
/** 常用的路由路径 */
type CommonRoutePath = Exclude<RoutePath, RootRoutePath | RouteCapturePath>;
/**
*
* - basic -
* - blank -
* - multi - ()
* - self - 使()
*/
type RouteComponent = 'basic' | 'blank' | 'multi' | 'self';
/** 路由描述 */
interface RouteMeta {
/** 路由标题(可用来作document.title或者菜单的名称) */
title: string;
/** 路由的动态路径(需要动态路径的页面需要将path添加进范型参数) */
dynamicPath?: AuthRouteUtils.GetDynamicPath<'/login'>;
/** 作为单级路由的父级路由布局组件 */
singleLayout?: Extract<RouteComponent, 'basic' | 'blank'>;
/** 需要登录权限 */
requiresAuth?: boolean;
/**
* 访()
* @description
*/
permissions?: Auth.RoleType[];
/** 缓存页面 */
keepAlive?: boolean;
/** 菜单和面包屑对应的图标 */
icon?: string;
/** 自定义的菜单和面包屑对应的图标 */
customIcon?: string;
/** 是否在菜单中隐藏(一些列表、表格的详情页面需要通过参数跳转,所以不能显示在菜单中) */
hide?: boolean;
/** 外链链接 */
href?: string;
/** 是否支持多个tab页签(默认一个即相同name的路由会被替换) */
multiTab?: boolean;
/** 路由顺序,可用于菜单的排序 */
order?: number;
/** 当前路由需要选中的菜单项(用于跳转至不在左侧菜单显示的路由且需要高亮某个菜单的情况) */
activeMenu?: RouteKey;
/** 表示是否是多级路由的中间级路由(用于转换路由数据时筛选多级路由的标识,定义路由时不用填写) */
multi?: boolean;
}
type Route<K extends RouteKey = RouteKey> = K extends RouteKey
? {
/** 路由名称(路由唯一标识) */
name: K;
/** 路由路径 */
path: AuthRouteUtils.GetRoutePath<K>;
/** 路由重定向 */
redirect?: CommonRoutePath;
/**
*
* - basic: 基础布局
* - blank: 空白布局
* - multi: 多级路由布局()
* - self: 作为子路由使()
*/
component?: RouteComponent;
/** 子路由 */
children?: Route[];
/** 路由描述 */
meta: RouteMeta;
} & Omit<import('vue-router').RouteRecordRaw, 'name' | 'path' | 'redirect' | 'component' | 'children' | 'meta'>
: never;
}
declare namespace AuthRouteUtils {
/** 路由key层级分割符 */
type RouteKeySplitMark = '_';
/** 路由path层级分割符 */
type RoutePathSplitMark = '/';
/** 空白字符串 */
type BlankString = '';
/** key转换成path */
type KeyToPath<K extends string> = K extends `${infer _Left}${RouteKeySplitMark}${RouteKeySplitMark}${infer _Right}`
? never
: K extends `${infer Left}${RouteKeySplitMark}${infer Right}`
? Left extends BlankString
? never
: Right extends BlankString
? never
: KeyToPath<`${Left}${RoutePathSplitMark}${Right}`>
: `${RoutePathSplitMark}${K}`;
/** 根据路由key获取路由路径 */
type GetRoutePath<K extends AuthRoutes.RouteKey = AuthRoutes.RouteKey> = K extends AuthRoutes.RouteKey
? K extends AuthRoutes.RootRouteKey
? AuthRoutes.RootRoutePath
: K extends AuthRoutes.RouteCaptureKey
? AuthRoutes.RouteCapturePath
: KeyToPath<K>
: never;
/** 获取一级路由(有子路由的一级路由和没有子路由的路由) */
type GetFirstDegreeRouteKey<K extends AuthRoutes.RouteKey = AuthRoutes.RouteKey> =
K extends `${infer _Left}${RouteKeySplitMark}${infer _Right}` ? never : K;
/** 获取有子路由的一级路由 */
type GetFirstDegreeRouteKeyWithChildren<K extends AuthRoutes.RouteKey = AuthRoutes.RouteKey> =
K extends `${infer Left}${RouteKeySplitMark}${infer _Right}` ? Left : never;
/** 单级路由的key (单级路由需要添加一个父级路由用于应用布局组件) */
type SingleRouteKey = Exclude<
GetFirstDegreeRouteKey,
GetFirstDegreeRouteKeyWithChildren | AuthRoutes.RootRouteKey | AuthRoutes.RouteCaptureKey
>;
/** 单独路由父级路由key */
type SingleRouteParentKey = `${SingleRouteKey}-parent`;
/** 单独路由父级路由path */
type SingleRouteParentPath = KeyToPath<SingleRouteParentKey>;
/** 获取路由动态路径 */
type GetDynamicPath<P extends AuthRoutes.CommonRoutePath> =
| `${P}/:${string}`
| `${P}/:${string}(${string})`
| `${P}/:${string}(${string})?`;
}

View File

@ -254,49 +254,48 @@ declare namespace Theme {
}
}
/** 全局头部属性 */
interface GlobalHeaderProps {
declare namespace App {
/** 全局头部属性 */
interface GlobalHeaderProps {
/** 显示logo */
showLogo: boolean;
/** 显示头部菜单 */
showHeaderMenu: boolean;
/** 显示菜单折叠按钮 */
showMenuCollapse: boolean;
}
}
/** 菜单项配置 */
type GlobalMenuOption = import('naive-ui').MenuOption & {
/** 菜单项配置 */
type GlobalMenuOption = import('naive-ui').MenuOption & {
key: string;
label: string;
routeName: string;
routePath: string;
icon?: () => import('vue').VNodeChild;
children?: GlobalMenuOption[];
};
};
/** 面包屑 */
type GlobalBreadcrumb = import('naive-ui').DropdownOption & {
/** 面包屑 */
type GlobalBreadcrumb = import('naive-ui').DropdownOption & {
key: string;
label: string;
disabled: boolean;
routeName: string;
hasChildren: boolean;
children?: GlobalBreadcrumb[];
};
};
/** 多页签Tab的路由 */
interface GlobalTabRoute
/** 多页签Tab的路由 */
interface GlobalTabRoute
extends Pick<import('vue-router').RouteLocationNormalizedLoaded, 'name' | 'fullPath' | 'meta'> {
/** 滚动的位置 */
scrollPosition: {
left: number;
top: number;
};
}
}
/** 系统消息 */
declare namespace Message {
interface Tab {
interface MessageTab {
/** tab的key */
key: number;
/** tab名称 */
@ -304,10 +303,10 @@ declare namespace Message {
/** badge类型 */
badgeProps?: import('naive-ui').BadgeProps;
/** 消息数据 */
list: List[];
list: MessageList[];
}
interface List {
interface MessageList {
/** 数据唯一值 */
id: number;
/** 头像 */

View File

@ -1,38 +1,57 @@
import { EnumDataType } from '@/enum';
export function isNumber(data: unknown) {
export function isNumber<T extends number>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.number;
}
export function isString(data: unknown) {
export function isString<T extends string>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.string;
}
export function isBoolean(data: unknown) {
export function isBoolean<T extends boolean>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.boolean;
}
export function isNull(data: unknown) {
export function isNull<T extends null>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.null;
}
export function isUndefined(data: unknown) {
export function isUndefined<T extends undefined>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.undefined;
}
export function isObject(data: unknown) {
export function isObject<T extends Record<string, any>>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.object;
}
export function isArray(data: unknown) {
export function isArray<T extends any[]>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.array;
}
export function isDate(data: unknown) {
export function isFunction<T extends (...args: any[]) => any | void | never>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.function;
}
export function isDate<T extends Date>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.date;
}
export function isRegExp(data: unknown) {
export function isRegExp<T extends RegExp>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.regexp;
}
export function isSet(data: unknown) {
export function isPromise<T extends Promise<any>>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.promise;
}
export function isSet<T extends Set<any>>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.set;
}
export function isMap(data: unknown) {
export function isMap<T extends Map<any, any>>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.map;
}
export function isFile(data: unknown) {
export function isFile<T extends File>(data: T | unknown): data is T {
return Object.prototype.toString.call(data) === EnumDataType.file;
}

View File

@ -4,7 +4,7 @@
* @param menus -
* @param rootPath -
*/
export function getBreadcrumbByRouteKey(activeKey: string, menus: GlobalMenuOption[], rootPath: string) {
export function getBreadcrumbByRouteKey(activeKey: string, menus: App.GlobalMenuOption[], rootPath: string) {
const breadcrumbMenu = getBreadcrumbMenu(activeKey, menus);
const breadcrumb = breadcrumbMenu.map(item => transformBreadcrumbMenuToBreadcrumb(item, rootPath));
return breadcrumb;
@ -15,8 +15,8 @@ export function getBreadcrumbByRouteKey(activeKey: string, menus: GlobalMenuOpti
* @param activeKey - key
* @param menus -
*/
function getBreadcrumbMenu(activeKey: string, menus: GlobalMenuOption[]) {
const breadcrumbMenu: GlobalMenuOption[] = [];
function getBreadcrumbMenu(activeKey: string, menus: App.GlobalMenuOption[]) {
const breadcrumbMenu: App.GlobalMenuOption[] = [];
menus.some(menu => {
const flag = activeKey.includes(menu.routeName);
if (flag) {
@ -32,15 +32,15 @@ function getBreadcrumbMenu(activeKey: string, menus: GlobalMenuOption[]) {
* @param activeKey - key
* @param menu -
*/
function getBreadcrumbMenuItem(activeKey: string, menu: GlobalMenuOption) {
const breadcrumbMenu: GlobalMenuOption[] = [];
function getBreadcrumbMenuItem(activeKey: string, menu: App.GlobalMenuOption) {
const breadcrumbMenu: App.GlobalMenuOption[] = [];
if (activeKey === menu.routeName) {
breadcrumbMenu.push(menu);
}
if (activeKey.includes(menu.routeName) && menu.children && menu.children.length) {
breadcrumbMenu.push(menu);
breadcrumbMenu.push(
...menu.children.map(item => getBreadcrumbMenuItem(activeKey, item as GlobalMenuOption)).flat(1)
...menu.children.map(item => getBreadcrumbMenuItem(activeKey, item as App.GlobalMenuOption)).flat(1)
);
}
@ -52,9 +52,9 @@ function getBreadcrumbMenuItem(activeKey: string, menu: GlobalMenuOption) {
* @param menu -
* @param rootPath -
*/
function transformBreadcrumbMenuToBreadcrumb(menu: GlobalMenuOption, rootPath: string) {
function transformBreadcrumbMenuToBreadcrumb(menu: App.GlobalMenuOption, rootPath: string) {
const hasChildren = Boolean(menu.children && menu.children.length);
const breadcrumb: GlobalBreadcrumb = {
const breadcrumb: App.GlobalBreadcrumb = {
key: menu.routeName,
label: menu.label as string,
routeName: menu.routeName,
@ -66,7 +66,7 @@ function transformBreadcrumbMenuToBreadcrumb(menu: GlobalMenuOption, rootPath: s
}
if (hasChildren) {
breadcrumb.children = menu.children?.map(item =>
transformBreadcrumbMenuToBreadcrumb(item as GlobalMenuOption, rootPath)
transformBreadcrumbMenuToBreadcrumb(item as App.GlobalMenuOption, rootPath)
);
}
return breadcrumb;

View File

@ -1,35 +0,0 @@
import type { Component } from 'vue';
import { BasicLayout, BlankLayout } from '@/layouts';
import { views } from '@/views';
type LayoutComponent = Record<EnumType.LayoutComponentName, () => Promise<Component>>;
/**
* vue文件()
* @param layoutType -
*/
export function getLayoutComponent(layoutType: EnumType.LayoutComponentName) {
const layoutComponent: LayoutComponent = {
basic: BasicLayout,
blank: BlankLayout
};
return layoutComponent[layoutType];
}
/**
* vue文件()
* @param routeKey - key
*/
export function getViewComponent(routeKey: AuthRoute.RouteKey) {
if (!views[routeKey]) {
window.console.error(`路由“${routeKey}”没有对应的组件文件!`);
}
return () => setViewComponentName(views[routeKey], routeKey) as Promise<Component>;
}
/** 给页面组件设置名称 */
async function setViewComponentName(asyncComponent: () => Promise<Component>, name: string) {
const component = (await asyncComponent()) as { default: Component };
Object.assign(component.default, { name });
return component;
}

View File

@ -1,6 +1,3 @@
import type { RouteRecordRaw } from 'vue-router';
import { getLayoutComponent, getViewComponent } from './component';
/**
*
* @param routes -
@ -9,41 +6,30 @@ export function getConstantRouteNames(routes: AuthRoute.Route[]) {
return routes.map(route => getConstantRouteName(route)).flat(1);
}
/**
* vue路由
* @param routes -
* @description
*/
export function transformAuthRoutesToVueRoutes(routes: AuthRoute.Route[]) {
return routes.map(route => transformAuthRouteToVueRoute(route)).flat(1);
}
/**
*
* @param routes -
* @param treeMap
*/
export function transformAuthRoutesToSearchMenus(routes: AuthRoute.Route[], treeMap: AuthRoute.Route[] = []) {
export function transformAuthRouteToSearchMenus(routes: AuthRoute.Route[], treeMap: AuthRoute.Route[] = []) {
if (routes && routes.length === 0) return [];
return routes.reduce((acc, cur) => {
if (!cur.meta?.hide) {
acc.push(cur);
}
if (cur.children && cur.children.length > 0) {
transformAuthRoutesToSearchMenus(cur.children, treeMap);
transformAuthRouteToSearchMenus(cur.children, treeMap);
}
return acc;
}, treeMap);
}
/** 将路由名字转换成路由路径 */
export function transformRouteNameToRoutePath(
name: Exclude<AuthRoute.RouteKey, 'not-found-page'>
): AuthRoute.RoutePath {
export function transformRouteNameToRoutePath(name: Exclude<AuthRoute.AllRouteKey, 'not-found'>): AuthRoute.RoutePath {
const rootPath: AuthRoute.RoutePath = '/';
if (name === 'root') return rootPath;
const splitMark: AuthRoute.RouteSplitMark = '_';
const splitMark = '_';
const pathSplitMark = '/';
const path = name.split(splitMark).join(pathSplitMark);
@ -51,15 +37,13 @@ export function transformRouteNameToRoutePath(
}
/** 将路由路径转换成路由名字 */
export function transformRoutePathToRouteName(
path: Exclude<AuthRoute.RoutePath, '/not-found-page' | '/:pathMatch(.*)*'>
): AuthRoute.RouteKey {
export function transformRoutePathToRouteName<K extends AuthRoute.RoutePath>(path: K) {
if (path === '/') return 'root';
const pathSplitMark = '/';
const routeSplitMark: AuthRoute.RouteSplitMark = '_';
const routeSplitMark = '_';
const name = path.split(pathSplitMark).slice(1).join(routeSplitMark) as AuthRoute.RouteKey;
const name = path.split(pathSplitMark).slice(1).join(routeSplitMark) as AuthRoute.AllRouteKey;
return name;
}
@ -76,140 +60,6 @@ function getConstantRouteName(route: AuthRoute.Route) {
return names;
}
type ComponentAction = Record<AuthRoute.RouteComponent, () => void>;
/**
* vue路由
* @param item -
*/
export function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
const resultRoute: RouteRecordRaw[] = [];
const itemRoute = { ...item } as RouteRecordRaw;
// 动态path
if (hasDynamicPath(item)) {
Object.assign(itemRoute, { path: item.meta.dynamicPath });
}
// 外链路由
if (hasHref(item)) {
Object.assign(itemRoute, { component: getViewComponent('not-found-page') });
}
// 路由组件
if (hasComponent(item)) {
const action: ComponentAction = {
basic() {
itemRoute.component = getLayoutComponent('basic');
},
blank() {
itemRoute.component = getLayoutComponent('blank');
},
multi() {
// 多级路由一定有子路由
if (hasChildren(item)) {
Object.assign(itemRoute, { meta: { ...itemRoute.meta, multi: true } });
delete itemRoute.component;
} else {
window.console.error('多级路由缺少子路由: ', item);
}
},
self() {
itemRoute.component = getViewComponent(item.name);
}
};
try {
if (item.component) {
action[item.component]();
} else {
window.console.error('路由组件解析失败: ', item);
}
} catch {
window.console.error('路由组件解析失败: ', item);
}
}
// 注意单独路由没有children
if (isSingleRoute(item)) {
if (hasChildren(item)) {
window.console.error('单独路由不应该有子路由: ', item);
}
// 捕获无效路由的需特殊处理
if (item.name === 'not-found-page') {
itemRoute.children = [
{
path: '',
name: item.name,
component: getViewComponent('not-found-page')
}
];
} else {
const parentPath = `${itemRoute.path}-parent` as AuthRoute.SingleRouteParentPath;
const layout = item.meta.singleLayout === 'basic' ? getLayoutComponent('basic') : getLayoutComponent('blank');
const parentRoute: RouteRecordRaw = {
path: parentPath,
component: layout,
redirect: item.path,
children: [itemRoute]
};
return [parentRoute];
}
}
// 子路由
if (hasChildren(item)) {
const children = (item.children as AuthRoute.Route[]).map(child => transformAuthRouteToVueRoute(child)).flat();
// 找出第一个不为多级路由中间级的子路由路径作为重定向路径
const redirectPath: AuthRoute.RoutePath = (children.find(v => !v.meta?.multi)?.path || '/') as AuthRoute.RoutePath;
if (redirectPath === '/') {
window.console.error('该多级路由没有有效的子路径', item);
}
if (item.component === 'multi') {
// 多级路由,将子路由提取出来变成同级
resultRoute.push(...children);
delete itemRoute.children;
} else {
itemRoute.children = children;
}
itemRoute.redirect = redirectPath;
}
resultRoute.push(itemRoute);
return resultRoute;
}
/**
*
* @param item -
*/
function hasHref(item: AuthRoute.Route) {
return Boolean(item.meta.href);
}
/**
* path
* @param item -
*/
function hasDynamicPath(item: AuthRoute.Route) {
return Boolean(item.meta.dynamicPath);
}
/**
*
* @param item -
*/
function hasComponent(item: AuthRoute.Route) {
return Boolean(item.component);
}
/**
*
* @param item -
@ -217,11 +67,3 @@ function hasComponent(item: AuthRoute.Route) {
function hasChildren(item: AuthRoute.Route) {
return Boolean(item.children && item.children.length);
}
/**
*
* @param item -
*/
function isSingleRoute(item: AuthRoute.Route) {
return Boolean(item.meta.singleLayout);
}

View File

@ -7,10 +7,10 @@ function hideInMenu(route: AuthRoute.Route) {
/** 给菜单添加可选属性 */
function addPartialProps(config: {
menu: GlobalMenuOption;
menu: App.GlobalMenuOption;
icon?: string;
localIcon?: string;
children?: GlobalMenuOption[];
children?: App.GlobalMenuOption[];
}) {
const { iconRender } = useIconRender();
@ -36,16 +36,16 @@ function addPartialProps(config: {
*
* @param routes -
*/
export function transformAuthRouteToMenu(routes: AuthRoute.Route[]): GlobalMenuOption[] {
const globalMenu: GlobalMenuOption[] = [];
export function transformAuthRouteToMenu(routes: AuthRoute.Route[]): App.GlobalMenuOption[] {
const globalMenu: App.GlobalMenuOption[] = [];
routes.forEach(route => {
const { name, path, meta } = route;
const routeName = name as string;
let menuChildren: GlobalMenuOption[] | undefined;
let menuChildren: App.GlobalMenuOption[] | undefined;
if (route.children) {
menuChildren = transformAuthRouteToMenu(route.children);
}
const menuItem: GlobalMenuOption = addPartialProps({
const menuItem: App.GlobalMenuOption = addPartialProps({
menu: {
key: routeName,
label: meta.title,
@ -70,18 +70,18 @@ export function transformAuthRouteToMenu(routes: AuthRoute.Route[]): GlobalMenuO
* @param activeKey - key
* @param menus -
*/
export function getActiveKeyPathsOfMenus(activeKey: string, menus: GlobalMenuOption[]) {
export function getActiveKeyPathsOfMenus(activeKey: string, menus: App.GlobalMenuOption[]) {
const keys = menus.map(menu => getActiveKeyPathsOfMenu(activeKey, menu)).flat(1);
return keys;
}
function getActiveKeyPathsOfMenu(activeKey: string, menu: GlobalMenuOption) {
function getActiveKeyPathsOfMenu(activeKey: string, menu: App.GlobalMenuOption) {
const keys: string[] = [];
if (activeKey.includes(menu.routeName)) {
keys.push(menu.routeName);
}
if (menu.children) {
keys.push(...menu.children.map(item => getActiveKeyPathsOfMenu(activeKey, item as GlobalMenuOption)).flat(1));
keys.push(...menu.children.map(item => getActiveKeyPathsOfMenu(activeKey, item as App.GlobalMenuOption)).flat(1));
}
return keys;
}

View File

@ -0,0 +1,214 @@
import type { RouteComponent, RouteRecordRaw } from 'vue-router';
import { BasicLayout, BlankLayout } from '@/layouts';
import { views } from '@/views';
import { isFunction } from '@/utils';
type Lazy<T> = () => Promise<T>;
type LayoutComponent = Record<EnumType.LayoutComponentName, Lazy<RouteComponent>>;
/**
* vue文件()
* @param layoutType -
*/
export function getLayoutComponent(layoutType: EnumType.LayoutComponentName) {
const layoutComponent: LayoutComponent = {
basic: BasicLayout,
blank: BlankLayout
};
return layoutComponent[layoutType];
}
/**
* vue文件
* @param routeKey - key
*/
export function getViewComponent(routeKey: AuthRoute.LastDegreeRouteKey) {
if (!views[routeKey]) {
throw new Error(`路由“${routeKey}”没有对应的组件文件!`);
}
return setViewComponentName(views[routeKey], routeKey);
}
interface ModuleComponent {
default: RouteComponent;
}
/** 给页面组件设置名称 */
function setViewComponentName(component: RouteComponent | Lazy<ModuleComponent>, name: string) {
if (isAsyncComponent(component)) {
return async () => {
const result = await component();
Object.assign(result.default, { name });
return result;
};
}
Object.assign(component, { name });
return component;
}
function isAsyncComponent(component: RouteComponent | Lazy<ModuleComponent>): component is Lazy<ModuleComponent> {
return isFunction(component);
}
/**
*
* @param item -
*/
function hasHref(item: AuthRoute.Route) {
return Boolean(item.meta.href);
}
/**
* path
* @param item -
*/
function hasDynamicPath(item: AuthRoute.Route) {
return Boolean(item.meta.dynamicPath);
}
/**
*
* @param item -
*/
function hasComponent(item: AuthRoute.Route) {
return Boolean(item.component);
}
/**
*
* @param item -
*/
function hasChildren(item: AuthRoute.Route) {
return Boolean(item.children && item.children.length);
}
/**
*
* @param item -
*/
function isSingleRoute(item: AuthRoute.Route) {
return Boolean(item.meta.singleLayout);
}
/**
* vue路由
* @param routes -
* @description
*/
export function transformAuthRouteToVueRoutes(routes: AuthRoute.Route[]) {
return routes.map(route => transformAuthRouteToVueRoute(route)).flat(1);
}
type ComponentAction = Record<AuthRoute.RouteComponentType, () => void>;
/**
* vue路由
* @param item -
*/
export function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
const resultRoute: RouteRecordRaw[] = [];
const itemRoute = { ...item } as RouteRecordRaw;
// 动态path
if (hasDynamicPath(item)) {
Object.assign(itemRoute, { path: item.meta.dynamicPath });
}
// 外链路由
if (hasHref(item)) {
Object.assign(itemRoute, { component: getViewComponent('404') });
}
// 路由组件
if (hasComponent(item)) {
const action: ComponentAction = {
basic() {
itemRoute.component = getLayoutComponent('basic');
},
blank() {
itemRoute.component = getLayoutComponent('blank');
},
multi() {
// 多级路由一定有子路由
if (hasChildren(item)) {
Object.assign(itemRoute, { meta: { ...itemRoute.meta, multi: true } });
delete itemRoute.component;
} else {
window.console.error('多级路由缺少子路由: ', item);
}
},
self() {
itemRoute.component = getViewComponent(item.name as AuthRoute.LastDegreeRouteKey);
}
};
try {
if (item.component) {
action[item.component]();
} else {
window.console.error('路由组件解析失败: ', item);
}
} catch {
window.console.error('路由组件解析失败: ', item);
}
}
// 注意单独路由没有children
if (isSingleRoute(item)) {
if (hasChildren(item)) {
window.console.error('单独路由不应该有子路由: ', item);
}
// 捕获无效路由的需特殊处理
if (item.name === 'not-found') {
itemRoute.children = [
{
path: '',
name: item.name,
component: getViewComponent('not-found')
}
];
} else {
const parentPath = `${itemRoute.path}-parent` as AuthRouteUtils.SingleRouteKey;
const layout = item.meta.singleLayout === 'basic' ? getLayoutComponent('basic') : getLayoutComponent('blank');
const parentRoute: RouteRecordRaw = {
path: parentPath,
component: layout,
redirect: item.path,
children: [itemRoute]
};
return [parentRoute];
}
}
// 子路由
if (hasChildren(item)) {
const children = (item.children as AuthRoute.Route[]).map(child => transformAuthRouteToVueRoute(child)).flat();
// 找出第一个不为多级路由中间级的子路由路径作为重定向路径
const redirectPath = (children.find(v => !v.meta?.multi)?.path || '/') as AuthRoute.RoutePath;
if (redirectPath === '/') {
window.console.error('该多级路由没有有效的子路径', item);
}
if (item.component === 'multi') {
// 多级路由,将子路由提取出来变成同级
resultRoute.push(...children);
delete itemRoute.children;
} else {
itemRoute.children = children;
}
itemRoute.redirect = redirectPath;
}
resultRoute.push(itemRoute);
return resultRoute;
}

View File

@ -0,0 +1,7 @@
<template>
<div></div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@ -1,31 +1,45 @@
import type { Component } from 'vue';
import type { RouteComponent } from 'vue-router';
type ViewComponent = Record<string, () => Promise<Component>>;
const importViews = import.meta.glob('./**/index.vue') as ViewComponent;
const COMPONENTS_KEY = 'components';
const PREFIX = './';
const SUFFIX = '/index.vue';
const PATH_SPLIT_MARK = '/';
const ROUTE_KEY_SPLIT_MARK = '_';
/** 系统的内置路由该文件夹名称不作为RouteKey */
const SYSTEM_VIEW = 'system-view_';
/** 过滤掉组件文件 */
const viewKeys = Object.keys(importViews).filter(key => !key.includes(COMPONENTS_KEY));
function getViewComponent() {
const components: ViewComponent = {};
viewKeys.forEach(key => {
const routeKey = key
.replace(PREFIX, '')
.replace(SUFFIX, '')
.replace(new RegExp(PATH_SPLIT_MARK, 'g'), ROUTE_KEY_SPLIT_MARK)
.replace(SYSTEM_VIEW, '');
components[routeKey] = importViews[key];
});
return components;
}
export const views = getViewComponent();
export const views: Record<RouterPage.LastDegreeRouteKey, RouteComponent | (() => Promise<RouteComponent>)> = {
403: () => import('./_builtin/403/index.vue'),
404: () => import('./_builtin/404/index.vue'),
500: () => import('./_builtin/500/index.vue'),
'constant-page': () => import('./_builtin/constant-page/index.vue'),
login: () => import('./_builtin/login/index.vue'),
'not-found': () => import('./_builtin/not-found/index.vue'),
about: () => import('./about/index.vue'),
'auth-demo_permission': () => import('./auth-demo/permission/index.vue'),
'auth-demo_super': () => import('./auth-demo/super/index.vue'),
component_button: () => import('./component/button/index.vue'),
component_card: () => import('./component/card/index.vue'),
component_table: () => import('./component/table/index.vue'),
dashboard_analysis: () => import('./dashboard/analysis/index.vue'),
dashboard_workbench: () => import('./dashboard/workbench/index.vue'),
document_naive: () => import('./document/naive/index.vue'),
'document_project-link': () => import('./document/project-link/index.vue'),
document_project: () => import('./document/project/index.vue'),
document_vite: () => import('./document/vite/index.vue'),
document_vue: () => import('./document/vue/index.vue'),
exception_403: () => import('./exception/403/index.vue'),
exception_404: () => import('./exception/404/index.vue'),
exception_500: () => import('./exception/500/index.vue'),
'function_tab-detail': () => import('./function/tab-detail/index.vue'),
'function_tab-multi-detail': () => import('./function/tab-multi-detail/index.vue'),
function_tab: () => import('./function/tab/index.vue'),
management_auth: () => import('./management/auth/index.vue'),
management_role: () => import('./management/role/index.vue'),
management_route: () => import('./management/route/index.vue'),
management_user: () => import('./management/user/index.vue'),
'multi-menu_first_second-new_third': () => import('./multi-menu/first/second-new/third/index.vue'),
'multi-menu_first_second': () => import('./multi-menu/first/second/index.vue'),
plugin_charts_antv: () => import('./plugin/charts/antv/index.vue'),
plugin_charts_echarts: () => import('./plugin/charts/echarts/index.vue'),
plugin_copy: () => import('./plugin/copy/index.vue'),
plugin_editor_markdown: () => import('./plugin/editor/markdown/index.vue'),
plugin_editor_quill: () => import('./plugin/editor/quill/index.vue'),
plugin_icon: () => import('./plugin/icon/index.vue'),
plugin_map: () => import('./plugin/map/index.vue'),
plugin_print: () => import('./plugin/print/index.vue'),
plugin_swiper: () => import('./plugin/swiper/index.vue'),
plugin_video: () => import('./plugin/video/index.vue')
};