feat: 优化路由逻辑
自动部署 / build (push) Successful in 1m27s Details

master
绝弹 2023-11-07 21:12:26 +08:00
parent b11d43a0a6
commit f23f8f53e6
18 changed files with 205 additions and 118 deletions

10
.env
View File

@ -5,8 +5,12 @@
VITE_TITLE = 绝弹项目管理 VITE_TITLE = 绝弹项目管理
# 网站副标题 # 网站副标题
VITE_SUBTITLE = 快速开发web应用的模板工具 VITE_SUBTITLE = 快速开发web应用的模板工具
# 接口前缀 说明:参见 axios 的 baseURL # 接口前缀:参见 axios 的 baseURL
VITE_API = / VITE_API = /
# 首页路径
VITE_HOME_PATH = /home/home
# 路由模式web(路径) hash(锚点)
VITE_HISTORY = web
# ===================================================================================== # =====================================================================================
# 开发设置 # 开发设置
@ -19,9 +23,5 @@ VITE_PORT = 3020
VITE_PROXY = http://127.0.0.1:3030/ VITE_PROXY = http://127.0.0.1:3030/
# API文档 说明:需返回符合 OPENAPI 规范的json内容 # API文档 说明:需返回符合 OPENAPI 规范的json内容
VITE_OPENAPI = http://127.0.0.1:3030/openapi.json VITE_OPENAPI = http://127.0.0.1:3030/openapi.json
# =====================================================================================
# 构建设置
# =====================================================================================
# 文件后缀 说明设为dev时会优先加载index.dev.vue文件否则回退至index.vue文件 # 文件后缀 说明设为dev时会优先加载index.dev.vue文件否则回退至index.vue文件
VITE_EXTENSION = dev VITE_EXTENSION = dev

View File

@ -10,7 +10,7 @@ export default defineComponent({
watch( watch(
() => route.path, () => route.path,
() => { () => {
selectedKeys.value = route.matched.map((i) => i.path); selectedKeys.value = route.matched.map((i) => i.aliasOf?.path ?? i.path);
}, },
{ immediate: true } { immediate: true }
); );
@ -31,7 +31,9 @@ export default defineComponent({
const icon = route.icon ? () => <i class={route.icon} /> : null; const icon = route.icon ? () => <i class={route.icon} /> : null;
const node: any = route.children?.length ? ( const node: any = route.children?.length ? (
<> <>
<div class="px-2"><a-divider margin={6} class="!border-slate-100"></a-divider></div> <div class="px-2">
<a-divider margin={6} class="!border-slate-100"></a-divider>
</div>
{this.renderItem(route?.children)} {this.renderItem(route?.children)}
</> </>
) : ( ) : (

View File

@ -7,11 +7,11 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { api } from "@/api"; import { api } from "@/api";
import { createColumn, updateColumn, useAniTable } from "@/components"; import { createColumn, updateColumn, useAniTable } from "@/components";
import { MenuTypes, MenuType } from "@/constants/menu"; import { MenuType, MenuTypes } from "@/constants/menu";
import { flatedMenus } from "@/router"; import { flatMenus } from "@/router";
import { listToTree } from "@/utils/listToTree"; import { listToTree } from "@/utils/listToTree";
const menuArr = flatedMenus.map((i) => ({ label: i.title, value: i.id })); const menuArr = flatMenus.map((i) => ({ label: i.title, value: i.id }));
const expanded = ref(false); const expanded = ref(false);
const toggleExpand = () => { const toggleExpand = () => {

68
src/router/guards/auth.ts Normal file
View File

@ -0,0 +1,68 @@
import { api } from "@/api";
import { store, useUserStore } from "@/store";
import { useMenuStore } from "@/store/menu";
import { treeFind } from "@/utils/listToTree";
import { Notification } from "@arco-design/web-vue";
import { Router } from "vue-router";
import { menus } from "../menus";
import { APP_HOME_NAME } from "../routes/base";
import { APP_ROUTE_NAME, routes } from "../routes/page";
const WHITE_LIST = ["/:all(.*)*"];
const UNSIGNIN_LIST = ["/login"];
/**
*
* @param to
* @description store pinia-plugin-peristedstate
* @returns
*/
export function useAuthGuard(router: Router) {
api.expireHandler = () => {
const userStore = useUserStore(store);
const redirect = router.currentRoute.value.path;
userStore.clearUser();
router.push({ path: "/login", query: { redirect } });
};
router.beforeEach(async function (to) {
const userStore = useUserStore(store);
const menuStore = useMenuStore(store);
if (to.meta.auth?.some((i) => i === "*")) {
return true;
}
if (WHITE_LIST.includes(to.path)) {
return true;
}
if (UNSIGNIN_LIST.includes(to.path)) {
if (!userStore.accessToken) {
return true;
}
Notification.warning({
title: "跳转提示",
content: `您已登陆,如需重新登陆请退出后再操作!`,
});
return false;
}
if (!userStore.accessToken) {
return { path: "/login", query: { redirect: to.path } };
}
if (!menuStore.menus.length) {
menuStore.setMenus(menus);
menuStore.setHome(import.meta.env.VITE_HOME_PATH);
for (const route of routes) {
router.addRoute(route);
}
const home = treeFind(routes, (i) => i.path === menuStore.home);
if (home) {
const route = { ...home, name: APP_HOME_NAME, alias: "/" };
router.removeRoute(home.name!);
router.addRoute(APP_ROUTE_NAME, route);
return router.replace(to.path);
}
}
return true;
});
return router;
}

View File

@ -1,31 +0,0 @@
import { store, useUserStore } from "@/store";
import { Notification } from "@arco-design/web-vue";
import { NavigationGuardWithThis } from "vue-router";
const WHITE_LIST = ["/:all(.*)*"];
const UNSIGNIN_LIST = ["/login"];
// store不能放在外面否则 pinia-plugin-peristedstate 插件会失效
export const authGuard: NavigationGuardWithThis<undefined> = async function (to) {
const userStore = useUserStore(store);
if (to.meta.auth?.some((i) => i === "*")) {
return true;
}
if (WHITE_LIST.includes(to.path)) {
return true;
}
if (UNSIGNIN_LIST.includes(to.path)) {
if (userStore.accessToken) {
Notification.warning({
title: "跳转提示",
content: `提示:您已登陆,如需重新登陆请退出后再操作!`,
});
return false;
}
return true;
}
if (!userStore.accessToken) {
return { path: "/login", query: { redirect: to.path } };
}
return true;
};

View File

@ -1,31 +0,0 @@
import { NProgress } from "@/libs/nprogress";
import { useAppStore } from "@/store";
import { NavigationGuardWithThis, NavigationHookAfter } from "vue-router";
const routeMap = new Map<string, boolean>();
const before: NavigationGuardWithThis<undefined> = function (to) {
NProgress.start();
if (routeMap.get(to.fullPath)) {
return true;
}
const appStore = useAppStore();
appStore.setPageLoading(true);
};
const after: NavigationHookAfter = function (to) {
NProgress.done();
if (routeMap.get(to.fullPath)) {
return;
}
const appStore = useAppStore();
setTimeout(() => {
appStore.setPageLoading(false);
routeMap.set(to.fullPath, true);
}, 500);
};
export const progressGuard = {
before,
after,
};

View File

@ -1,9 +0,0 @@
import { store, useAppStore } from "@/store";
import { NavigationHookAfter } from "vue-router";
export const titleGuard: NavigationHookAfter = function (to) {
const appStore = useAppStore(store);
const title = to.meta.title || appStore.title;
const subtitle = appStore.subtitle;
document.title = `${title} | ${subtitle}`;
};

View File

@ -0,0 +1,30 @@
import { NProgress } from "@/libs/nprogress";
import { useAppStore } from "@/store";
import { Router } from "vue-router";
const routeMap = new Map<string, boolean>();
export function useProgressGard(router: Router) {
router.beforeEach(function (to) {
NProgress.start();
if (routeMap.get(to.fullPath)) {
return true;
}
const appStore = useAppStore();
appStore.setPageLoading(true);
});
router.afterEach(function (to) {
NProgress.done();
if (routeMap.get(to.fullPath)) {
return;
}
const appStore = useAppStore();
setTimeout(() => {
appStore.setPageLoading(false);
routeMap.set(to.fullPath, true);
}, 500);
});
return router;
}

View File

@ -0,0 +1,12 @@
import { store, useAppStore } from "@/store";
import { Router } from "vue-router";
export function useTitleGuard(router: Router) {
router.beforeEach(function (to) {
const appStore = useAppStore(store);
const title = to.meta.title || appStore.title;
const subtitle = appStore.subtitle;
document.title = `${title} | ${subtitle}`;
});
return router;
}

View File

@ -1,3 +1,4 @@
export * from "./menus"; export * from "./menus";
export * from "./router"; export * from "./router";
export * from "./routes"; export * from "./routes/page";

View File

@ -1,5 +1,5 @@
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from "vue-router";
import { appRoutes } from "../routes"; import { appRoutes } from "../routes/page";
/** /**
* *

View File

@ -1,42 +1,29 @@
import { createRouter, createWebHashHistory } from "vue-router"; import { createRouter } from "vue-router";
import { authGuard } from "../guards/guard-auth"; import { useAuthGuard } from "../guards/auth";
import { progressGuard } from "../guards/guard-progress"; import { useProgressGard } from "../guards/progress";
import { titleGuard } from "../guards/guard-title"; import { useTitleGuard } from "../guards/title";
import { routes } from "../routes";
import { baseRoutes } from "../routes/base"; import { baseRoutes } from "../routes/base";
import { api } from "@/api"; import { historyMode } from "./util";
import { store, useUserStore } from "@/store";
/** /**
* *
*/ */
export const router = createRouter({ export const router = createRouter({
history: createWebHashHistory(), history: historyMode(),
routes: [...baseRoutes, ...routes], routes: [...baseRoutes],
}); });
/** /**
* *
*/ */
router.beforeEach(progressGuard.before); useProgressGard(router);
router.afterEach(progressGuard.after);
/** /**
* *
*/ */
router.beforeEach(authGuard); useAuthGuard(router);
/** /**
* *
*/ */
router.afterEach(titleGuard); useTitleGuard(router);
/**
*
*/
api.expireHandler = () => {
const userStore = useUserStore(store);
const redirect = router.currentRoute.value.path;
userStore.clearUser();
router.push({ path: "/login", query: { redirect } });
};

17
src/router/router/util.ts Normal file
View File

@ -0,0 +1,17 @@
import { createWebHashHistory, createWebHistory } from "vue-router";
/**
*
*/
const HistoryMap = {
web: createWebHistory,
hash: createWebHashHistory,
};
/**
*
*/
export function historyMode() {
const mode = HistoryMap[import.meta.env.VITE_HISTORY];
return mode();
}

View File

@ -1,14 +1,14 @@
import { useMenuStore } from "@/store/menu";
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from "vue-router";
export const APP_HOME_NAME = "__APP_HOME__";
/**
*
*/
export const baseRoutes: RouteRecordRaw[] = [ export const baseRoutes: RouteRecordRaw[] = [
{ {
path: "/", path: "/",
redirect: (to) => { name: APP_HOME_NAME,
const { home } = useMenuStore(); component: () => "Home Page",
return {
name: home,
};
},
}, },
]; ];

View File

@ -1,8 +1,8 @@
import generatedRoutes from "virtual:generated-pages"; import generatedRoutes from "virtual:generated-pages";
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from "vue-router";
const TOP_ROUTE_PREF = "_"; export const TOP_ROUTE_PREF = "_";
const APP_ROUTE_NAME = "_layout"; export const APP_ROUTE_NAME = "_layout";
/** /**
* *

View File

@ -1,12 +1,12 @@
import { defineStore } from "pinia";
import { MenuItem } from "@/router"; import { MenuItem } from "@/router";
import { defineStore } from "pinia";
export const useMenuStore = defineStore({ export const useMenuStore = defineStore({
id: "menu", id: "menu",
state: (): MenuStore => { state: (): MenuStore => {
return { return {
menus: [], menus: [],
home: "/", home: "",
}; };
}, },
actions: { actions: {
@ -16,6 +16,14 @@ export const useMenuStore = defineStore({
setMenus(menus: MenuItem[]) { setMenus(menus: MenuItem[]) {
this.menus = menus; this.menus = menus;
}, },
/**
*
* @param path
*/
setHome(path: string) {
this.home = path;
}
}, },
}); });

9
src/types/env.d.ts vendored
View File

@ -1,4 +1,3 @@
import 'vite';
interface ImportMetaEnv { interface ImportMetaEnv {
/** /**
@ -25,6 +24,14 @@ interface ImportMetaEnv {
* *
*/ */
VITE_PORT: number; VITE_PORT: number;
/**
*
*/
VITE_HOME_PATH: string;
/**
*
*/
VITE_HISTORY: "web" | "hash";
} }
interface ImportMeta { interface ImportMeta {

View File

@ -33,3 +33,29 @@ export function treeEach(tree: any[], fn: (item: any) => void, before = true) {
!before && fn(item); !before && fn(item);
} }
} }
/**
*
* @param tree
* @param fn
* @returns
*/
export function treeFind<T extends { children?: T[]; [key: string]: any } = any>(
tree: T[],
fn: (item: T) => boolean
): T | null {
let data: T | null = null;
for (const item of tree) {
if (fn(item)) {
data = item;
break;
}
if (item.children) {
data = treeFind(item.children, fn);
if (data) {
break;
}
}
}
return data;
}