feat: 优化路由逻辑

master
luoer 2023-11-07 17:49:42 +08:00
parent c648519d42
commit b11d43a0a6
13 changed files with 312 additions and 281 deletions

View File

@ -41,7 +41,7 @@
margin: 0; margin: 0;
margin-top: 20px; margin-top: 20px;
font-size: 22px; font-size: 22px;
font-weight: 300; font-weight: 400;
line-height: 1; line-height: 1;
} }
.loading-tip { .loading-tip {

View File

@ -1,32 +1,139 @@
/**
*
* @see "src/api/instance/useRequest.ts"
*/
export function useRequest<T extends PromiseFn, E = unknown>(fn: T, options: Options<T> = {}) {
type Data = Awaited<ReturnType<T>>;
type Args = Parameters<T>;
const { initialParams, initialData, retry = 0, retryDelay = 0, interval = 0 } = options;
const { onBefore, onSuccess, onError, onFinally } = options;
/**
*
*/
const data = ref<Data | null>(initialData ?? null);
/**
*
*/
const error = ref<E | null>(null);
/**
*
*/
const loading = ref(false);
let isCanceled = false;
let retryCount = 0;
let retryTimer = 0;
let interTimer = 0;
let latestArgs = initialParams ?? [];
const _send = async (...args: Args) => {
try {
onBefore?.(args);
loading.value = true;
const res = await fn(...args);
retryCount = 0;
if (isCanceled) {
return [];
}
onSuccess?.(res);
data.value = res;
error.value = null;
} catch (err: any) {
if (isCanceled) {
return [];
}
onError?.(err);
data.value = null;
error.value = err;
if (retry > 0 && retryCount < retry) {
retryCount++;
retryTimer = setTimeout(() => _send(...args), retryDelay) as any;
}
} finally {
loading.value = false;
if (isCanceled) {
return [];
}
onFinally?.();
if (!retryCount && interval > 0) {
interTimer = setTimeout(() => _send(...args), interval) as any;
}
}
return [error.value, data.value];
};
const clearAllTimer = () => {
clearTimeout(retryTimer);
clearTimeout(interTimer);
};
/**
*
*/
const cancel = () => {
isCanceled = true;
clearAllTimer();
};
/**
*
*/
const send = (...args: Args) => {
isCanceled = false;
retryCount = 0;
latestArgs = args;
clearAllTimer();
return _send(...args);
};
onMounted(() => initialParams && send(...initialParams));
onUnmounted(cancel);
return {
data,
error,
loading,
send,
cancel,
};
}
type PromiseFn = (...args: any[]) => Promise<any>; type PromiseFn = (...args: any[]) => Promise<any>;
type Options<T extends PromiseFn = PromiseFn> = { interface Options<T extends PromiseFn = PromiseFn> {
/**
*
* @description
*/
initialParams?: Parameters<T>;
/**
*
* @description
*/
initialData?: Awaited<ReturnType<T>>;
/** /**
* loading * loading
* @default false
*/ */
toast?: boolean | string; toast?: boolean | string;
/**
*
*/
initialParams?: boolean | Parameters<T>;
/**
*
*/
initialData?: Partial<Awaited<ReturnType<T>>>;
/** /**
* *
* @default 0
*/ */
retry?: number; retry?: number;
/** /**
* (ms) * (ms)
* @default 0
*/ */
retryDelay?: number; retryDelay?: number;
/** /**
* (ms) * (ms)
* @default 0
*/ */
interval?: number; interval?: number;
/** /**
* *
*/ */
onBefore?: (args: Parameters<T>) => void; onBefore?: (args: Parameters<T>) => void;
/** /**
@ -41,139 +148,4 @@ type Options<T extends PromiseFn = PromiseFn> = {
* *
*/ */
onFinally?: () => void; onFinally?: () => void;
};
type State<T extends PromiseFn = PromiseFn, D = Awaited<ReturnType<T>>> = {
/**
*
*/
data: D | undefined;
/**
*
*/
error: unknown;
/**
*
*/
loading: boolean;
/**
*
*/
send: (...args: Parameters<T>) => Promise<[unknown, undefined] | [undefined, D]>;
/**
*
*/
cancel: () => void;
};
const log = (...args: any[]) => {
if (process.env.NODE_ENV === "development") {
console.log(...args);
}
};
/**
*
* @see src/api/instance/useRequest.ts
*/
export function useRequest<T extends PromiseFn>(fn: T, options: Options<T> = {}) {
const {
initialParams,
retry,
retryDelay = 0,
interval,
initialData,
onBefore,
onSuccess,
onError,
onFinally,
} = options;
const state = reactive<State<T>>({
data: initialData,
error: null,
loading: false,
send: null,
cancel: null,
} as any);
const inner = {
canceled: false,
retryCount: 0,
retryTimer: 0 as any,
intervalTimer: 0 as any,
latestParams: (initialParams || []) as any,
clearAllTimer: () => {
inner.retryTimer && clearTimeout(inner.retryTimer);
inner.intervalTimer && clearTimeout(inner.intervalTimer);
},
};
const _send: any = async (...args: Parameters<T>) => {
let data;
let error;
inner.retryCount && log(`retry: ${inner.retryCount}`);
try {
state.loading = true;
onBefore?.(args);
const res = await fn(...args);
inner.retryCount = 0;
if (!inner.canceled) {
onSuccess?.(res.data);
data = res.data;
}
} catch (err) {
if (!inner.canceled) {
error = err;
onError?.(err);
if (retry && retry > 0 && inner.retryCount < retry) {
inner.retryCount++;
inner.retryTimer = setTimeout(() => {
_send(...args);
}, retryDelay);
}
}
} finally {
log("finally");
state.loading = false;
state.error = error;
if (!error) {
state.data = data;
}
if (!inner.canceled) {
onFinally?.();
if (!inner.retryCount && interval && interval > 0) {
inner.intervalTimer = setTimeout(() => {
_send(...args);
}, interval);
}
}
}
return [error, data];
};
state.cancel = () => {
inner.canceled = true;
inner.clearAllTimer();
};
state.send = (...args: Parameters<T>) => {
inner.canceled = false;
inner.retryCount = 0;
inner.latestParams = args;
inner.clearAllTimer();
return _send(...args);
};
onMounted(() => {
if (initialParams) {
state.send(...(Array.isArray(initialParams) ? initialParams : ([] as any)));
}
});
onUnmounted(() => {
state.cancel();
});
return state;
} }

View File

@ -101,7 +101,7 @@ const onSubmitForm = async () => {
try { try {
loading.value = true; loading.value = true;
const res = await api.auth.login(model); const res = await api.auth.login(model);
userStore.setAccessToken(res.data.data as unknown as string); userStore.setAccessToken(res.data.data);
Notification.success({ Notification.success({
title: "登陆提示", title: "登陆提示",
content: `欢迎,您已成功登陆系统!`, content: `欢迎,您已成功登陆系统!`,
@ -124,7 +124,7 @@ const onSubmitForm = async () => {
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
} }
.login-left { .login-left {
background: rgb(var(--primary-6)) url(/src/pages/_login/image-br.svg) no-repeat center center/90% auto; background: rgb(var(--primary-6)) url(./image-br.svg) no-repeat center center/90% auto;
} }
</style> </style>

View File

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

View File

@ -4,7 +4,7 @@ import { appRoutes } from "../routes";
/** /**
* *
*/ */
interface MenuItem { export interface MenuItem {
id: string; id: string;
parentId?: string; parentId?: string;
path: string; path: string;
@ -23,38 +23,32 @@ interface MenuItem {
function routesToItems(routes: RouteRecordRaw[]): MenuItem[] { function routesToItems(routes: RouteRecordRaw[]): MenuItem[] {
const items: MenuItem[] = []; const items: MenuItem[] = [];
routes.forEach((route) => { for (const route of routes) {
const { meta = {}, parentMeta, path } = route as any;
const { title, sort, icon } = meta;
let id = path;
let paths = route.path.split("/"); let paths = route.path.split("/");
let id = route.path;
let parentId = paths.slice(0, -1).join("/"); let parentId = paths.slice(0, -1).join("/");
if (parentMeta) {
if ((route as any).parentMeta) { const { title, icon, sort } = parentMeta;
id = `${route.path}/index`; id = `${path}/index`;
parentId = route.path; parentId = path;
items.push({ items.push({
id: route.path, title,
icon,
sort,
path,
id: path,
parentId: paths.slice(0, -1).join("/"), parentId: paths.slice(0, -1).join("/"),
path: `${route.path}`,
title: (route as any).parentMeta.title,
icon: (route as any).parentMeta.icon,
sort: (route as any).parentMeta.sort,
}); });
} else { } else {
const p = paths.slice(0, -1).join("/"); const p = paths.slice(0, -1).join("/");
if (routes.some((i) => i.path === p && (i as any).parentMeta)) { if (routes.some((i) => i.path === p) && parentMeta) {
parentId = p; parentId = p;
} }
} }
items.push({ id, title, parentId, path, icon, sort });
items.push({ }
id,
parentId,
path: route.path,
sort: route.meta?.sort,
title: route.meta?.title,
icon: route.meta?.icon,
});
});
return items; return items;
} }
@ -68,18 +62,18 @@ function listToTree(list: MenuItem[]) {
const map: Record<string, MenuItem> = {}; const map: Record<string, MenuItem> = {};
const tree: MenuItem[] = []; const tree: MenuItem[] = [];
list.forEach((item) => { for (const item of list) {
map[item.id] = item; map[item.id] = item;
}); }
list.forEach((item) => { for (const item of list) {
const parent = map[item.parentId as string]; const parent = map[item.parentId as string];
if (parent) { if (parent) {
(parent.children || (parent.children = [])).push(item); (parent.children || (parent.children = [])).push(item);
} else { } else {
tree.push(item); tree.push(item);
} }
}); }
return tree; return tree;
} }
@ -102,31 +96,17 @@ function sort<T extends { children?: T[]; [key: string]: any }>(routes: T[], key
}); });
} }
/**
*
* @param routes
* @returns
*/
function transformToMenuItems(routes: RouteRecordRaw[]) {
const menus = routesToItems(routes);
const tree = listToTree(menus);
return sort(tree);
}
/** /**
* *
*/ */
const flatedMenus = routesToItems(appRoutes); export const flatMenus = routesToItems(appRoutes);
/** /**
* *
*/ */
const treeMenus = listToTree(flatedMenus); export const treeMenus = listToTree(flatMenus);
/** /**
* *
*/ */
const menus = sort(treeMenus); export const menus = sort(treeMenus);
export { menus, treeMenus, flatedMenus };
export type { MenuItem };

View File

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

14
src/router/routes/base.ts Normal file
View File

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

View File

@ -1,10 +1,11 @@
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 = "_";
const APP_ROUTE_NAME = "_layout"; const APP_ROUTE_NAME = "_layout";
/** /**
* *
* @description _ * @description _
*/ */
const transformRoutes = (routes: RouteRecordRaw[]) => { const transformRoutes = (routes: RouteRecordRaw[]) => {
@ -12,11 +13,11 @@ const transformRoutes = (routes: RouteRecordRaw[]) => {
const appRoutes: RouteRecordRaw[] = []; const appRoutes: RouteRecordRaw[] = [];
for (const route of routes) { for (const route of routes) {
if ((route.name as string)?.startsWith("_")) { if ((route.name as string)?.startsWith(TOP_ROUTE_PREF)) {
if (route.name === APP_ROUTE_NAME) { if (route.name === APP_ROUTE_NAME) {
route.children = appRoutes; route.children = appRoutes;
} }
route.path = route.path.replace("_", ""); route.path = route.path.replace(TOP_ROUTE_PREF, "");
topRoutes.push(route); topRoutes.push(route);
continue; continue;
} }

View File

@ -1,43 +1,13 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
interface PageTag {
id: string;
title: string;
path: string;
closable?: boolean;
closible?: boolean;
actived?: boolean;
}
export const useAppStore = defineStore({ export const useAppStore = defineStore({
id: "app", id: "app",
state: () => ({ state: (): AppStore => ({
/**
*
*/
isDarkMode: false, isDarkMode: false,
/**
*
*/
title: import.meta.env.VITE_TITLE, title: import.meta.env.VITE_TITLE,
/**
*
*/
subtitle: import.meta.env.VITE_SUBTITLE, subtitle: import.meta.env.VITE_SUBTITLE,
/**
*
*/
pageLoding: false, pageLoding: false,
pageTags: [ pageTags: [],
{
id: "/",
title: "首页",
path: "/",
closable: false,
closible: false,
actived: false,
},
] as PageTag[],
}), }),
actions: { actions: {
/** /**
@ -46,6 +16,7 @@ export const useAppStore = defineStore({
toggleDark() { toggleDark() {
this.isDarkMode ? this.setLight() : this.setDark(); this.isDarkMode ? this.setLight() : this.setDark();
}, },
/** /**
* *
*/ */
@ -54,6 +25,7 @@ export const useAppStore = defineStore({
document.body.classList.remove("dark"); document.body.classList.remove("dark");
this.isDarkMode = false; this.isDarkMode = false;
}, },
/** /**
* *
*/ */
@ -62,12 +34,14 @@ export const useAppStore = defineStore({
document.body.classList.add("dark"); document.body.classList.add("dark");
this.isDarkMode = true; this.isDarkMode = true;
}, },
/** /**
* loading * loading
*/ */
setPageLoading(loading: boolean) { setPageLoading(loading: boolean) {
this.pageLoding = loading; this.pageLoding = loading;
}, },
/** /**
* *
* @param tag * @param tag
@ -83,14 +57,13 @@ export const useAppStore = defineStore({
actived: false, actived: false,
...tag, ...tag,
}); });
console.log(this.pageTags);
}, },
/** /**
* *
* @param tag * @param tag
*/ */
delPageTag(tag: PageTag) { delPageTag(tag: PageTag) {
console.log("del page tag");
const index = this.pageTags.findIndex((i) => i.id === tag.id); const index = this.pageTags.findIndex((i) => i.id === tag.id);
if (index > -1) { if (index > -1) {
this.pageTags.splice(index, 1); this.pageTags.splice(index, 1);
@ -99,3 +72,35 @@ export const useAppStore = defineStore({
}, },
persist: !import.meta.env.DEV, persist: !import.meta.env.DEV,
}); });
interface AppStore {
/**
*
*/
isDarkMode: boolean;
/**
*
*/
title: string;
/**
*
*/
subtitle: string;
/**
*
*/
pageLoding: boolean;
/**
*
*/
pageTags: PageTag[];
}
interface PageTag {
id: string;
title: string;
path: string;
closable?: boolean;
closible?: boolean;
actived?: boolean;
}

31
src/store/menu/index.ts Normal file
View File

@ -0,0 +1,31 @@
import { defineStore } from "pinia";
import { MenuItem } from "@/router";
export const useMenuStore = defineStore({
id: "menu",
state: (): MenuStore => {
return {
menus: [],
home: "/",
};
},
actions: {
/**
*
*/
setMenus(menus: MenuItem[]) {
this.menus = menus;
},
},
});
export interface MenuStore {
/**
*
*/
menus: MenuItem[];
/**
*
*/
home: string;
}

View File

@ -2,4 +2,5 @@ import { createPinia } from "pinia";
import persistedstatePlugin from "pinia-plugin-persistedstate"; import persistedstatePlugin from "pinia-plugin-persistedstate";
export const store = createPinia(); export const store = createPinia();
store.use(persistedstatePlugin); store.use(persistedstatePlugin);

View File

@ -2,31 +2,13 @@ import { defineStore } from "pinia";
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: "user", id: "user",
state: () => { state: (): UserStore => {
return { return {
/**
* ID
*/
id: 0, id: 0,
/**
*
*/
username: "juetan", username: "juetan",
/**
*
*/
nickname: "绝弹", nickname: "绝弹",
/** `
*
*/
avatar: "https://github.com/juetan.png", avatar: "https://github.com/juetan.png",
/**
* JWT
*/
accessToken: "", accessToken: "",
/**
*
*/
refreshToken: undefined, refreshToken: undefined,
}; };
}, },
@ -38,7 +20,11 @@ export const useUserStore = defineStore({
this.accessToken = token; this.accessToken = token;
}, },
setAccessToken(token: string) { /**
* 访
* @param token
*/
setAccessToken(token?: string) {
this.accessToken = token; this.accessToken = token;
}, },
@ -52,13 +38,41 @@ export const useUserStore = defineStore({
/** /**
* *
*/ */
setUser(user: any) { setUser(user: Partial<UserStore>) {
this.id = user.id; const { id, username, nickname, avatar, accessToken } = user;
this.username = user.username; id && (this.id = id);
this.nickname = user.nickname; username && (this.username = username);
this.avatar = user.avatar; nickname && (this.nickname = nickname);
this.accessToken = user.token; avatar && (this.avatar = avatar);
accessToken && (this.accessToken = accessToken);
}, },
}, },
persist: true, persist: true,
}); });
export interface UserStore {
/**
* ID
*/
id: number;
/**
*
*/
username: string;
/**
*
*/
nickname: string;
/**
*
*/
avatar?: string;
/**
* JWT
*/
accessToken?: string;
/**
*
*/
refreshToken?: string;
}

View File

@ -1,7 +1,7 @@
import 'vue-router'; import "vue-router";
declare module 'vue-router' { declare module "vue-router" {
interface RouteRecordRaw { interface RouteRecordSingleView {
parentMeta: { parentMeta: {
/** /**
* *
@ -34,7 +34,7 @@ declare module 'vue-router' {
/** /**
* *
*/ */
hidden?: boolean; hide?: boolean;
/** /**
* *
*/ */