mirror of
https://github.com/Tencent/tdesign-vue-next-starter.git
synced 2024-12-22 22:16:32 +08:00
feat: add backend dynamic router permission code (#394)
* chore: 大致雏形 * feat: complete permission code * feat: 修复渲染问题并完善权限代码 * chore: release 0.7.0 * chore: 更新依赖 * chore: fix echarts version * feat: upgrade dependencies * chore: mock route map * chore: hash router * chore: concat fixed routers * chore: fix dependencies * feat: optimize * chore: remove roles from get menu process * chore: remain fe permssion code Co-authored-by: 悠静萝莉 <i@mikuhl.cn>
This commit is contained in:
parent
56189daa3b
commit
5b9a418600
170
mock/index.ts
170
mock/index.ts
|
@ -144,4 +144,174 @@ export default [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
url: '/api/get-menu-list',
|
||||
method: 'get',
|
||||
timeout: 2000,
|
||||
response: {
|
||||
code: 0,
|
||||
data: {
|
||||
...Mock.mock({
|
||||
list: [
|
||||
{
|
||||
path: '/list',
|
||||
name: 'list',
|
||||
component: 'LAYOUT',
|
||||
redirect: '/list/base',
|
||||
meta: {
|
||||
title: '列表页',
|
||||
icon: 'view-list',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'base',
|
||||
name: 'ListBase',
|
||||
component: '/list/base/index',
|
||||
meta: {
|
||||
title: '基础列表页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'card',
|
||||
name: 'ListCard',
|
||||
component: '/list/card/index',
|
||||
meta: {
|
||||
title: '卡片列表页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'filter',
|
||||
name: 'ListFilter',
|
||||
component: '/list/filter/index',
|
||||
meta: {
|
||||
title: '筛选列表页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tree',
|
||||
name: 'ListTree',
|
||||
component: '/list/tree/index',
|
||||
meta: {
|
||||
title: '树状筛选列表页',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/form',
|
||||
name: 'form',
|
||||
component: 'LAYOUT',
|
||||
redirect: '/form/base',
|
||||
meta: {
|
||||
title: '表单页',
|
||||
icon: 'edit-1',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'base',
|
||||
name: 'FormBase',
|
||||
component: '/form/base/index',
|
||||
meta: {
|
||||
title: '基础表单页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'step',
|
||||
name: 'FormStep',
|
||||
component: '/form/step/index',
|
||||
meta: {
|
||||
title: '分步表单页',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/detail',
|
||||
name: 'detail',
|
||||
component: 'LAYOUT',
|
||||
redirect: '/detail/base',
|
||||
meta: {
|
||||
title: '详情页',
|
||||
icon: 'layers',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'base',
|
||||
name: 'DetailBase',
|
||||
component: '/detail/base/index',
|
||||
meta: {
|
||||
title: '基础详情页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'advanced',
|
||||
name: 'DetailAdvanced',
|
||||
component: '/detail/advanced/index',
|
||||
meta: {
|
||||
title: '多卡片详情页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'deploy',
|
||||
name: 'DetailDeploy',
|
||||
component: '/detail/deploy/index',
|
||||
meta: {
|
||||
title: '数据详情页',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'secondary',
|
||||
name: 'DetailSecondary',
|
||||
component: '/detail/secondary/index',
|
||||
meta: {
|
||||
title: '二级详情页',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/frame',
|
||||
name: 'Frame',
|
||||
component: 'Layout',
|
||||
redirect: '/frame/doc',
|
||||
meta: {
|
||||
icon: 'internet',
|
||||
title: '外部页面',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'doc',
|
||||
name: 'Doc',
|
||||
component: 'IFrame',
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/starter/docs/vue-next/get-started',
|
||||
title: '使用文档(内嵌)',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'TDesign',
|
||||
name: 'TDesign',
|
||||
component: 'IFrame',
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/vue-next/getting-started',
|
||||
title: 'TDesign 文档(内嵌)',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'TDesign2',
|
||||
name: 'TDesign2',
|
||||
component: 'IFrame',
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/vue-next/getting-started',
|
||||
frameBlank: true,
|
||||
title: 'TDesign 文档(外链)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as MockMethod[];
|
||||
|
|
75
package.json
75
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tdesign-vue-next-starter",
|
||||
"version": "0.6.1",
|
||||
"version": "0.7.0",
|
||||
"scripts": {
|
||||
"dev:mock": "vite --open --mode mock",
|
||||
"dev": "vite --open --mode development",
|
||||
|
@ -19,58 +19,59 @@
|
|||
"test:coverage": "echo \"no test:coverage specified,work in process\""
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.1.3",
|
||||
"dayjs": "^1.10.6",
|
||||
"echarts": "~5.1.2",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"axios": "^1.2.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"echarts": "5.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.11",
|
||||
"pinia-plugin-persistedstate": "^3.0.1",
|
||||
"qrcode.vue": "^3.2.2",
|
||||
"qs": "^6.10.5",
|
||||
"pinia": "^2.0.28",
|
||||
"pinia-plugin-persistedstate": "^3.0.2",
|
||||
"qrcode.vue": "^3.3.3",
|
||||
"qs": "^6.11.0",
|
||||
"tdesign-icons-vue-next": "^0.1.7",
|
||||
"tdesign-vue-next": "^1.0.0",
|
||||
"tvision-color": "^1.5.0",
|
||||
"vue": "^3.2.31",
|
||||
"vue": "^3.2.45",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-router": "~4.1.5"
|
||||
"vue-router": "~4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.3",
|
||||
"@commitlint/config-conventional": "^17.0.3",
|
||||
"@types/echarts": "^4.9.10",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@commitlint/cli": "^17.3.0",
|
||||
"@commitlint/config-conventional": "^17.3.0",
|
||||
"@types/echarts": "^4.9.16",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/ws": "^8.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||
"@typescript-eslint/parser": "^4.29.3",
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||
"@typescript-eslint/parser": "^5.47.1",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vitejs/plugin-vue-jsx": "^1.1.7",
|
||||
"@vue/compiler-sfc": "^3.0.5",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/compiler-sfc": "^3.2.45",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"commitizen": "^4.2.4",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint": "^8.30.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^9.2.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"eslint-plugin-vue-scoped-css": "^2.2.0",
|
||||
"husky": "^7.0.4",
|
||||
"less": "^4.1.1",
|
||||
"lint-staged": "^12.1.2",
|
||||
"husky": "^8.0.2",
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.1.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"prettier": "^2.4.1",
|
||||
"stylelint": "~13.13.1",
|
||||
"stylelint-config-prettier": "~9.0.3",
|
||||
"stylelint-less": "1.0.1",
|
||||
"stylelint-order": "~4.1.0",
|
||||
"typescript": "~4.8.4",
|
||||
"vite": "^2.7.1",
|
||||
"prettier": "^2.8.1",
|
||||
"stylelint": "~14.9.1",
|
||||
"stylelint-config-prettier": "~9.0.4",
|
||||
"stylelint-less": "1.0.6",
|
||||
"stylelint-order": "~6.0.1",
|
||||
"typescript": "~4.9.4",
|
||||
"vite": "^3.0.3",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vite-svg-loader": "^3.1.0",
|
||||
"vue-tsc": "^1.0.8"
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
"vue-tsc": "^1.0.19"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
|
29
src/api/model/permissionModel.ts
Normal file
29
src/api/model/permissionModel.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { defineComponent } from 'vue';
|
||||
|
||||
export interface MenuListResult {
|
||||
list: Array<RouteItem>;
|
||||
}
|
||||
|
||||
export type Component<T = any> =
|
||||
| ReturnType<typeof defineComponent>
|
||||
| (() => Promise<typeof import('*.vue')>)
|
||||
| (() => Promise<T>);
|
||||
|
||||
export interface RouteItem {
|
||||
path: string;
|
||||
name: string;
|
||||
component?: Component | string;
|
||||
components?: Component;
|
||||
redirect?: string;
|
||||
meta: RouteMeta;
|
||||
children?: Array<RouteItem>;
|
||||
}
|
||||
export interface RouteMeta {
|
||||
title: string;
|
||||
icon?: string;
|
||||
expanded?: boolean;
|
||||
orderNo?: number;
|
||||
hidden?: boolean;
|
||||
hiddenBreadcrumb?: boolean;
|
||||
single?: boolean;
|
||||
}
|
12
src/api/permission.ts
Normal file
12
src/api/permission.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { request } from '@/utils/request';
|
||||
import type { MenuListResult } from '@/api/model/permissionModel';
|
||||
|
||||
const Api = {
|
||||
MenuList: '/get-menu-list',
|
||||
};
|
||||
|
||||
export function getMenuList() {
|
||||
return request.get<MenuListResult>({
|
||||
url: Api.MenuList,
|
||||
});
|
||||
}
|
|
@ -40,3 +40,10 @@ export const NOTIFICATION_TYPES = {
|
|||
middle: 'warning',
|
||||
high: 'danger',
|
||||
};
|
||||
|
||||
// 通用请求头
|
||||
export enum ContentTypeEnum {
|
||||
Json = 'application/json;charset=UTF-8',
|
||||
FormURLEncoded = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
FormData = 'multipart/form-data;charset=UTF-8',
|
||||
}
|
||||
|
|
|
@ -4,37 +4,34 @@
|
|||
<template v-if="!item.children || !item.children.length || item.meta?.single">
|
||||
<t-menu-item v-if="getHref(item)" :name="item.path" :value="getPath(item)" @click="openHref(getHref(item)[0])">
|
||||
<template #icon>
|
||||
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
||||
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
||||
<component :is="menuIcon(item)"></component>
|
||||
</template>
|
||||
{{ item.title }}
|
||||
</t-menu-item>
|
||||
<t-menu-item v-else :name="item.path" :value="getPath(item)" :to="item.path">
|
||||
<template #icon>
|
||||
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
||||
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
||||
<component :is="menuIcon(item)"></component>
|
||||
</template>
|
||||
{{ item.title }}
|
||||
</t-menu-item>
|
||||
</template>
|
||||
<t-submenu v-else :name="item.path" :value="item.path" :title="item.title">
|
||||
<template #icon>
|
||||
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
||||
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
||||
<component :is="menuIcon(item)"></component>
|
||||
</template>
|
||||
<menu-content v-if="item.children" :nav-data="item.children" />
|
||||
</t-submenu>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import { computed } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import isObject from 'lodash/isObject';
|
||||
import type { MenuRoute } from '@/types/interface';
|
||||
import { getActive } from '@/router';
|
||||
|
||||
type ListItemType = MenuRoute & { icon?: string };
|
||||
|
||||
const props = defineProps({
|
||||
navData: {
|
||||
type: Array as PropType<MenuRoute[]>,
|
||||
|
@ -43,12 +40,17 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
const active = computed(() => getActive());
|
||||
|
||||
const list = computed(() => {
|
||||
const { navData } = props;
|
||||
return getMenuList(navData);
|
||||
});
|
||||
|
||||
type ListItemType = MenuRoute & { icon?: string };
|
||||
const menuIcon = (item: ListItemType) => {
|
||||
if (typeof item.icon === 'string') return <t-icon name={item.icon} />;
|
||||
const RenderIcon = item.icon;
|
||||
return RenderIcon;
|
||||
};
|
||||
|
||||
const getMenuList = (list: MenuRoute[], basePath?: string): ListItemType[] => {
|
||||
if (!list) {
|
||||
|
@ -61,10 +63,11 @@ const getMenuList = (list: MenuRoute[], basePath?: string): ListItemType[] => {
|
|||
return list
|
||||
.map((item) => {
|
||||
const path = basePath && !item.path.includes(basePath) ? `${basePath}/${item.path}` : item.path;
|
||||
|
||||
return {
|
||||
path,
|
||||
title: item.meta?.title,
|
||||
icon: item.meta?.icon || '',
|
||||
icon: item.meta?.icon,
|
||||
children: getMenuList(item.children, path),
|
||||
meta: item.meta,
|
||||
redirect: item.redirect,
|
||||
|
@ -81,33 +84,14 @@ const getHref = (item: MenuRoute) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const getPath = (item) => {
|
||||
const getPath = (item: ListItemType) => {
|
||||
if (active.value.startsWith(item.path)) {
|
||||
return active.value;
|
||||
}
|
||||
return item.meta?.single ? item.redirect : item.path;
|
||||
};
|
||||
|
||||
const beIcon = (item: MenuRoute) => {
|
||||
return item.icon && typeof item.icon === 'string';
|
||||
};
|
||||
|
||||
const beRender = (item: MenuRoute) => {
|
||||
if (isObject(item.icon) && typeof item.icon.render === 'function') {
|
||||
return {
|
||||
can: true,
|
||||
render: item.icon.render,
|
||||
};
|
||||
}
|
||||
return {
|
||||
can: false,
|
||||
render: null,
|
||||
};
|
||||
};
|
||||
|
||||
const openHref = (url: string) => {
|
||||
window.open(url);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { createApp } from 'vue';
|
|||
|
||||
import TDesign from 'tdesign-vue-next';
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
|
||||
import { store } from './store';
|
||||
import router from './router';
|
||||
import '@/style/index.less';
|
||||
|
|
|
@ -2,18 +2,20 @@ import { MessagePlugin } from 'tdesign-vue-next';
|
|||
import NProgress from 'nprogress'; // progress bar
|
||||
import 'nprogress/nprogress.css'; // progress bar style
|
||||
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { getPermissionStore, getUserStore } from '@/store';
|
||||
import router from '@/router';
|
||||
import { PAGE_NOT_FOUND_ROUTE } from '@/utils/route/constant';
|
||||
|
||||
NProgress.configure({ showSpinner: false });
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start();
|
||||
|
||||
const userStore = getUserStore();
|
||||
const permissionStore = getPermissionStore();
|
||||
const { whiteListRouters } = permissionStore;
|
||||
|
||||
const userStore = getUserStore();
|
||||
const { token } = userStore;
|
||||
if (token) {
|
||||
if (to.path === '/login') {
|
||||
|
@ -21,32 +23,40 @@ router.beforeEach(async (to, from, next) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { roles } = userStore;
|
||||
const { asyncRoutes } = permissionStore;
|
||||
|
||||
if (roles && roles.length > 0) {
|
||||
next();
|
||||
} else {
|
||||
try {
|
||||
await userStore.getUserInfo();
|
||||
if (asyncRoutes && asyncRoutes.length === 0) {
|
||||
const routeList = await permissionStore.buildAsyncRoutes();
|
||||
routeList.forEach((item: RouteRecordRaw) => {
|
||||
router.addRoute(item);
|
||||
});
|
||||
|
||||
const { roles } = userStore;
|
||||
|
||||
await permissionStore.initRoutes(roles);
|
||||
|
||||
if (router.hasRoute(to.name)) {
|
||||
next();
|
||||
} else {
|
||||
next(`/`);
|
||||
}
|
||||
} catch (error) {
|
||||
MessagePlugin.error(error);
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
});
|
||||
NProgress.done();
|
||||
if (to.name === PAGE_NOT_FOUND_ROUTE.name) {
|
||||
// 动态添加路由后,此处应当重定向到fullPath,否则会加载404页面内容
|
||||
next({ path: to.fullPath, replace: true, query: to.query });
|
||||
} else {
|
||||
const redirect = decodeURIComponent((from.query.redirect || to.path) as string);
|
||||
next(to.path === redirect ? { ...to, replace: true } : { path: redirect });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await userStore.getUserInfo();
|
||||
|
||||
if (router.hasRoute(to.name)) {
|
||||
next();
|
||||
} else {
|
||||
next(`/`);
|
||||
}
|
||||
} catch (error) {
|
||||
MessagePlugin.error(error);
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
});
|
||||
NProgress.done();
|
||||
}
|
||||
} else {
|
||||
/* white list router */
|
||||
if (whiteListRouters.indexOf(to.path) !== -1) {
|
||||
|
@ -67,7 +77,7 @@ router.afterEach((to) => {
|
|||
const permissionStore = getPermissionStore();
|
||||
|
||||
userStore.logout();
|
||||
permissionStore.restore();
|
||||
permissionStore.restoreRoutes();
|
||||
}
|
||||
NProgress.done();
|
||||
});
|
||||
|
|
|
@ -1,27 +1,15 @@
|
|||
import { useRoute, createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||
import { useRoute, createRouter, RouteRecordRaw, createWebHistory } from 'vue-router';
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
const env = import.meta.env.MODE || 'development';
|
||||
|
||||
// 自动导入modules文件夹下所有ts文件
|
||||
const modules = import.meta.globEager('./modules/**/*.ts');
|
||||
// 导入homepage相关固定路由
|
||||
const homepageModules = import.meta.globEager('./modules/**/homepage.ts');
|
||||
|
||||
// 路由暂存
|
||||
const routeModuleList: Array<RouteRecordRaw> = [];
|
||||
// 导入modules非homepage相关固定路由
|
||||
const fixedModules = import.meta.globEager('./modules/**/!(homepage).ts');
|
||||
|
||||
Object.keys(modules).forEach((key) => {
|
||||
// @ts-ignore
|
||||
const mod = modules[key].default || {};
|
||||
const modList = Array.isArray(mod) ? [...mod] : [mod];
|
||||
routeModuleList.push(...modList);
|
||||
});
|
||||
|
||||
// 关于单层路由,meta 中设置 { single: true } 即可为单层路由,{ hidden: true } 即可在侧边栏隐藏该路由
|
||||
|
||||
// 存放动态路由
|
||||
export const asyncRouterList: Array<RouteRecordRaw> = [...routeModuleList];
|
||||
|
||||
// 存放固定的路由
|
||||
// 其他固定路由
|
||||
const defaultRouterList: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/login',
|
||||
|
@ -32,19 +20,29 @@ const defaultRouterList: Array<RouteRecordRaw> = [
|
|||
path: '/',
|
||||
redirect: '/dashboard/base',
|
||||
},
|
||||
{
|
||||
path: '/:w+',
|
||||
name: '404Page',
|
||||
redirect: '/result/404',
|
||||
},
|
||||
];
|
||||
// 存放固定路由
|
||||
export const homepageRouterList: Array<RouteRecordRaw> = mapModuleRouterList(homepageModules);
|
||||
export const fixedRouterList: Array<RouteRecordRaw> = mapModuleRouterList(fixedModules);
|
||||
|
||||
export const allRoutes = [...defaultRouterList, ...asyncRouterList];
|
||||
export const allRoutes = [...homepageRouterList, ...fixedRouterList, ...defaultRouterList];
|
||||
|
||||
// 固定路由模块转换为路由
|
||||
export function mapModuleRouterList(modules: Record<string, unknown>): Array<RouteRecordRaw> {
|
||||
const routerList: Array<RouteRecordRaw> = [];
|
||||
Object.keys(modules).forEach((key) => {
|
||||
// @ts-ignore
|
||||
const mod = modules[key].default || {};
|
||||
const modList = Array.isArray(mod) ? [...mod] : [mod];
|
||||
routerList.push(...modList);
|
||||
});
|
||||
return routerList;
|
||||
}
|
||||
|
||||
export const getRoutesExpanded = () => {
|
||||
const expandedRoutes = [];
|
||||
|
||||
allRoutes.forEach((item) => {
|
||||
fixedRouterList.forEach((item) => {
|
||||
if (item.meta && item.meta.expanded) {
|
||||
expandedRoutes.push(item.path);
|
||||
}
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
import Layout from '@/layouts/index.vue';
|
||||
import ListIcon from '@/assets/assets-slide-list.svg';
|
||||
import FormIcon from '@/assets/assets-slide-form.svg';
|
||||
import DetailIcon from '@/assets/assets-slide-detail.svg';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/list',
|
||||
name: 'list',
|
||||
component: Layout,
|
||||
redirect: '/list/base',
|
||||
meta: { title: '列表页', icon: ListIcon },
|
||||
children: [
|
||||
{
|
||||
path: 'base',
|
||||
name: 'ListBase',
|
||||
component: () => import('@/pages/list/base/index.vue'),
|
||||
meta: { title: '基础列表页' },
|
||||
},
|
||||
{
|
||||
path: 'card',
|
||||
name: 'ListCard',
|
||||
component: () => import('@/pages/list/card/index.vue'),
|
||||
meta: { title: '卡片列表页' },
|
||||
},
|
||||
{
|
||||
path: 'filter',
|
||||
name: 'ListFilter',
|
||||
component: () => import('@/pages/list/filter/index.vue'),
|
||||
meta: { title: '筛选列表页' },
|
||||
},
|
||||
{
|
||||
path: 'tree',
|
||||
name: 'ListTree',
|
||||
component: () => import('@/pages/list/tree/index.vue'),
|
||||
meta: { title: '树状筛选列表页' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/form',
|
||||
name: 'form',
|
||||
component: Layout,
|
||||
redirect: '/form/base',
|
||||
meta: { title: '表单页', icon: FormIcon },
|
||||
children: [
|
||||
{
|
||||
path: 'base',
|
||||
name: 'FormBase',
|
||||
component: () => import('@/pages/form/base/index.vue'),
|
||||
meta: { title: '基础表单页' },
|
||||
},
|
||||
{
|
||||
path: 'step',
|
||||
name: 'FormStep',
|
||||
component: () => import('@/pages/form/step/index.vue'),
|
||||
meta: { title: '分步表单页' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/detail',
|
||||
name: 'detail',
|
||||
component: Layout,
|
||||
redirect: '/detail/base',
|
||||
meta: { title: '详情页', icon: DetailIcon },
|
||||
children: [
|
||||
{
|
||||
path: 'base',
|
||||
name: 'DetailBase',
|
||||
component: () => import('@/pages/detail/base/index.vue'),
|
||||
meta: { title: '基础详情页' },
|
||||
},
|
||||
{
|
||||
path: 'advanced',
|
||||
name: 'DetailAdvanced',
|
||||
component: () => import('@/pages/detail/advanced/index.vue'),
|
||||
meta: { title: '多卡片详情页' },
|
||||
},
|
||||
{
|
||||
path: 'deploy',
|
||||
name: 'DetailDeploy',
|
||||
component: () => import('@/pages/detail/deploy/index.vue'),
|
||||
meta: { title: '数据详情页' },
|
||||
},
|
||||
{
|
||||
path: 'secondary',
|
||||
name: 'DetailSecondary',
|
||||
component: () => import('@/pages/detail/secondary/index.vue'),
|
||||
meta: { title: '二级详情页' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/result',
|
||||
name: 'result',
|
||||
component: Layout,
|
||||
redirect: '/result/success',
|
||||
meta: { title: '结果页', icon: 'check-circle' },
|
||||
children: [
|
||||
{
|
||||
path: 'success',
|
||||
name: 'ResultSuccess',
|
||||
component: () => import('@/pages/result/success/index.vue'),
|
||||
meta: { title: '成功页' },
|
||||
},
|
||||
{
|
||||
path: 'fail',
|
||||
name: 'ResultFail',
|
||||
component: () => import('@/pages/result/fail/index.vue'),
|
||||
meta: { title: '失败页' },
|
||||
},
|
||||
{
|
||||
path: 'network-error',
|
||||
name: 'ResultNetworkError',
|
||||
component: () => import('@/pages/result/network-error/index.vue'),
|
||||
meta: { title: '网络异常' },
|
||||
},
|
||||
{
|
||||
path: '403',
|
||||
name: 'Result403',
|
||||
component: () => import('@/pages/result/403/index.vue'),
|
||||
meta: { title: '无权限' },
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
name: 'Result404',
|
||||
component: () => import('@/pages/result/404/index.vue'),
|
||||
meta: { title: '访问页面不存在页' },
|
||||
},
|
||||
{
|
||||
path: '500',
|
||||
name: 'Result500',
|
||||
component: () => import('@/pages/result/500/index.vue'),
|
||||
meta: { title: '服务器出错页' },
|
||||
},
|
||||
{
|
||||
path: 'browser-incompatible',
|
||||
name: 'ResultBrowserIncompatible',
|
||||
component: () => import('@/pages/result/browser-incompatible/index.vue'),
|
||||
meta: { title: '浏览器不兼容页' },
|
||||
},
|
||||
{
|
||||
path: 'maintenance',
|
||||
name: 'ResultMaintenance',
|
||||
component: () => import('@/pages/result/maintenance/index.vue'),
|
||||
meta: { title: '系统维护页' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -1,5 +1,6 @@
|
|||
import { DashboardIcon } from 'tdesign-icons-vue-next';
|
||||
import { shallowRef } from 'vue';
|
||||
import Layout from '@/layouts/index.vue';
|
||||
import DashboardIcon from '@/assets/assets-slide-dashboard.svg';
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -7,19 +8,27 @@ export default [
|
|||
component: Layout,
|
||||
redirect: '/dashboard/base',
|
||||
name: 'dashboard',
|
||||
meta: { title: '仪表盘', icon: DashboardIcon },
|
||||
meta: {
|
||||
title: '仪表盘',
|
||||
icon: shallowRef(DashboardIcon),
|
||||
orderNo: 0,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'base',
|
||||
name: 'DashboardBase',
|
||||
component: () => import('@/pages/dashboard/base/index.vue'),
|
||||
meta: { title: '概览仪表盘' },
|
||||
meta: {
|
||||
title: '概览仪表盘',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'detail',
|
||||
name: 'DashboardDetail',
|
||||
component: () => import('@/pages/dashboard/detail/index.vue'),
|
||||
meta: { title: '统计报表' },
|
||||
meta: {
|
||||
title: '统计报表',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
|
@ -1,47 +0,0 @@
|
|||
import Layout from '@/layouts/index.vue';
|
||||
|
||||
const IFrame = () => import('@/layouts/components/FrameBlank.vue');
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/frame',
|
||||
name: 'Frame',
|
||||
component: Layout,
|
||||
redirect: '/frame/doc',
|
||||
meta: {
|
||||
icon: 'internet',
|
||||
title: '外部页面',
|
||||
},
|
||||
|
||||
children: [
|
||||
{
|
||||
path: 'doc',
|
||||
name: 'Doc',
|
||||
component: IFrame,
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/starter/docs/vue-next/get-started',
|
||||
title: '使用文档(内嵌)',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'TDesign',
|
||||
name: 'TDesign',
|
||||
component: IFrame,
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/vue-next/getting-started',
|
||||
title: 'TDesign 文档(内嵌)',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'TDesign2',
|
||||
name: 'TDesign2',
|
||||
component: IFrame,
|
||||
meta: {
|
||||
frameSrc: 'https://tdesign.tencent.com/vue-next/getting-started',
|
||||
frameBlank: true,
|
||||
title: 'TDesign 文档(外链)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
61
src/router/modules/result.ts
Normal file
61
src/router/modules/result.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import Layout from '@/layouts/index.vue';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/result',
|
||||
name: 'result',
|
||||
component: Layout,
|
||||
redirect: '/result/success',
|
||||
meta: { title: '结果页', icon: 'check-circle' },
|
||||
children: [
|
||||
{
|
||||
path: 'success',
|
||||
name: 'ResultSuccess',
|
||||
component: () => import('@/pages/result/success/index.vue'),
|
||||
meta: { title: '成功页' },
|
||||
},
|
||||
{
|
||||
path: 'fail',
|
||||
name: 'ResultFail',
|
||||
component: () => import('@/pages/result/fail/index.vue'),
|
||||
meta: { title: '失败页' },
|
||||
},
|
||||
{
|
||||
path: 'network-error',
|
||||
name: 'ResultNetworkError',
|
||||
component: () => import('@/pages/result/network-error/index.vue'),
|
||||
meta: { title: '网络异常' },
|
||||
},
|
||||
{
|
||||
path: '403',
|
||||
name: 'Result403',
|
||||
component: () => import('@/pages/result/403/index.vue'),
|
||||
meta: { title: '无权限' },
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
name: 'Result404',
|
||||
component: () => import('@/pages/result/404/index.vue'),
|
||||
meta: { title: '访问页面不存在页' },
|
||||
},
|
||||
{
|
||||
path: '500',
|
||||
name: 'Result500',
|
||||
component: () => import('@/pages/result/500/index.vue'),
|
||||
meta: { title: '服务器出错页' },
|
||||
},
|
||||
{
|
||||
path: 'browser-incompatible',
|
||||
name: 'ResultBrowserIncompatible',
|
||||
component: () => import('@/pages/result/browser-incompatible/index.vue'),
|
||||
meta: { title: '浏览器不兼容页' },
|
||||
},
|
||||
{
|
||||
path: 'maintenance',
|
||||
name: 'ResultMaintenance',
|
||||
component: () => import('@/pages/result/maintenance/index.vue'),
|
||||
meta: { title: '系统维护页' },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -1,5 +1,6 @@
|
|||
import { LogoutIcon } from 'tdesign-icons-vue-next';
|
||||
import { shallowRef } from 'vue';
|
||||
import Layout from '@/layouts/index.vue';
|
||||
import LogoutIcon from '@/assets/assets-slide-logout.svg';
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -21,7 +22,7 @@ export default [
|
|||
path: '/loginRedirect',
|
||||
name: 'loginRedirect',
|
||||
redirect: '/login',
|
||||
meta: { title: '登录页', icon: LogoutIcon },
|
||||
meta: { title: '登录页', icon: shallowRef(LogoutIcon) },
|
||||
component: () => import('@/layouts/blank.vue'),
|
||||
children: [
|
||||
{
|
69
src/store/modules/permission-fe.ts
Normal file
69
src/store/modules/permission-fe.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
// 前端 roles 控制菜单权限 通过登录后的角色对菜单就行过滤处理
|
||||
// 如果需要前端 roles 控制菜单权限 请使用此文件代码替换 permission.ts 的内容
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import router, { allRoutes } from '@/router';
|
||||
import { store } from '@/store';
|
||||
|
||||
function filterPermissionsRouters(routes: Array<RouteRecordRaw>, roles: Array<unknown>) {
|
||||
const res = [];
|
||||
const removeRoutes = [];
|
||||
routes.forEach((route) => {
|
||||
const children = [];
|
||||
route.children?.forEach((childRouter) => {
|
||||
const roleCode = childRouter.meta?.roleCode || childRouter.name;
|
||||
if (roles.indexOf(roleCode) !== -1) {
|
||||
children.push(childRouter);
|
||||
} else {
|
||||
removeRoutes.push(childRouter);
|
||||
}
|
||||
});
|
||||
if (children.length > 0) {
|
||||
route.children = children;
|
||||
res.push(route);
|
||||
}
|
||||
});
|
||||
return { accessedRouters: res, removeRoutes };
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state: () => ({
|
||||
whiteListRouters: ['/login'],
|
||||
routers: [],
|
||||
removeRoutes: [],
|
||||
}),
|
||||
actions: {
|
||||
async initRoutes(roles: Array<unknown>) {
|
||||
let accessedRouters = [];
|
||||
|
||||
let removeRoutes = [];
|
||||
// special token
|
||||
if (roles.includes('all')) {
|
||||
accessedRouters = allRoutes;
|
||||
} else {
|
||||
const res = filterPermissionsRouters(allRoutes, roles);
|
||||
accessedRouters = res.accessedRouters;
|
||||
removeRoutes = res.removeRoutes;
|
||||
}
|
||||
|
||||
this.routers = accessedRouters;
|
||||
this.removeRoutes = removeRoutes;
|
||||
|
||||
removeRoutes.forEach((item: RouteRecordRaw) => {
|
||||
if (router.hasRoute(item.name)) {
|
||||
router.removeRoute(item.name);
|
||||
}
|
||||
});
|
||||
},
|
||||
async restore() {
|
||||
this.removeRoutes.forEach((item: RouteRecordRaw) => {
|
||||
router.addRoute(item);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function getPermissionStore() {
|
||||
return usePermissionStore(store);
|
||||
}
|
|
@ -1,59 +1,41 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import router, { asyncRouterList } from '@/router';
|
||||
import router, { fixedRouterList, homepageRouterList } from '@/router';
|
||||
import { store } from '@/store';
|
||||
|
||||
function filterPermissionsRouters(routes: Array<RouteRecordRaw>, roles: Array<unknown>) {
|
||||
const res = [];
|
||||
const removeRoutes = [];
|
||||
routes.forEach((route) => {
|
||||
const children = [];
|
||||
route.children?.forEach((childRouter) => {
|
||||
const roleCode = childRouter.meta?.roleCode || childRouter.name;
|
||||
if (roles.indexOf(roleCode) !== -1) {
|
||||
children.push(childRouter);
|
||||
} else {
|
||||
removeRoutes.push(childRouter);
|
||||
}
|
||||
});
|
||||
if (children.length > 0) {
|
||||
route.children = children;
|
||||
res.push(route);
|
||||
}
|
||||
});
|
||||
return { accessedRouters: res, removeRoutes };
|
||||
}
|
||||
import { RouteItem } from '@/api/model/permissionModel';
|
||||
import { getMenuList } from '@/api/permission';
|
||||
import { transformObjectToRoute } from '@/utils/route';
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state: () => ({
|
||||
whiteListRouters: ['/login'],
|
||||
routers: [],
|
||||
removeRoutes: [],
|
||||
asyncRoutes: [],
|
||||
}),
|
||||
actions: {
|
||||
async initRoutes(roles: Array<unknown>) {
|
||||
let accessedRouters = [];
|
||||
async initRoutes() {
|
||||
const accessedRouters = this.asyncRoutes;
|
||||
|
||||
let removeRoutes = [];
|
||||
// special token
|
||||
if (roles.includes('all')) {
|
||||
accessedRouters = asyncRouterList;
|
||||
} else {
|
||||
const res = filterPermissionsRouters(asyncRouterList, roles);
|
||||
accessedRouters = res.accessedRouters;
|
||||
removeRoutes = res.removeRoutes;
|
||||
}
|
||||
|
||||
this.routers = accessedRouters;
|
||||
this.removeRoutes = removeRoutes;
|
||||
|
||||
removeRoutes.forEach((item: RouteRecordRaw) => {
|
||||
if (router.hasRoute(item.name)) {
|
||||
router.removeRoute(item.name);
|
||||
}
|
||||
});
|
||||
// 在菜单展示全部路由
|
||||
this.routers = [...homepageRouterList, ...accessedRouters, ...fixedRouterList];
|
||||
// 在菜单只展示动态路由和首页
|
||||
// this.routers = [...homepageRouterList, ...accessedRouters];
|
||||
// 在菜单只展示动态路由
|
||||
// this.routers = [...accessedRouters];
|
||||
},
|
||||
async restore() {
|
||||
async buildAsyncRoutes() {
|
||||
try {
|
||||
// 发起菜单权限请求 获取菜单列表
|
||||
const asyncRoutes: Array<RouteItem> = (await getMenuList()).list;
|
||||
this.asyncRoutes = transformObjectToRoute(asyncRoutes);
|
||||
await this.initRoutes();
|
||||
return this.asyncRoutes;
|
||||
} catch (error) {
|
||||
throw new Error("Can't build routes");
|
||||
}
|
||||
},
|
||||
async restoreRoutes() {
|
||||
this.removeRoutes.forEach((item: RouteRecordRaw) => {
|
||||
router.addRoute(item);
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TOKEN_NAME } from '@/config/global';
|
|||
import { store, usePermissionStore } from '@/store';
|
||||
|
||||
const InitUserInfo = {
|
||||
roles: [],
|
||||
roles: [], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
|
||||
};
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
|
@ -20,7 +20,7 @@ export const useUserStore = defineStore('user', {
|
|||
async login(userInfo: Record<string, unknown>) {
|
||||
const mockLogin = async (userInfo: Record<string, unknown>) => {
|
||||
// 登录请求流程
|
||||
console.log(userInfo);
|
||||
console.log(`用户信息:`, userInfo);
|
||||
// const { account, password } = userInfo;
|
||||
// if (account !== 'td') {
|
||||
// return {
|
||||
|
@ -57,15 +57,14 @@ export const useUserStore = defineStore('user', {
|
|||
if (token === 'main_token') {
|
||||
return {
|
||||
name: 'td_main',
|
||||
roles: ['all'],
|
||||
roles: ['all'], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'td_dev',
|
||||
roles: ['UserIndex', 'DashboardBase', 'login'],
|
||||
roles: ['UserIndex', 'DashboardBase', 'login'], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
|
||||
};
|
||||
};
|
||||
|
||||
const res = await mockRemoteUserInfo(this.token);
|
||||
|
||||
this.userInfo = res;
|
||||
|
@ -80,11 +79,9 @@ export const useUserStore = defineStore('user', {
|
|||
},
|
||||
},
|
||||
persist: {
|
||||
afterRestore: (ctx) => {
|
||||
if (ctx.store.roles && ctx.store.roles.length > 0) {
|
||||
const permissionStore = usePermissionStore();
|
||||
permissionStore.initRoutes(ctx.store.roles);
|
||||
}
|
||||
afterRestore: () => {
|
||||
const permissionStore = usePermissionStore();
|
||||
permissionStore.initRoutes();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
|||
import { CreateAxiosOptions } from './AxiosTransform';
|
||||
import { AxiosCanceler } from './AxiosCancel';
|
||||
import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
|
||||
import { ContentTypeEnum } from '@/constants';
|
||||
|
||||
// Axios模块
|
||||
export class VAxios {
|
||||
|
@ -64,12 +65,15 @@ export class VAxios {
|
|||
|
||||
// 请求配置处理
|
||||
this.instance.interceptors.request.use((config: AxiosRequestConfig) => {
|
||||
const ignoreRepeat = this.options.requestOptions?.ignoreRepeatRequest;
|
||||
// @ts-ignore
|
||||
const { ignoreRepeatRequest } = config.requestOptions;
|
||||
const ignoreRepeat = ignoreRepeatRequest ?? this.options.requestOptions?.ignoreRepeatRequest;
|
||||
if (!ignoreRepeat) axiosCanceler.addPending(config);
|
||||
|
||||
if (requestInterceptors && isFunction(requestInterceptors)) {
|
||||
config = requestInterceptors(config, this.options);
|
||||
}
|
||||
|
||||
return config;
|
||||
}, undefined);
|
||||
|
||||
|
@ -99,7 +103,7 @@ export class VAxios {
|
|||
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
|
||||
|
||||
if (
|
||||
contentType !== 'application/x-www-form-urlencoded;charset=UTF-8' ||
|
||||
contentType !== ContentTypeEnum.FormURLEncoded ||
|
||||
!Reflect.has(config, 'data') ||
|
||||
config.method?.toUpperCase() === 'GET'
|
||||
) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { VAxios } from './Axios';
|
|||
import proxy from '@/config/proxy';
|
||||
import { joinTimestamp, formatRequestDate, setObjToUrlParams } from './utils';
|
||||
import { TOKEN_NAME } from '@/config/global';
|
||||
import { ContentTypeEnum } from '@/constants';
|
||||
|
||||
const env = import.meta.env.MODE || 'development';
|
||||
|
||||
|
@ -141,7 +142,7 @@ const transform: AxiosTransform = {
|
|||
resolve(config);
|
||||
}, config.requestOptions.retry.delay || 1);
|
||||
});
|
||||
config.headers = { ...config.headers, 'Content-Type': 'application/json;charset=UTF-8' };
|
||||
config.headers = { ...config.headers, 'Content-Type': ContentTypeEnum.Json };
|
||||
return backoff.then((config) => request.request(config));
|
||||
},
|
||||
};
|
||||
|
@ -158,7 +159,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
|
|||
// 携带Cookie
|
||||
withCredentials: true,
|
||||
// 头信息
|
||||
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
||||
headers: { 'Content-Type': ContentTypeEnum.Json },
|
||||
// 数据处理方式
|
||||
transform,
|
||||
// 配置项,下面的选项都可以在独立的接口请求中覆盖
|
||||
|
|
14
src/utils/route/constant.ts
Normal file
14
src/utils/route/constant.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const LAYOUT = () => import('@/layouts/index.vue');
|
||||
export const BLANK_LAYOUT = () => import('@/layouts/blank.vue');
|
||||
export const IFRAME = () => import('@/layouts/components/FrameBlank.vue');
|
||||
export const EXCEPTION_COMPONENT = () => import('@/pages/result/500/index.vue');
|
||||
export const PARENT_LAYOUT = () =>
|
||||
new Promise((resolve) => {
|
||||
resolve({ name: 'ParentLayout' });
|
||||
});
|
||||
|
||||
export const PAGE_NOT_FOUND_ROUTE = {
|
||||
path: '/:w+',
|
||||
name: '404Page',
|
||||
redirect: '/result/404',
|
||||
};
|
107
src/utils/route/index.ts
Normal file
107
src/utils/route/index.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { shallowRef } from 'vue';
|
||||
import { RouteItem, RouteMeta } from '@/api/model/permissionModel';
|
||||
import {
|
||||
BLANK_LAYOUT,
|
||||
LAYOUT,
|
||||
IFRAME,
|
||||
EXCEPTION_COMPONENT,
|
||||
PARENT_LAYOUT,
|
||||
PAGE_NOT_FOUND_ROUTE,
|
||||
} from '@/utils/route/constant';
|
||||
|
||||
// vite 3+ support dynamic import from node_modules
|
||||
const iconsPath = import.meta.glob('../../../node_modules/tdesign-icons-vue-next/esm/components/*.js');
|
||||
|
||||
const LayoutMap = new Map<string, () => Promise<typeof import('*.vue')>>();
|
||||
|
||||
LayoutMap.set('LAYOUT', LAYOUT);
|
||||
LayoutMap.set('BLANK', BLANK_LAYOUT);
|
||||
LayoutMap.set('IFRAME', IFRAME);
|
||||
|
||||
let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
|
||||
|
||||
// 动态从包内引入单个Icon
|
||||
async function getMenuIcon(iconName: string) {
|
||||
const RenderIcon = iconsPath[`../../../node_modules/tdesign-icons-vue-next/esm/components/${iconName}.js`];
|
||||
|
||||
const Icon = await RenderIcon();
|
||||
// @ts-ignore
|
||||
return shallowRef(Icon.default);
|
||||
}
|
||||
|
||||
// 动态引入路由组件
|
||||
function asyncImportRoute(routes: RouteItem[] | undefined) {
|
||||
dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../pages/**/*.vue');
|
||||
if (!routes) return;
|
||||
|
||||
routes.forEach(async (item) => {
|
||||
const { component, name } = item;
|
||||
const { children } = item;
|
||||
|
||||
if (component) {
|
||||
const layoutFound = LayoutMap.get(component.toUpperCase());
|
||||
if (layoutFound) {
|
||||
item.component = layoutFound;
|
||||
} else {
|
||||
item.component = dynamicImport(dynamicViewsModules, component);
|
||||
}
|
||||
} else if (name) {
|
||||
item.component = PARENT_LAYOUT();
|
||||
}
|
||||
if (item.meta.icon) item.meta.icon = await getMenuIcon(item.meta.icon);
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
children && asyncImportRoute(children);
|
||||
});
|
||||
}
|
||||
|
||||
function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recordable>>, component: string) {
|
||||
const keys = Object.keys(dynamicViewsModules);
|
||||
const matchKeys = keys.filter((key) => {
|
||||
const k = key.replace('../../pages', '');
|
||||
const startFlag = component.startsWith('/');
|
||||
const endFlag = component.endsWith('.vue') || component.endsWith('.tsx');
|
||||
const startIndex = startFlag ? 0 : 1;
|
||||
const lastIndex = endFlag ? k.length : k.lastIndexOf('.');
|
||||
return k.substring(startIndex, lastIndex) === component;
|
||||
});
|
||||
if (matchKeys?.length === 1) {
|
||||
const matchKey = matchKeys[0];
|
||||
return dynamicViewsModules[matchKey];
|
||||
}
|
||||
if (matchKeys?.length > 1) {
|
||||
throw new Error(
|
||||
'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure',
|
||||
);
|
||||
} else {
|
||||
console.warn(`Can't find ${component} in pages folder`);
|
||||
}
|
||||
return EXCEPTION_COMPONENT;
|
||||
}
|
||||
|
||||
// 将背景对象变成路由对象
|
||||
export function transformObjectToRoute<T = RouteItem>(routeList: RouteItem[]): T[] {
|
||||
routeList.forEach(async (route) => {
|
||||
const component = route.component as string;
|
||||
|
||||
if (component) {
|
||||
if (component.toUpperCase() === 'LAYOUT') {
|
||||
route.component = LayoutMap.get(component.toUpperCase());
|
||||
} else {
|
||||
route.children = [cloneDeep(route)];
|
||||
route.component = LAYOUT;
|
||||
route.name = `${route.name}Parent`;
|
||||
route.path = '';
|
||||
route.meta = (route.meta || {}) as RouteMeta;
|
||||
}
|
||||
} else {
|
||||
throw new Error('component is undefined');
|
||||
}
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
route.children && asyncImportRoute(route.children);
|
||||
if (route.meta.icon) route.meta.icon = await getMenuIcon(route.meta.icon);
|
||||
});
|
||||
|
||||
return [PAGE_NOT_FOUND_ROUTE, ...routeList] as unknown as T[];
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { ConfigEnv, UserConfig, loadEnv } from 'vite';
|
||||
import { viteMockServe } from 'vite-plugin-mock';
|
||||
import createVuePlugin from '@vitejs/plugin-vue';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import svgLoader from 'vite-svg-loader';
|
||||
|
||||
|
@ -32,7 +32,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
|||
},
|
||||
|
||||
plugins: [
|
||||
createVuePlugin(),
|
||||
vue(),
|
||||
vueJsx(),
|
||||
viteMockServe({
|
||||
mockPath: 'mock',
|
||||
|
|
Loading…
Reference in New Issue
Block a user