feat: 优化路由加载机制
parent
46c6c9a3a7
commit
d8230ad3b9
|
|
@ -0,0 +1,4 @@
|
||||||
|
# 参见 .env
|
||||||
|
|
||||||
|
VITE_BASE = ./
|
||||||
|
VITE_HISTORY = hash
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -18,5 +18,7 @@ COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
# 复制nginx配置
|
# 复制nginx配置
|
||||||
COPY --from=builder /app/.github/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY --from=builder /app/.github/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 显式暴露端口
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
# 启动,关闭后台运行启动前台运行,不然 docker 会结束运行
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
143
README.md
143
README.md
|
|
@ -53,20 +53,153 @@ pnpm dev
|
||||||
|
|
||||||
根据 src/pages 目录生成路由数组,包含以下以下规则:
|
根据 src/pages 目录生成路由数组,包含以下以下规则:
|
||||||
|
|
||||||
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹
|
- 以文件夹为路由,读取该文件夹下 index.vue 的信息作为路由信息,其他文件会跳过,可以包含子文件夹作为嵌套路由
|
||||||
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
- 在 src/pages 的文件夹层级,作为菜单层级,路由层级最终只有 2 层(配合 keep-alive 缓存)
|
||||||
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
- 在 src/pages 目录下,以 _ 开头的文件夹作为 1 级路由,其他作为 2 级路由,也就是应用路由
|
||||||
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
- 子文件夹下,只有 index.vue 文件有效,其他文件会忽略,这些文件可以作为子组件使用
|
||||||
- components 目录会被忽视
|
- components 目录会被忽视。
|
||||||
- xxx.xx.xx 文件会被忽视,例如 index.my.vue
|
- xxx.xx.xx 文件会被忽视,例如 index.my.vue 文件。
|
||||||
|
|
||||||
对应目录下的 index.vue 文件中定义如下路由配置:
|
对应目录下的 index.vue 文件中定义如下路由配置:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
"parentMeta": {
|
// 其他 Route 参数
|
||||||
// 具体属性查阅 src/types/vue-router.d.ts
|
"meta": {
|
||||||
|
// 请看下面
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
||||||
|
```
|
||||||
|
|
||||||
|
目前支持的参数,如下:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface RouteMeta {
|
||||||
|
/**
|
||||||
|
* 页面标题
|
||||||
|
* @description
|
||||||
|
* 菜单和导航面包屑等地方会用到
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
/**
|
||||||
|
* 页面图标
|
||||||
|
* @description
|
||||||
|
* 使用 icon-park-outline 图标集的图标类名
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
/**
|
||||||
|
* 显示顺序
|
||||||
|
* @description
|
||||||
|
* 在菜单中的显示顺序,越小越靠前
|
||||||
|
*/
|
||||||
|
sort?: number;
|
||||||
|
/**
|
||||||
|
* 是否隐藏
|
||||||
|
* @description
|
||||||
|
* - false // 不隐藏(默认)
|
||||||
|
* - true // 在路由和菜单中隐藏,即忽略且不打包
|
||||||
|
* - 'menu' // 在菜单中隐藏,通过其他方式访问
|
||||||
|
* - 'prod' // 在生产环境下隐藏
|
||||||
|
*/
|
||||||
|
hide?: boolean | 'menu' | 'prod';
|
||||||
|
/**
|
||||||
|
* 所需权限
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* ['system:user']
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
auth?: string[];
|
||||||
|
/**
|
||||||
|
* 是否缓存
|
||||||
|
* @description
|
||||||
|
* 是否使用 keep-alive 缓存
|
||||||
|
*/
|
||||||
|
cache?: boolean;
|
||||||
|
/**
|
||||||
|
* 组件名字
|
||||||
|
* @description
|
||||||
|
* 组件名字,当 cache为true 时必须
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* 是否显示loading
|
||||||
|
* @description
|
||||||
|
* 可以自定义 loading 文本
|
||||||
|
*/
|
||||||
|
loading?: boolean | string;
|
||||||
|
/**
|
||||||
|
* 链接
|
||||||
|
* @description
|
||||||
|
* ```js
|
||||||
|
* 'https://juetan.cn'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 嵌套布局
|
||||||
|
|
||||||
|
默认情况下,嵌套路由会使用父级 index.vue 作为布局文件,如果不需要布局,只需在父级路由指定 component 为 null 即可,如下:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"component": null,
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样,其层级仅作为菜单层级,在路由上表现为扁平。
|
||||||
|
|
||||||
|
### 路由权限
|
||||||
|
|
||||||
|
在每个路由的 index.vue 文件中,通过 meta.auth 字段指定访问该路由所需的权限,示例如下:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"auth": ["system:user", "system:menu"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
默认全部需要登陆才可访问,其中有 2 个比较特殊的权限:
|
||||||
|
|
||||||
|
- `*` 表示无需登陆即可访问,适合挂一些比较通用的页面。
|
||||||
|
- `unlogin` 表示未登录才可以访问。例如登录页,登陆后访问该页面会被拒绝。
|
||||||
|
|
||||||
|
用户登陆后获取的权限,应存储在 userStore.auth 字段中,在路由的 beforeEach 守卫中,会比较两个是否匹配,匹配上则继续,否则会显示如下 403 页面:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 动态路由
|
||||||
|
|
||||||
|
相比于比较流行的加法挂载,我更倾向于减法挂载。即默认加载完所有路由,在 beforeEach 钩子根据权限移除不必要的路由。
|
||||||
|
|
||||||
|
### 动态首页
|
||||||
|
|
||||||
|
在作为首页路由的 index.vue 文件中,指定 alias 为 '/' 即可,默认是 home/index.vue 文件。如需动态更新首页,在 beforeEach 获取完菜单信息,通过 removeRoute 移除旧的首页路由,通过 addRoute 添加新的首页路由即可。
|
||||||
|
|
||||||
|
### 路由缓存
|
||||||
|
|
||||||
|
在路由的 index.vue 文件,首先指定好组件的名字,再通过 cache 字段开启缓存,示例如下:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
defineOptions({
|
||||||
|
name: "MyPage"
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<route>
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
// 组件名字
|
||||||
|
"name": "MyPage",
|
||||||
|
// 开启缓存
|
||||||
|
"cache": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</route>
|
</route>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,11 @@ export default defineComponent({
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
() => {
|
() => {
|
||||||
selectedKeys.value = route.matched.map(i => i.path);
|
selectedKeys.value = route.matched.map(i => i.aliasOf?.path ?? i.path);
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function goto(route: MenuItem) {
|
function goto(route: MenuItem) {
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,7 @@ const buttons = [
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
|
"redirect": "/",
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "LayoutPage",
|
"name": "LayoutPage",
|
||||||
"sort": 101,
|
"sort": 101,
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ const stat = {
|
||||||
|
|
||||||
<route lang="json">
|
<route lang="json">
|
||||||
{
|
{
|
||||||
|
"alias": "/",
|
||||||
"meta": {
|
"meta": {
|
||||||
"sort": 1000,
|
"sort": 1000,
|
||||||
"title": "首页",
|
"title": "首页",
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,6 @@ const { component: UserTable } = useTable({
|
||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "SystemDepartmentPage",
|
"name": "SystemDepartmentPage",
|
||||||
"keepAlive": true,
|
|
||||||
"sort": 10301,
|
"sort": 10301,
|
||||||
"title": "部门管理",
|
"title": "部门管理",
|
||||||
"icon": "icon-park-outline-group"
|
"icon": "icon-park-outline-group"
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ const { component: RoleTable } = useTable({
|
||||||
"name": "SystemRolePage",
|
"name": "SystemRolePage",
|
||||||
"sort": 10302,
|
"sort": 10302,
|
||||||
"title": "角色管理",
|
"title": "角色管理",
|
||||||
|
"auth": ["role"],
|
||||||
"icon": "icon-park-outline-shield"
|
"icon": "icon-park-outline-shield"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,6 @@ const { component: UserTable } = useTable({
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"sort": 10301,
|
"sort": 10301,
|
||||||
"title": "用户管理",
|
"title": "用户管理",
|
||||||
"auth": ["*"],
|
|
||||||
"icon": "icon-park-outline-user"
|
"icon": "icon-park-outline-user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,16 @@ import { api } from '@/api';
|
||||||
import { env } from '@/config/env';
|
import { env } from '@/config/env';
|
||||||
import { store, useUserStore } from '@/store';
|
import { store, useUserStore } from '@/store';
|
||||||
import { useMenuStore } from '@/store/menu';
|
import { useMenuStore } from '@/store/menu';
|
||||||
import { treeEach, treeFilter, treeFind } from '@/utils/listToTree';
|
import { treeEach } from '@/utils/listToTree';
|
||||||
import { Notification } from '@arco-design/web-vue';
|
import { Notification } from '@arco-design/web-vue';
|
||||||
import { Router } from 'vue-router';
|
import { Router } from 'vue-router';
|
||||||
import { menus } from '../menus';
|
import { menus } from '../menus';
|
||||||
import { APP_HOME_NAME } from '../routes/base';
|
import { appRoutes } from '../routes/page';
|
||||||
import { APP_ROUTE_NAME, routes } from '../routes/page';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限守卫
|
* 权限守卫
|
||||||
* @param to 路由
|
* @param to 路由
|
||||||
* @description store不能放在外面,否则 pinia-plugin-peristedstate 插件会失效
|
* @description store不能放在外面,否则 pinia-plugin-peristedstate 插件会失效
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export function useAuthGuard(router: Router) {
|
export function useAuthGuard(router: Router) {
|
||||||
api.expireHandler = () => {
|
api.expireHandler = () => {
|
||||||
|
|
@ -39,17 +37,17 @@ export function useAuthGuard(router: Router) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 直接访问跳转回首页(非路由跳转)
|
||||||
|
if (!from.matched.length) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
// 提示已登陆
|
// 提示已登陆
|
||||||
Notification.warning({
|
Notification.warning({
|
||||||
title: '跳转提示',
|
title: '跳转提示',
|
||||||
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
content: `您已登陆,如需重新登陆请退出后再操作!`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 直接访问跳转回首页(不是从路由跳转)
|
|
||||||
if (!from.matched.length) {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 已登陆不允许
|
// 已登陆不允许
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -64,37 +62,24 @@ export function useAuthGuard(router: Router) {
|
||||||
|
|
||||||
// 未获取权限进行获取
|
// 未获取权限进行获取
|
||||||
if (!menuStore.menus.length) {
|
if (!menuStore.menus.length) {
|
||||||
// 菜单处理
|
menuStore.setMenus(menus);
|
||||||
const authMenus = treeFilter(menus, item => {
|
|
||||||
if (item.path === env.homePath) {
|
|
||||||
item.path = '/';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
menuStore.setMenus(authMenus);
|
|
||||||
menuStore.setHome(env.homePath);
|
menuStore.setHome(env.homePath);
|
||||||
|
|
||||||
// 路由处理
|
treeEach(appRoutes, item => {
|
||||||
for (const route of routes) {
|
|
||||||
router.addRoute(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存处理
|
|
||||||
treeEach(routes, (item, level) => {
|
|
||||||
const { cache, name } = item.meta ?? {};
|
const { cache, name } = item.meta ?? {};
|
||||||
if (cache && name) {
|
if (cache && name) {
|
||||||
menuStore.caches.push(name);
|
menuStore.caches.push(name);
|
||||||
}
|
}
|
||||||
|
// if (item.path === menuStore.home) {
|
||||||
|
// item.alias = '/';
|
||||||
|
// }
|
||||||
|
// if (!router.hasRoute(item.name!)) {
|
||||||
|
// const route = { ...item, children: undefined } as any;
|
||||||
|
// router.addRoute(route.parentName!, route);
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 首页处理
|
return to.fullPath;
|
||||||
const home = treeFind(routes, i => i.path === menuStore.home);
|
|
||||||
if (home) {
|
|
||||||
const route = { ...home, name: APP_HOME_NAME, alias: '/' };
|
|
||||||
router.removeRoute(home.name!);
|
|
||||||
router.addRoute(APP_ROUTE_NAME, route);
|
|
||||||
return router.replace(to.path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底处理
|
// 兜底处理
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { createRouter } from 'vue-router';
|
||||||
import { useAuthGuard } from '../guards/auth';
|
import { useAuthGuard } from '../guards/auth';
|
||||||
import { useProgressGard } from '../guards/progress';
|
import { useProgressGard } from '../guards/progress';
|
||||||
import { useTitleGuard } from '../guards/title';
|
import { useTitleGuard } from '../guards/title';
|
||||||
import { baseRoutes } from '../routes/base';
|
|
||||||
import { historyMode } from './util';
|
import { historyMode } from './util';
|
||||||
import { routes } from '../routes/page';
|
import { routes } from '../routes/page';
|
||||||
|
|
||||||
|
|
@ -11,7 +10,7 @@ import { routes } from '../routes/page';
|
||||||
*/
|
*/
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: historyMode(),
|
history: historyMode(),
|
||||||
routes: [...baseRoutes, ...routes],
|
routes: routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
|
||||||
|
|
||||||
export const APP_HOME_NAME = '__APP_HOME__';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基本路由
|
|
||||||
*/
|
|
||||||
export const baseRoutes: RouteRecordRaw[] = [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: APP_HOME_NAME,
|
|
||||||
component: () => 'Home Page',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
@ -15,8 +15,8 @@ function treeRoutes(list: RouteRecordRaw[]) {
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
const parentPath = item.path.split('/').slice(0, -1).join('/');
|
const parentPath = item.path.split('/').slice(0, -1).join('/');
|
||||||
const parent = map[parentPath];
|
const parent = map[parentPath];
|
||||||
|
item.parentName = (parent?.name as string) || APP_ROUTE_NAME;
|
||||||
if (parent) {
|
if (parent) {
|
||||||
(item as any).parentPath = parentPath;
|
|
||||||
(parent.children || (parent.children = [])).push(item);
|
(parent.children || (parent.children = [])).push(item);
|
||||||
} else {
|
} else {
|
||||||
tree.push(item);
|
tree.push(item);
|
||||||
|
|
@ -47,12 +47,14 @@ function sortRoutes(routes: RouteRecordRaw[]) {
|
||||||
const transformRoutes = (routes: RouteRecordRaw[]) => {
|
const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
const topRoutes: RouteRecordRaw[] = [];
|
const topRoutes: RouteRecordRaw[] = [];
|
||||||
const appRoutes: RouteRecordRaw[] = [];
|
const appRoutes: RouteRecordRaw[] = [];
|
||||||
|
let app: RouteRecordRaw;
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
|
if (route.name === APP_ROUTE_NAME) {
|
||||||
|
app = route;
|
||||||
|
route.children = appRoutes;
|
||||||
|
}
|
||||||
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
|
||||||
if (route.name === APP_ROUTE_NAME) {
|
|
||||||
route.children = appRoutes;
|
|
||||||
}
|
|
||||||
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
route.path = route.path.replace(TOP_ROUTE_PREF, '');
|
||||||
topRoutes.push(route);
|
topRoutes.push(route);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -60,7 +62,8 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
|
||||||
appRoutes.push(route);
|
appRoutes.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [topRoutes, sortRoutes(treeRoutes(appRoutes))];
|
app!.children = sortRoutes(treeRoutes(appRoutes));
|
||||||
|
return [topRoutes, app!.children];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const [routes, appRoutes] = transformRoutes(generatedRoutes);
|
export const [routes, appRoutes] = transformRoutes(generatedRoutes);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
id: "user",
|
id: 'user',
|
||||||
state: (): UserStore => {
|
state: (): UserStore => {
|
||||||
return {
|
return {
|
||||||
id: 0,
|
id: 0,
|
||||||
username: "juetan",
|
username: 'juetan',
|
||||||
nickname: "绝弹",
|
nickname: '绝弹',
|
||||||
avatar: "https://github.com/juetan.png",
|
avatar: 'https://github.com/juetan.png',
|
||||||
accessToken: "",
|
accessToken: '',
|
||||||
refreshToken: undefined,
|
refreshToken: undefined,
|
||||||
auth: []
|
auth: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|
@ -48,7 +48,10 @@ export const useUserStore = defineStore({
|
||||||
accessToken && (this.accessToken = accessToken);
|
accessToken && (this.accessToken = accessToken);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
persist: true,
|
persist: {
|
||||||
|
key: '__APP_USER__',
|
||||||
|
paths: ['accessToken'],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface UserStore {
|
export interface UserStore {
|
||||||
|
|
@ -65,11 +68,11 @@ export interface UserStore {
|
||||||
*/
|
*/
|
||||||
nickname: string;
|
nickname: string;
|
||||||
/**
|
/**
|
||||||
* 用户头像地址
|
* 头像地址
|
||||||
*/
|
*/
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
/**
|
/**
|
||||||
* JWT令牌
|
* 访问令牌
|
||||||
*/
|
*/
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,30 @@
|
||||||
import "vue-router";
|
import 'vue-router';
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
interface RouteRecordRaw {
|
||||||
|
parentName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouteRecordSingleViewWithChildren {
|
||||||
|
parentName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouteRecordSingleView {
|
||||||
|
parentName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouteRecordMultipleViews {
|
||||||
|
parentName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouteRecordMultipleViewsWithChildren {
|
||||||
|
parentName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RouteRecordRedirect {
|
||||||
|
parentName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
declare module "vue-router" {
|
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
/**
|
/**
|
||||||
* 页面标题
|
* 页面标题
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue