update 优化 loadView 预建 Map 查找表,路由组件解析从 O(n) 降为 O(1)

update 优化 generateRoutes 三次 JSON 序列化改为 structuredClone
fix 修复 layout watchEffect 改为 watch(width) 消除循环依赖
fix 修复 useDict 返回 reactive 对象保持响应式,增加并发请求去重
fix 修复 FileUpload/ImageUpload headers 改为 computed 避免 token 过期
update 优化 debounce 箭头函数改为 function 声明修复 this 绑定丢失
fix 修复 duplicateRouteChecker 修复 route.name 为空时的空指针崩溃
fix 修复 AppMain 动画随机选择 Math.round 改为 Math.floor 防止越界
fix 修复 TagsView 关闭页签时始终清除缓存,修复动态路由内存泄漏
This commit is contained in:
疯狂的狮子Li
2026-04-09 11:24:02 +08:00
parent a2d9a6d5f2
commit fb5b1ed1bd
9 changed files with 70 additions and 66 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ const uploadList = ref<any[]>([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API; const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址 const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
const headers = ref(globalHeaders()); const headers = computed(() => globalHeaders());
const fileList = ref<any[]>([]); const fileList = ref<any[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize)); const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
+1 -1
View File
@@ -83,7 +83,7 @@ const dialogVisible = ref(false);
const baseUrl = import.meta.env.VITE_APP_BASE_API; const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址 const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址
const headers = ref(globalHeaders()); const headers = computed(() => globalHeaders());
const fileList = ref<any[]>([]); const fileList = ref<any[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize)); const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
+1 -1
View File
@@ -27,7 +27,7 @@ watch(
(val: boolean) => { (val: boolean) => {
if (val) { if (val) {
animate.value = proxy?.animate.animateList[ animate.value = proxy?.animate.animateList[
Math.round(Math.random() * proxy?.animate.animateList.length) Math.floor(Math.random() * proxy?.animate.animateList.length)
] as string; ] as string;
} else { } else {
animate.value = proxy?.animate.defaultAnimate as string; animate.value = proxy?.animate.defaultAnimate as string;
+3 -6
View File
@@ -45,17 +45,14 @@ const classObj = computed(() => ({
const { width } = useWindowSize(); const { width } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design const WIDTH = 992; // refer to Bootstrap's responsive design
watchEffect(() => { watch(width, (w) => {
if (device.value === 'mobile') { if (w - 1 < WIDTH) {
useAppStore().closeSideBar({ withoutAnimation: false });
}
if (width.value - 1 < WIDTH) {
useAppStore().toggleDevice('mobile'); useAppStore().toggleDevice('mobile');
useAppStore().closeSideBar({ withoutAnimation: true }); useAppStore().closeSideBar({ withoutAnimation: true });
} else { } else {
useAppStore().toggleDevice('desktop'); useAppStore().toggleDevice('desktop');
} }
}); }, { immediate: true });
const settingRef = ref<InstanceType<typeof Settings>>(); const settingRef = ref<InstanceType<typeof Settings>>();
+18 -17
View File
@@ -10,8 +10,15 @@ import InnerLink from '@/layout/components/InnerLink/index.vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { createCustomNameComponent } from '@/utils/createCustomNameComponent'; import { createCustomNameComponent } from '@/utils/createCustomNameComponent';
// 匹配views里面所有的.vue文件 // 匹配views里面所有的.vue文件,预建查找表避免每次 O(n) 扫描
const modules = import.meta.glob('./../../views/**/*.vue'); const modules = import.meta.glob('./../../views/**/*.vue');
const viewModuleMap = new Map<string, () => Promise<any>>();
for (const path in modules) {
const viewsIndex = path.indexOf('/views/');
if (viewsIndex === -1) continue;
const dir = path.substring(viewsIndex + 7, path.lastIndexOf('.vue'));
viewModuleMap.set(dir, modules[path] as () => Promise<any>);
}
export const usePermissionStore = defineStore('permission', () => { export const usePermissionStore = defineStore('permission', () => {
const routes = ref<RouteRecordRaw[]>([]); const routes = ref<RouteRecordRaw[]>([]);
const addRoutes = ref<RouteRecordRaw[]>([]); const addRoutes = ref<RouteRecordRaw[]>([]);
@@ -48,10 +55,9 @@ export const usePermissionStore = defineStore('permission', () => {
const generateRoutes = async (): Promise<RouteRecordRaw[]> => { const generateRoutes = async (): Promise<RouteRecordRaw[]> => {
const res = await getRouters(); const res = await getRouters();
const data = Array.isArray(res.data) ? res.data : []; const data = Array.isArray(res.data) ? res.data : [];
const text = JSON.stringify(data); const sdata = structuredClone(data);
const sdata = JSON.parse(text); const rdata = structuredClone(data);
const rdata = JSON.parse(text); const defaultData = structuredClone(data);
const defaultData = JSON.parse(text);
const sidebarRoutes = filterAsyncRouter(sdata); const sidebarRoutes = filterAsyncRouter(sdata);
const rewriteRoutes = filterAsyncRouter(rdata, undefined, true); const rewriteRoutes = filterAsyncRouter(rdata, undefined, true);
const defaultRoutes = filterAsyncRouter(defaultData); const defaultRoutes = filterAsyncRouter(defaultData);
@@ -149,17 +155,11 @@ export const filterDynamicRoutes = (routes: RouteRecordRaw[]) => {
}; };
export const loadView = (view: any, name: string) => { export const loadView = (view: any, name: string) => {
let res; const loader = viewModuleMap.get(view);
for (const path in modules) { if (loader) {
const viewsIndex = path.indexOf('/views/'); return createCustomNameComponent(loader, { name });
let dir = path.substring(viewsIndex + 7);
dir = dir.substring(0, dir.lastIndexOf('.vue'));
if (dir === view) {
res = createCustomNameComponent(modules[path], { name });
return res;
}
} }
return res; return undefined;
}; };
// 非setup // 非setup
@@ -197,7 +197,8 @@ function duplicateRouteChecker(localRoutes: Route[], routes: Route[]) {
const nameList: string[] = []; const nameList: string[] = [];
allRoutes.forEach(route => { allRoutes.forEach(route => {
const name = route.name?.toString() ?? ''; const name = route.name?.toString() ?? '';
if (name && nameList.includes(name)) { if (!name) return;
if (nameList.includes(name)) {
const message = `路由名称: [${name}] 重复, 会造成 404`; const message = `路由名称: [${name}] 重复, 会造成 404`;
console.error(message); console.error(message);
ElNotification({ ElNotification({
@@ -207,6 +208,6 @@ function duplicateRouteChecker(localRoutes: Route[], routes: Route[]) {
}); });
return; return;
} }
nameList.push(route.name.toString()); nameList.push(name);
}); });
} }
+1 -7
View File
@@ -143,9 +143,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
}> => { }> => {
return new Promise(resolve => { return new Promise(resolve => {
delVisitedView(view); delVisitedView(view);
if (!isDynamicRoute(view)) { delCachedView(view);
delCachedView(view);
}
resolve({ resolve({
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[], visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
cachedViews: [...cachedViews.value] cachedViews: [...cachedViews.value]
@@ -309,10 +307,6 @@ export const useTagsViewStore = defineStore('tagsView', () => {
} }
}; };
const isDynamicRoute = (view: RouteLocationNormalized): boolean => {
return view.matched.some(m => m.path.includes(':'));
};
return { return {
visitedViews, visitedViews,
cachedViews, cachedViews,
+3
View File
@@ -4,6 +4,9 @@ declare module '*.vue' {
export default Component; export default Component;
} }
declare module 'virtual:svg-icons-register' {}
declare module 'virtual:*' {}
// 环境变量 // 环境变量
interface ImportMetaEnv { interface ImportMetaEnv {
VITE_APP_TITLE: string; VITE_APP_TITLE: string;
+24 -16
View File
@@ -1,31 +1,39 @@
import { getDicts } from '@/api/system/dict/data'; import { getDicts } from '@/api/system/dict/data';
import { useDictStore } from '@/store/modules/dict'; import { useDictStore } from '@/store/modules/dict';
const pendingRequests = new Map<string, Promise<DictDataOption[]>>();
/** /**
* 获取字典数据 * 获取字典数据
*/ */
export const useDict = (...args: string[]): { [key: string]: DictDataOption[] } => { export const useDict = (...args: string[]): { [key: string]: DictDataOption[] } => {
const res = ref<{ const res = reactive<{ [key: string]: DictDataOption[] }>({});
[key: string]: DictDataOption[];
}>({});
args.forEach(async dictType => { args.forEach(async dictType => {
res.value[dictType] = []; res[dictType] = [];
const dicts = useDictStore().getDict(dictType); const dicts = useDictStore().getDict(dictType);
if (dicts) { if (dicts) {
res.value[dictType] = dicts; res[dictType] = dicts;
} else { } else {
await getDicts(dictType).then(resp => { if (!pendingRequests.has(dictType)) {
res.value[dictType] = resp.data.map( const request = getDicts(dictType)
(p): DictDataOption => ({ .then(resp => {
label: p.dictLabel, const data = resp.data.map(
value: p.dictValue, (p): DictDataOption => ({
elTagType: p.listClass, label: p.dictLabel,
elTagClass: p.cssClass value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass
})
);
useDictStore().setDict(dictType, data);
return data;
}) })
); .finally(() => pendingRequests.delete(dictType));
useDictStore().setDict(dictType, res.value[dictType]); pendingRequests.set(dictType, request);
}); }
res[dictType] = await pendingRequests.get(dictType)!;
} }
}); });
return res.value; return res;
}; };
+18 -17
View File
@@ -204,39 +204,40 @@ export const getTime = (type: string) => {
* @param {boolean} immediate * @param {boolean} immediate
* @return {*} * @return {*}
*/ */
export const debounce = (func: any, wait: number, immediate: boolean) => { export function debounce(func: (...args: any[]) => any, wait: number, immediate: boolean) {
let timeout: any, args: any, context: any, timestamp: any, result: any; let timeout: ReturnType<typeof setTimeout> | null = null;
let lastArgs: any[] | null = null;
let lastContext: any = null;
let timestamp = 0;
let result: any;
const later = function () { function later() {
// 据上一次触发时间间隔 const last = Date.now() - timestamp;
const last = +new Date() - timestamp;
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) { if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last); timeout = setTimeout(later, wait - last);
} else { } else {
timeout = null; timeout = null;
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) { if (!immediate) {
result = func.apply(context, args); result = func.apply(lastContext, lastArgs!);
if (!timeout) context = args = null; if (!timeout) lastContext = lastArgs = null;
} }
} }
}; }
return (...args: any) => { return function (this: any, ...args: any[]) {
context = this; lastContext = this;
timestamp = +new Date(); lastArgs = args;
timestamp = Date.now();
const callNow = immediate && !timeout; const callNow = immediate && !timeout;
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait); if (!timeout) timeout = setTimeout(later, wait);
if (callNow) { if (callNow) {
result = func.apply(context, args); result = func.apply(lastContext, lastArgs);
context = args = null; lastContext = lastArgs = null;
} }
return result; return result;
}; };
}; }
/** /**
* This is just a simple version of deep copy * This is just a simple version of deep copy