feat(projects): 多页签绑定路由

This commit is contained in:
Soybean 2021-09-17 19:50:24 +08:00
parent eec0b36f59
commit f29bc05dd9
13 changed files with 195 additions and 65 deletions

View File

@ -5,6 +5,7 @@ export enum EnumRoutePath {
'not-found' = '/404', 'not-found' = '/404',
'no-permission' = '/403', 'no-permission' = '/403',
'service-error' = '/500', 'service-error' = '/500',
'reload' = '/reload',
// 自定义路由 // 自定义路由
'dashboard' = '/dashboard', 'dashboard' = '/dashboard',
'dashboard-analysis' = '/dashboard/analysis', 'dashboard-analysis' = '/dashboard/analysis',
@ -22,6 +23,8 @@ export enum EnumRouteTitle {
'not-found' = '未找到', 'not-found' = '未找到',
'no-permission' = '无权限', 'no-permission' = '无权限',
'service-error' = '服务器错误', 'service-error' = '服务器错误',
'reload' = '重载',
// 自定义路由
'dashboard' = '仪表盘', 'dashboard' = '仪表盘',
'dashboard-analysis' = '分析页', 'dashboard-analysis' = '分析页',
'dashboard-workbench' = '工作台', 'dashboard-workbench' = '工作台',

View File

@ -1,5 +1,6 @@
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import type { RouteLocationRaw } from 'vue-router'; import type { RouteLocationRaw } from 'vue-router';
import { EnumRoutePath } from '@/enum';
import { router as globalRouter, RouteNameMap } from '@/router'; import { router as globalRouter, RouteNameMap } from '@/router';
import type { LoginModuleType } from '@/interface'; import type { LoginModuleType } from '@/interface';
@ -61,9 +62,14 @@ export default function useRouterChange(inSetup: boolean = true) {
} }
} }
function toReload(redirectUrl: string) {
router.push({ path: EnumRoutePath.reload, query: { redirectUrl } });
}
return { return {
toHome, toHome,
toLogin, toLogin,
toCurrentLogin toCurrentLogin,
toReload
}; };
} }

View File

@ -15,13 +15,13 @@ export interface ThemeSettings {
menuStyle: MenuStyle; menuStyle: MenuStyle;
/** 头部样式 */ /** 头部样式 */
headerStyle: HeaderStyle; headerStyle: HeaderStyle;
/** 多签样式 */ /** 多签样式 */
multiTabStyle: MultiTabStyle; multiTabStyle: MultiTabStyle;
/** 面包屑样式 */ /** 面包屑样式 */
crumbsStyle: CrumbsStyle; crumbsStyle: CrumbsStyle;
/** 页面样式 */ /** 页面样式 */
pageStyle: PageStyle; pageStyle: PageStyle;
/** 固定头部和多签 */ /** 固定头部和多签 */
fixedHeaderAndTab: boolean; fixedHeaderAndTab: boolean;
/** 显示重载按钮 */ /** 显示重载按钮 */
showReload: boolean; showReload: boolean;
@ -70,9 +70,9 @@ interface MenuStyle {
} }
interface MultiTabStyle { interface MultiTabStyle {
/** 多签高度 */ /** 多签高度 */
height: number; height: number;
/** 多签可见 */ /** 多签可见 */
visible: boolean; visible: boolean;
/** 背景颜色 */ /** 背景颜色 */
bgColor: string; bgColor: string;

View File

@ -1,25 +1,33 @@
<template> <template>
<div v-if="fixedHeaderAndTab && theme.navStyle.mode !== 'horizontal-mix'" class="multi-tab-height w-full"></div> <div v-if="fixedHeaderAndTab && theme.navStyle.mode !== 'horizontal-mix'" class="multi-tab-height w-full"></div>
<div <div
class="multi-tab-height flex-y-center w-full px-10px" class="multi-tab-height flex-y-center justify-between w-full px-10px"
:class="{ 'multi-tab-top absolute': fixedHeaderAndTab, 'bg-[#f5f7f9]': !theme.darkMode }" :class="{ 'multi-tab-top absolute': fixedHeaderAndTab, 'bg-[#f5f7f9]': !theme.darkMode }"
:style="{ zIndex }" :style="{ zIndex }"
> >
<n-space :align="'center'"> <n-space :align="'center'">
<n-tag>爱在西元前</n-tag> <n-tag
<n-tag type="success">不该</n-tag> v-for="item in app.multiTab.routes"
<n-tag type="warning">超人不会飞</n-tag> :key="item.path"
<n-tag type="error">手写的从前</n-tag> :type="app.multiTab.activeRoute === item.fullPath ? 'primary' : 'default'"
<n-tag type="info">哪里都是你</n-tag> class="cursor-pointer"
<n-gradient-text size="24">这是MultiTab组件</n-gradient-text> @click="handleClickTab(item.fullPath)"
>
{{ item.meta?.title }}
</n-tag>
</n-space> </n-space>
<div class="flex-center w-32px h-32px bg-white cursor-pointer" @click="handleReload">
<icon-mdi-refresh class="text-16px" />
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, watch } from 'vue';
import { NSpace, NTag, NGradientText } from 'naive-ui'; import { useRoute } from 'vue-router';
import { useThemeStore } from '@/store'; import { NSpace, NTag } from 'naive-ui';
import { useThemeStore, useAppStore } from '@/store';
import { useRouterChange } from '@/hooks';
defineProps({ defineProps({
zIndex: { zIndex: {
@ -28,7 +36,11 @@ defineProps({
} }
}); });
const route = useRoute();
const theme = useThemeStore(); const theme = useThemeStore();
const app = useAppStore();
const { initMultiTab, addMultiTab, setActiveMultiTab, handleClickTab } = useAppStore();
const { toReload } = useRouterChange();
const fixedHeaderAndTab = computed(() => theme.fixedHeaderAndTab || theme.navStyle.mode === 'horizontal-mix'); const fixedHeaderAndTab = computed(() => theme.fixedHeaderAndTab || theme.navStyle.mode === 'horizontal-mix');
const multiTabHeight = computed(() => { const multiTabHeight = computed(() => {
@ -39,6 +51,25 @@ const headerHeight = computed(() => {
const { height } = theme.headerStyle; const { height } = theme.headerStyle;
return `${height}px`; return `${height}px`;
}); });
function handleReload() {
toReload(route.fullPath);
}
function init() {
initMultiTab();
}
watch(
() => route.fullPath,
newValue => {
addMultiTab(route);
setActiveMultiTab(newValue);
}
);
//
init();
</script> </script>
<style scoped> <style scoped>
.multi-tab-height { .multi-tab-height {

View File

@ -4,7 +4,7 @@
<setting-menu-item label="分割菜单"> <setting-menu-item label="分割菜单">
<n-switch :value="theme.menuStyle.splitMenu" @update:value="handleSplitMenu" /> <n-switch :value="theme.menuStyle.splitMenu" @update:value="handleSplitMenu" />
</setting-menu-item> </setting-menu-item>
<setting-menu-item label="固定头部和多签"> <setting-menu-item label="固定头部和多签">
<n-switch :value="splitMenu" :disabled="disabledSplitMenu" @update:value="handleFixedHeaderAndTab" /> <n-switch :value="splitMenu" :disabled="disabledSplitMenu" @update:value="handleFixedHeaderAndTab" />
</setting-menu-item> </setting-menu-item>
<setting-menu-item label="头部高度"> <setting-menu-item label="头部高度">
@ -16,7 +16,7 @@
@update:value="handleHeaderHeight" @update:value="handleHeaderHeight"
/> />
</setting-menu-item> </setting-menu-item>
<setting-menu-item label="多签高度"> <setting-menu-item label="多签高度">
<n-input-number <n-input-number
class="w-120px" class="w-120px"
size="small" size="small"

View File

@ -7,7 +7,7 @@
<setting-menu-item label="面包屑图标"> <setting-menu-item label="面包屑图标">
<n-switch :value="theme.crumbsStyle.showIcon" @update:value="handleCrumbsIconVisible" /> <n-switch :value="theme.crumbsStyle.showIcon" @update:value="handleCrumbsIconVisible" />
</setting-menu-item> </setting-menu-item>
<setting-menu-item label="多签"> <setting-menu-item label="多签">
<n-switch :value="theme.multiTabStyle.visible" @update:value="handleMultiTabVisible" /> <n-switch :value="theme.multiTabStyle.visible" @update:value="handleMultiTabVisible" />
</setting-menu-item> </setting-menu-item>
<setting-menu-item label="页面切换动画"> <setting-menu-item label="页面切换动画">

View File

@ -1,24 +1,3 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'; export { router, setupRouter } from './setup';
import type { App } from 'vue'; export { RouteNameMap, ROUTE_HOME, customRoutes } from './routes';
import type { RouteRecordRaw } from 'vue-router';
import { constantRoutes, customRoutes, RouteNameMap } from './routes';
import createRouterGuide from './permission';
const routes: Array<RouteRecordRaw> = [...customRoutes, ...constantRoutes];
/** 用于部署vercel托管服务 */
const isVercel = import.meta.env.VITE_HTTP_ENV === 'VERCEL';
export const router = createRouter({
history: isVercel ? createWebHashHistory() : createWebHistory(),
routes
});
export async function setupRouter(app: App) {
app.use(router);
createRouterGuide(router);
await router.isReady();
}
export { RouteNameMap };
export { menus } from './menus'; export { menus } from './menus';

View File

@ -17,7 +17,7 @@ const loginModuleRegExp = getLoginModuleRegExp();
* *
* @description ! * @description !
*/ */
export const constantRoutes: RouteRecordRaw[] = [ const constantRoutes: RouteRecordRaw[] = [
{ {
name: RouteNameMap.get('system'), name: RouteNameMap.get('system'),
path: EnumRoutePath.system, path: EnumRoutePath.system,
@ -82,6 +82,17 @@ export const constantRoutes: RouteRecordRaw[] = [
} }
]; ];
/** 路由首页 */
export const ROUTE_HOME: CustomRoute = {
name: RouteNameMap.get('dashboard-analysis'),
path: EnumRoutePath['dashboard-analysis'],
component: () => import('@/views/dashboard/analysis/index.vue'),
meta: {
requiresAuth: true,
title: EnumRouteTitle['dashboard-analysis']
}
};
/** /**
* *
*/ */
@ -89,10 +100,24 @@ export const customRoutes: CustomRoute[] = [
{ {
name: RouteNameMap.get('root'), name: RouteNameMap.get('root'),
path: EnumRoutePath.root, path: EnumRoutePath.root,
redirect: { name: RouteNameMap.get('dashboard-analysis') }, component: BasicLayout,
redirect: { name: ROUTE_HOME.name },
meta: { meta: {
isNotMenu: true isNotMenu: true
},
children: [
// 重载
{
name: RouteNameMap.get('reload'),
path: EnumRoutePath.reload,
component: () => import('@/views/system/reload/index.vue'),
meta: {
title: EnumRouteTitle.reload,
isNotMenu: true,
fullPage: true
} }
}
]
}, },
{ {
name: RouteNameMap.get('dashboard'), name: RouteNameMap.get('dashboard'),
@ -105,15 +130,7 @@ export const customRoutes: CustomRoute[] = [
icon: Dashboard icon: Dashboard
}, },
children: [ children: [
{ ROUTE_HOME,
name: RouteNameMap.get('dashboard-analysis'),
path: EnumRoutePath['dashboard-analysis'],
component: () => import('@/views/dashboard/analysis/index.vue'),
meta: {
requiresAuth: true,
title: EnumRouteTitle['dashboard-analysis']
}
},
{ {
name: RouteNameMap.get('dashboard-workbench'), name: RouteNameMap.get('dashboard-workbench'),
path: EnumRoutePath['dashboard-workbench'], path: EnumRoutePath['dashboard-workbench'],
@ -169,10 +186,5 @@ export const customRoutes: CustomRoute[] = [
} }
]; ];
/** 路由白名单(不需要登录) */ /** 所有路由 */
export const whitelistRoutes: string[] = [ export const routes: RouteRecordRaw[] = [...customRoutes, ...constantRoutes];
RouteNameMap.get('login')!,
RouteNameMap.get('exception-403')!,
RouteNameMap.get('exception-404')!,
RouteNameMap.get('exception-500')!
];

18
src/router/setup.ts Normal file
View File

@ -0,0 +1,18 @@
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';
import type { App } from 'vue';
import { routes } from './routes';
import createRouterGuide from './permission';
/** 用于部署vercel托管服务 */
const isVercel = import.meta.env.VITE_HTTP_ENV === 'VERCEL';
export const router = createRouter({
history: isVercel ? createWebHashHistory() : createWebHistory(),
routes
});
export async function setupRouter(app: App) {
app.use(router);
createRouterGuide(router);
await router.isReady();
}

View File

@ -48,7 +48,7 @@ const themeSettings: ThemeSettings = {
bgColor: '#fff' bgColor: '#fff'
}, },
multiTabStyle: { multiTabStyle: {
height: 48, height: 44,
visible: true, visible: true,
bgColor: '#fff' bgColor: '#fff'
}, },

View File

@ -1,11 +1,15 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { nextTick } from 'vue';
import type { RouteLocationNormalizedLoaded } from 'vue-router'; import type { RouteLocationNormalizedLoaded } from 'vue-router';
import { store } from '@/store'; import { store } from '@/store';
import { router, ROUTE_HOME } from '@/router';
/** app状态 */ /** app状态 */
interface AppState { interface AppState {
menu: MenuState; menu: MenuState;
multiTab: MultiTab; multiTab: MultiTab;
/** 重新加载标记 */
reloadFlag: boolean;
settingDrawer: SettingDrawer; settingDrawer: SettingDrawer;
} }
@ -15,8 +19,15 @@ interface MenuState {
collapsed: boolean; collapsed: boolean;
} }
type MultiTabRoute = Partial<RouteLocationNormalizedLoaded> & {
path: string;
fullPath: string;
};
/** 多页签 */
interface MultiTab { interface MultiTab {
routes: RouteLocationNormalizedLoaded[]; routes: MultiTabRoute[];
activeRoute: string;
} }
/** 项目配置抽屉的状态 */ /** 项目配置抽屉的状态 */
@ -32,8 +43,10 @@ const appStore = defineStore({
collapsed: false collapsed: false
}, },
multiTab: { multiTab: {
routes: [] routes: [],
activeRoute: ''
}, },
reloadFlag: true,
settingDrawer: { settingDrawer: {
visible: false visible: false
} }
@ -47,9 +60,59 @@ const appStore = defineStore({
toggleMenu() { toggleMenu() {
this.menu.collapsed = !this.menu.collapsed; this.menu.collapsed = !this.menu.collapsed;
}, },
/** 判断tab路由是否存在某个路由 */
getIndexInTabRoutes(fullPath: string) {
return this.multiTab.routes.findIndex(item => item.fullPath === fullPath);
},
/** 添加多tab的数据 */ /** 添加多tab的数据 */
addMultiTab(route: RouteLocationNormalizedLoaded) { addMultiTab(route: RouteLocationNormalizedLoaded) {
this.multiTab.routes.push(route); const { fullPath } = route;
const isExist = this.getIndexInTabRoutes(fullPath) > -1;
if (!isExist) {
this.multiTab.routes.push({ ...route });
}
},
handleClickTab(fullPath: string) {
if (this.multiTab.activeRoute !== fullPath) {
router.push(fullPath);
this.setActiveMultiTab(fullPath);
}
},
/** 设置当前路由对应的tab为激活状态 */
setActiveMultiTab(fullPath: string) {
this.multiTab.activeRoute = fullPath;
},
/** 获取路由首页信息 */
getHomeTabRoute(route: RouteLocationNormalizedLoaded, isHome: boolean) {
const { name, path, meta } = ROUTE_HOME;
const home: MultiTabRoute = {
name,
path,
fullPath: path,
meta
};
if (isHome) {
Object.assign(home, route);
}
return home;
},
/** 初始化多页签数据 */
initMultiTab() {
const { currentRoute } = router;
const isHome = currentRoute.value.name === ROUTE_HOME.name;
const home = this.getHomeTabRoute(currentRoute.value, isHome);
const routes = [home];
if (!isHome) {
routes.push(currentRoute.value);
}
this.multiTab.routes = routes;
this.setActiveMultiTab(currentRoute.value.fullPath);
},
/** 重新加载页面 */
async handleReload() {
this.reloadFlag = false;
await nextTick();
this.reloadFlag = true;
}, },
/** 打开配置抽屉 */ /** 打开配置抽屉 */
openSettingDrawer() { openSettingDrawer() {

View File

@ -97,7 +97,7 @@ const themeStore = defineStore({
this.multiTabStyle.height = height; this.multiTabStyle.height = height;
} }
}, },
/** 设置多签的显示 */ /** 设置多签的显示 */
handleMultiTabVisible(visible: boolean) { handleMultiTabVisible(visible: boolean) {
this.multiTabStyle.visible = visible; this.multiTabStyle.visible = visible;
}, },

View File

@ -0,0 +1,18 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import { useRoute, useRouter } from 'vue-router';
const { query } = useRoute();
const router = useRouter();
function init() {
const redirect = (query.redirectUrl as string) || '/';
router.replace(redirect);
}
init();
</script>
<style scoped></style>