mirror of
https://github.com/Tencent/tdesign-vue-next-starter.git
synced 2024-12-23 02:56:35 +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[];
|
] as MockMethod[];
|
||||||
|
|
75
package.json
75
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tdesign-vue-next-starter",
|
"name": "tdesign-vue-next-starter",
|
||||||
"version": "0.6.1",
|
"version": "0.7.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:mock": "vite --open --mode mock",
|
"dev:mock": "vite --open --mode mock",
|
||||||
"dev": "vite --open --mode development",
|
"dev": "vite --open --mode development",
|
||||||
|
@ -19,58 +19,59 @@
|
||||||
"test:coverage": "echo \"no test:coverage specified,work in process\""
|
"test:coverage": "echo \"no test:coverage specified,work in process\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.1.3",
|
"@types/nprogress": "^0.2.0",
|
||||||
"dayjs": "^1.10.6",
|
"axios": "^1.2.2",
|
||||||
"echarts": "~5.1.2",
|
"dayjs": "^1.11.7",
|
||||||
|
"echarts": "5.1.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.0.11",
|
"pinia": "^2.0.28",
|
||||||
"pinia-plugin-persistedstate": "^3.0.1",
|
"pinia-plugin-persistedstate": "^3.0.2",
|
||||||
"qrcode.vue": "^3.2.2",
|
"qrcode.vue": "^3.3.3",
|
||||||
"qs": "^6.10.5",
|
"qs": "^6.11.0",
|
||||||
"tdesign-icons-vue-next": "^0.1.7",
|
"tdesign-icons-vue-next": "^0.1.7",
|
||||||
"tdesign-vue-next": "^1.0.0",
|
"tdesign-vue-next": "^1.0.0",
|
||||||
"tvision-color": "^1.5.0",
|
"tvision-color": "^1.5.0",
|
||||||
"vue": "^3.2.31",
|
"vue": "^3.2.45",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-router": "~4.1.5"
|
"vue-router": "~4.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.0.3",
|
"@commitlint/cli": "^17.3.0",
|
||||||
"@commitlint/config-conventional": "^17.0.3",
|
"@commitlint/config-conventional": "^17.3.0",
|
||||||
"@types/echarts": "^4.9.10",
|
"@types/echarts": "^4.9.16",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/ws": "^8.2.2",
|
"@types/ws": "^8.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||||
"@typescript-eslint/parser": "^4.29.3",
|
"@typescript-eslint/parser": "^5.47.1",
|
||||||
"@vitejs/plugin-vue": "^2.3.1",
|
"@vitejs/plugin-vue": "^3.2.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^1.1.7",
|
"@vitejs/plugin-vue-jsx": "^1.1.7",
|
||||||
"@vue/compiler-sfc": "^3.0.5",
|
"@vue/compiler-sfc": "^3.2.45",
|
||||||
"@vue/eslint-config-typescript": "^11.0.0",
|
"@vue/eslint-config-typescript": "^11.0.2",
|
||||||
"commitizen": "^4.2.4",
|
"commitizen": "^4.2.4",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^8.30.0",
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-import": "^2.24.2",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-vue": "^9.2.0",
|
"eslint-plugin-vue": "^9.8.0",
|
||||||
"eslint-plugin-vue-scoped-css": "^2.2.0",
|
"eslint-plugin-vue-scoped-css": "^2.2.0",
|
||||||
"husky": "^7.0.4",
|
"husky": "^8.0.2",
|
||||||
"less": "^4.1.1",
|
"less": "^4.1.3",
|
||||||
"lint-staged": "^12.1.2",
|
"lint-staged": "^13.1.0",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.8.1",
|
||||||
"stylelint": "~13.13.1",
|
"stylelint": "~14.9.1",
|
||||||
"stylelint-config-prettier": "~9.0.3",
|
"stylelint-config-prettier": "~9.0.4",
|
||||||
"stylelint-less": "1.0.1",
|
"stylelint-less": "1.0.6",
|
||||||
"stylelint-order": "~4.1.0",
|
"stylelint-order": "~6.0.1",
|
||||||
"typescript": "~4.8.4",
|
"typescript": "~4.9.4",
|
||||||
"vite": "^2.7.1",
|
"vite": "^3.0.3",
|
||||||
"vite-plugin-mock": "^2.9.6",
|
"vite-plugin-mock": "^2.9.6",
|
||||||
"vite-svg-loader": "^3.1.0",
|
"vite-svg-loader": "^4.0.0",
|
||||||
"vue-tsc": "^1.0.8"
|
"vue-tsc": "^1.0.19"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"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',
|
middle: 'warning',
|
||||||
high: 'danger',
|
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">
|
<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])">
|
<t-menu-item v-if="getHref(item)" :name="item.path" :value="getPath(item)" @click="openHref(getHref(item)[0])">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
<component :is="menuIcon(item)"></component>
|
||||||
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
|
||||||
</template>
|
</template>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</t-menu-item>
|
</t-menu-item>
|
||||||
<t-menu-item v-else :name="item.path" :value="getPath(item)" :to="item.path">
|
<t-menu-item v-else :name="item.path" :value="getPath(item)" :to="item.path">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
<component :is="menuIcon(item)"></component>
|
||||||
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
|
||||||
</template>
|
</template>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</t-menu-item>
|
</t-menu-item>
|
||||||
</template>
|
</template>
|
||||||
<t-submenu v-else :name="item.path" :value="item.path" :title="item.title">
|
<t-submenu v-else :name="item.path" :value="item.path" :title="item.title">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<t-icon v-if="beIcon(item)" :name="item.icon" />
|
<component :is="menuIcon(item)"></component>
|
||||||
<component :is="beRender(item).render" v-else-if="beRender(item).can" class="t-icon" />
|
|
||||||
</template>
|
</template>
|
||||||
<menu-content v-if="item.children" :nav-data="item.children" />
|
<menu-content v-if="item.children" :nav-data="item.children" />
|
||||||
</t-submenu>
|
</t-submenu>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup lang="tsx">
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import isObject from 'lodash/isObject';
|
|
||||||
import type { MenuRoute } from '@/types/interface';
|
import type { MenuRoute } from '@/types/interface';
|
||||||
import { getActive } from '@/router';
|
import { getActive } from '@/router';
|
||||||
|
|
||||||
|
type ListItemType = MenuRoute & { icon?: string };
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
navData: {
|
navData: {
|
||||||
type: Array as PropType<MenuRoute[]>,
|
type: Array as PropType<MenuRoute[]>,
|
||||||
|
@ -43,12 +40,17 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
const active = computed(() => getActive());
|
const active = computed(() => getActive());
|
||||||
|
|
||||||
const list = computed(() => {
|
const list = computed(() => {
|
||||||
const { navData } = props;
|
const { navData } = props;
|
||||||
return getMenuList(navData);
|
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[] => {
|
const getMenuList = (list: MenuRoute[], basePath?: string): ListItemType[] => {
|
||||||
if (!list) {
|
if (!list) {
|
||||||
|
@ -61,10 +63,11 @@ const getMenuList = (list: MenuRoute[], basePath?: string): ListItemType[] => {
|
||||||
return list
|
return list
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const path = basePath && !item.path.includes(basePath) ? `${basePath}/${item.path}` : item.path;
|
const path = basePath && !item.path.includes(basePath) ? `${basePath}/${item.path}` : item.path;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path,
|
path,
|
||||||
title: item.meta?.title,
|
title: item.meta?.title,
|
||||||
icon: item.meta?.icon || '',
|
icon: item.meta?.icon,
|
||||||
children: getMenuList(item.children, path),
|
children: getMenuList(item.children, path),
|
||||||
meta: item.meta,
|
meta: item.meta,
|
||||||
redirect: item.redirect,
|
redirect: item.redirect,
|
||||||
|
@ -81,33 +84,14 @@ const getHref = (item: MenuRoute) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPath = (item) => {
|
const getPath = (item: ListItemType) => {
|
||||||
if (active.value.startsWith(item.path)) {
|
if (active.value.startsWith(item.path)) {
|
||||||
return active.value;
|
return active.value;
|
||||||
}
|
}
|
||||||
return item.meta?.single ? item.redirect : item.path;
|
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) => {
|
const openHref = (url: string) => {
|
||||||
window.open(url);
|
window.open(url);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { createApp } from 'vue';
|
||||||
|
|
||||||
import TDesign from 'tdesign-vue-next';
|
import TDesign from 'tdesign-vue-next';
|
||||||
import 'tdesign-vue-next/es/style/index.css';
|
import 'tdesign-vue-next/es/style/index.css';
|
||||||
|
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import '@/style/index.less';
|
import '@/style/index.less';
|
||||||
|
|
|
@ -2,18 +2,20 @@ import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
import NProgress from 'nprogress'; // progress bar
|
import NProgress from 'nprogress'; // progress bar
|
||||||
import 'nprogress/nprogress.css'; // progress bar style
|
import 'nprogress/nprogress.css'; // progress bar style
|
||||||
|
|
||||||
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
import { getPermissionStore, getUserStore } from '@/store';
|
import { getPermissionStore, getUserStore } from '@/store';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
|
import { PAGE_NOT_FOUND_ROUTE } from '@/utils/route/constant';
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false });
|
NProgress.configure({ showSpinner: false });
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
|
|
||||||
const userStore = getUserStore();
|
|
||||||
const permissionStore = getPermissionStore();
|
const permissionStore = getPermissionStore();
|
||||||
const { whiteListRouters } = permissionStore;
|
const { whiteListRouters } = permissionStore;
|
||||||
|
|
||||||
|
const userStore = getUserStore();
|
||||||
const { token } = userStore;
|
const { token } = userStore;
|
||||||
if (token) {
|
if (token) {
|
||||||
if (to.path === '/login') {
|
if (to.path === '/login') {
|
||||||
|
@ -21,18 +23,27 @@ router.beforeEach(async (to, from, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { roles } = userStore;
|
const { asyncRoutes } = permissionStore;
|
||||||
|
|
||||||
if (roles && roles.length > 0) {
|
if (asyncRoutes && asyncRoutes.length === 0) {
|
||||||
next();
|
const routeList = await permissionStore.buildAsyncRoutes();
|
||||||
|
routeList.forEach((item: RouteRecordRaw) => {
|
||||||
|
router.addRoute(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (to.name === PAGE_NOT_FOUND_ROUTE.name) {
|
||||||
|
// 动态添加路由后,此处应当重定向到fullPath,否则会加载404页面内容
|
||||||
|
next({ path: to.fullPath, replace: true, query: to.query });
|
||||||
} else {
|
} else {
|
||||||
|
const redirect = decodeURIComponent((from.query.redirect || to.path) as string);
|
||||||
|
next(to.path === redirect ? { ...to, replace: true } : { path: redirect });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await userStore.getUserInfo();
|
await userStore.getUserInfo();
|
||||||
|
|
||||||
const { roles } = userStore;
|
|
||||||
|
|
||||||
await permissionStore.initRoutes(roles);
|
|
||||||
|
|
||||||
if (router.hasRoute(to.name)) {
|
if (router.hasRoute(to.name)) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
|
@ -46,7 +57,6 @@ router.beforeEach(async (to, from, next) => {
|
||||||
});
|
});
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
/* white list router */
|
/* white list router */
|
||||||
if (whiteListRouters.indexOf(to.path) !== -1) {
|
if (whiteListRouters.indexOf(to.path) !== -1) {
|
||||||
|
@ -67,7 +77,7 @@ router.afterEach((to) => {
|
||||||
const permissionStore = getPermissionStore();
|
const permissionStore = getPermissionStore();
|
||||||
|
|
||||||
userStore.logout();
|
userStore.logout();
|
||||||
permissionStore.restore();
|
permissionStore.restoreRoutes();
|
||||||
}
|
}
|
||||||
NProgress.done();
|
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';
|
import uniq from 'lodash/uniq';
|
||||||
|
|
||||||
const env = import.meta.env.MODE || 'development';
|
const env = import.meta.env.MODE || 'development';
|
||||||
|
|
||||||
// 自动导入modules文件夹下所有ts文件
|
// 导入homepage相关固定路由
|
||||||
const modules = import.meta.globEager('./modules/**/*.ts');
|
const homepageModules = import.meta.globEager('./modules/**/homepage.ts');
|
||||||
|
|
||||||
// 路由暂存
|
// 导入modules非homepage相关固定路由
|
||||||
const routeModuleList: Array<RouteRecordRaw> = [];
|
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> = [
|
const defaultRouterList: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
|
@ -32,19 +20,29 @@ const defaultRouterList: Array<RouteRecordRaw> = [
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/dashboard/base',
|
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 = () => {
|
export const getRoutesExpanded = () => {
|
||||||
const expandedRoutes = [];
|
const expandedRoutes = [];
|
||||||
|
|
||||||
allRoutes.forEach((item) => {
|
fixedRouterList.forEach((item) => {
|
||||||
if (item.meta && item.meta.expanded) {
|
if (item.meta && item.meta.expanded) {
|
||||||
expandedRoutes.push(item.path);
|
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 Layout from '@/layouts/index.vue';
|
||||||
import DashboardIcon from '@/assets/assets-slide-dashboard.svg';
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
|
@ -7,19 +8,27 @@ export default [
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/dashboard/base',
|
redirect: '/dashboard/base',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
meta: { title: '仪表盘', icon: DashboardIcon },
|
meta: {
|
||||||
|
title: '仪表盘',
|
||||||
|
icon: shallowRef(DashboardIcon),
|
||||||
|
orderNo: 0,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'base',
|
path: 'base',
|
||||||
name: 'DashboardBase',
|
name: 'DashboardBase',
|
||||||
component: () => import('@/pages/dashboard/base/index.vue'),
|
component: () => import('@/pages/dashboard/base/index.vue'),
|
||||||
meta: { title: '概览仪表盘' },
|
meta: {
|
||||||
|
title: '概览仪表盘',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'detail',
|
path: 'detail',
|
||||||
name: 'DashboardDetail',
|
name: 'DashboardDetail',
|
||||||
component: () => import('@/pages/dashboard/detail/index.vue'),
|
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 Layout from '@/layouts/index.vue';
|
||||||
import LogoutIcon from '@/assets/assets-slide-logout.svg';
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
|
@ -21,7 +22,7 @@ export default [
|
||||||
path: '/loginRedirect',
|
path: '/loginRedirect',
|
||||||
name: 'loginRedirect',
|
name: 'loginRedirect',
|
||||||
redirect: '/login',
|
redirect: '/login',
|
||||||
meta: { title: '登录页', icon: LogoutIcon },
|
meta: { title: '登录页', icon: shallowRef(LogoutIcon) },
|
||||||
component: () => import('@/layouts/blank.vue'),
|
component: () => import('@/layouts/blank.vue'),
|
||||||
children: [
|
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 { defineStore } from 'pinia';
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
import router, { asyncRouterList } from '@/router';
|
import router, { fixedRouterList, homepageRouterList } from '@/router';
|
||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
|
import { RouteItem } from '@/api/model/permissionModel';
|
||||||
function filterPermissionsRouters(routes: Array<RouteRecordRaw>, roles: Array<unknown>) {
|
import { getMenuList } from '@/api/permission';
|
||||||
const res = [];
|
import { transformObjectToRoute } from '@/utils/route';
|
||||||
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', {
|
export const usePermissionStore = defineStore('permission', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
whiteListRouters: ['/login'],
|
whiteListRouters: ['/login'],
|
||||||
routers: [],
|
routers: [],
|
||||||
removeRoutes: [],
|
removeRoutes: [],
|
||||||
|
asyncRoutes: [],
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async initRoutes(roles: Array<unknown>) {
|
async initRoutes() {
|
||||||
let accessedRouters = [];
|
const accessedRouters = this.asyncRoutes;
|
||||||
|
|
||||||
let removeRoutes = [];
|
// 在菜单展示全部路由
|
||||||
// special token
|
this.routers = [...homepageRouterList, ...accessedRouters, ...fixedRouterList];
|
||||||
if (roles.includes('all')) {
|
// 在菜单只展示动态路由和首页
|
||||||
accessedRouters = asyncRouterList;
|
// this.routers = [...homepageRouterList, ...accessedRouters];
|
||||||
} else {
|
// 在菜单只展示动态路由
|
||||||
const res = filterPermissionsRouters(asyncRouterList, roles);
|
// this.routers = [...accessedRouters];
|
||||||
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() {
|
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) => {
|
this.removeRoutes.forEach((item: RouteRecordRaw) => {
|
||||||
router.addRoute(item);
|
router.addRoute(item);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { TOKEN_NAME } from '@/config/global';
|
||||||
import { store, usePermissionStore } from '@/store';
|
import { store, usePermissionStore } from '@/store';
|
||||||
|
|
||||||
const InitUserInfo = {
|
const InitUserInfo = {
|
||||||
roles: [],
|
roles: [], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', {
|
export const useUserStore = defineStore('user', {
|
||||||
|
@ -20,7 +20,7 @@ export const useUserStore = defineStore('user', {
|
||||||
async login(userInfo: Record<string, unknown>) {
|
async login(userInfo: Record<string, unknown>) {
|
||||||
const mockLogin = async (userInfo: Record<string, unknown>) => {
|
const mockLogin = async (userInfo: Record<string, unknown>) => {
|
||||||
// 登录请求流程
|
// 登录请求流程
|
||||||
console.log(userInfo);
|
console.log(`用户信息:`, userInfo);
|
||||||
// const { account, password } = userInfo;
|
// const { account, password } = userInfo;
|
||||||
// if (account !== 'td') {
|
// if (account !== 'td') {
|
||||||
// return {
|
// return {
|
||||||
|
@ -57,15 +57,14 @@ export const useUserStore = defineStore('user', {
|
||||||
if (token === 'main_token') {
|
if (token === 'main_token') {
|
||||||
return {
|
return {
|
||||||
name: 'td_main',
|
name: 'td_main',
|
||||||
roles: ['all'],
|
roles: ['all'], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: 'td_dev',
|
name: 'td_dev',
|
||||||
roles: ['UserIndex', 'DashboardBase', 'login'],
|
roles: ['UserIndex', 'DashboardBase', 'login'], // 前端权限模型使用 如果使用请配置modules/permission-fe.ts使用
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await mockRemoteUserInfo(this.token);
|
const res = await mockRemoteUserInfo(this.token);
|
||||||
|
|
||||||
this.userInfo = res;
|
this.userInfo = res;
|
||||||
|
@ -80,11 +79,9 @@ export const useUserStore = defineStore('user', {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
persist: {
|
persist: {
|
||||||
afterRestore: (ctx) => {
|
afterRestore: () => {
|
||||||
if (ctx.store.roles && ctx.store.roles.length > 0) {
|
|
||||||
const permissionStore = usePermissionStore();
|
const permissionStore = usePermissionStore();
|
||||||
permissionStore.initRoutes(ctx.store.roles);
|
permissionStore.initRoutes();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { CreateAxiosOptions } from './AxiosTransform';
|
import { CreateAxiosOptions } from './AxiosTransform';
|
||||||
import { AxiosCanceler } from './AxiosCancel';
|
import { AxiosCanceler } from './AxiosCancel';
|
||||||
import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
|
import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
|
||||||
|
import { ContentTypeEnum } from '@/constants';
|
||||||
|
|
||||||
// Axios模块
|
// Axios模块
|
||||||
export class VAxios {
|
export class VAxios {
|
||||||
|
@ -64,12 +65,15 @@ export class VAxios {
|
||||||
|
|
||||||
// 请求配置处理
|
// 请求配置处理
|
||||||
this.instance.interceptors.request.use((config: AxiosRequestConfig) => {
|
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 (!ignoreRepeat) axiosCanceler.addPending(config);
|
||||||
|
|
||||||
if (requestInterceptors && isFunction(requestInterceptors)) {
|
if (requestInterceptors && isFunction(requestInterceptors)) {
|
||||||
config = requestInterceptors(config, this.options);
|
config = requestInterceptors(config, this.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}, undefined);
|
}, undefined);
|
||||||
|
|
||||||
|
@ -99,7 +103,7 @@ export class VAxios {
|
||||||
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
|
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
contentType !== 'application/x-www-form-urlencoded;charset=UTF-8' ||
|
contentType !== ContentTypeEnum.FormURLEncoded ||
|
||||||
!Reflect.has(config, 'data') ||
|
!Reflect.has(config, 'data') ||
|
||||||
config.method?.toUpperCase() === 'GET'
|
config.method?.toUpperCase() === 'GET'
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { VAxios } from './Axios';
|
||||||
import proxy from '@/config/proxy';
|
import proxy from '@/config/proxy';
|
||||||
import { joinTimestamp, formatRequestDate, setObjToUrlParams } from './utils';
|
import { joinTimestamp, formatRequestDate, setObjToUrlParams } from './utils';
|
||||||
import { TOKEN_NAME } from '@/config/global';
|
import { TOKEN_NAME } from '@/config/global';
|
||||||
|
import { ContentTypeEnum } from '@/constants';
|
||||||
|
|
||||||
const env = import.meta.env.MODE || 'development';
|
const env = import.meta.env.MODE || 'development';
|
||||||
|
|
||||||
|
@ -141,7 +142,7 @@ const transform: AxiosTransform = {
|
||||||
resolve(config);
|
resolve(config);
|
||||||
}, config.requestOptions.retry.delay || 1);
|
}, 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));
|
return backoff.then((config) => request.request(config));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -158,7 +159,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
|
||||||
// 携带Cookie
|
// 携带Cookie
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
// 头信息
|
// 头信息
|
||||||
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
headers: { 'Content-Type': ContentTypeEnum.Json },
|
||||||
// 数据处理方式
|
// 数据处理方式
|
||||||
transform,
|
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 { ConfigEnv, UserConfig, loadEnv } from 'vite';
|
||||||
import { viteMockServe } from 'vite-plugin-mock';
|
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 vueJsx from '@vitejs/plugin-vue-jsx';
|
||||||
import svgLoader from 'vite-svg-loader';
|
import svgLoader from 'vite-svg-loader';
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
createVuePlugin(),
|
vue(),
|
||||||
vueJsx(),
|
vueJsx(),
|
||||||
viteMockServe({
|
viteMockServe({
|
||||||
mockPath: 'mock',
|
mockPath: 'mock',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user