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 uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
const headers = ref(globalHeaders());
const headers = computed(() => globalHeaders());
const fileList = ref<any[]>([]);
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 uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址
const headers = ref(globalHeaders());
const headers = computed(() => globalHeaders());
const fileList = ref<any[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
+1 -1
View File
@@ -27,7 +27,7 @@ watch(
(val: boolean) => {
if (val) {
animate.value = proxy?.animate.animateList[
Math.round(Math.random() * proxy?.animate.animateList.length)
Math.floor(Math.random() * proxy?.animate.animateList.length)
] as string;
} else {
animate.value = proxy?.animate.defaultAnimate as string;
+3 -6
View File
@@ -45,17 +45,14 @@ const classObj = computed(() => ({
const { width } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
watchEffect(() => {
if (device.value === 'mobile') {
useAppStore().closeSideBar({ withoutAnimation: false });
}
if (width.value - 1 < WIDTH) {
watch(width, (w) => {
if (w - 1 < WIDTH) {
useAppStore().toggleDevice('mobile');
useAppStore().closeSideBar({ withoutAnimation: true });
} else {
useAppStore().toggleDevice('desktop');
}
});
}, { immediate: true });
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 { createCustomNameComponent } from '@/utils/createCustomNameComponent';
// 匹配views里面所有的.vue文件
// 匹配views里面所有的.vue文件,预建查找表避免每次 O(n) 扫描
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', () => {
const routes = ref<RouteRecordRaw[]>([]);
const addRoutes = ref<RouteRecordRaw[]>([]);
@@ -48,10 +55,9 @@ export const usePermissionStore = defineStore('permission', () => {
const generateRoutes = async (): Promise<RouteRecordRaw[]> => {
const res = await getRouters();
const data = Array.isArray(res.data) ? res.data : [];
const text = JSON.stringify(data);
const sdata = JSON.parse(text);
const rdata = JSON.parse(text);
const defaultData = JSON.parse(text);
const sdata = structuredClone(data);
const rdata = structuredClone(data);
const defaultData = structuredClone(data);
const sidebarRoutes = filterAsyncRouter(sdata);
const rewriteRoutes = filterAsyncRouter(rdata, undefined, true);
const defaultRoutes = filterAsyncRouter(defaultData);
@@ -149,17 +155,11 @@ export const filterDynamicRoutes = (routes: RouteRecordRaw[]) => {
};
export const loadView = (view: any, name: string) => {
let res;
for (const path in modules) {
const viewsIndex = path.indexOf('/views/');
let dir = path.substring(viewsIndex + 7);
dir = dir.substring(0, dir.lastIndexOf('.vue'));
if (dir === view) {
res = createCustomNameComponent(modules[path], { name });
return res;
}
const loader = viewModuleMap.get(view);
if (loader) {
return createCustomNameComponent(loader, { name });
}
return res;
return undefined;
};
// 非setup
@@ -197,7 +197,8 @@ function duplicateRouteChecker(localRoutes: Route[], routes: Route[]) {
const nameList: string[] = [];
allRoutes.forEach(route => {
const name = route.name?.toString() ?? '';
if (name && nameList.includes(name)) {
if (!name) return;
if (nameList.includes(name)) {
const message = `路由名称: [${name}] 重复, 会造成 404`;
console.error(message);
ElNotification({
@@ -207,6 +208,6 @@ function duplicateRouteChecker(localRoutes: Route[], routes: Route[]) {
});
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 => {
delVisitedView(view);
if (!isDynamicRoute(view)) {
delCachedView(view);
}
delCachedView(view);
resolve({
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
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 {
visitedViews,
cachedViews,
+3
View File
@@ -4,6 +4,9 @@ declare module '*.vue' {
export default Component;
}
declare module 'virtual:svg-icons-register' {}
declare module 'virtual:*' {}
// 环境变量
interface ImportMetaEnv {
VITE_APP_TITLE: string;
+24 -16
View File
@@ -1,31 +1,39 @@
import { getDicts } from '@/api/system/dict/data';
import { useDictStore } from '@/store/modules/dict';
const pendingRequests = new Map<string, Promise<DictDataOption[]>>();
/**
* 获取字典数据
*/
export const useDict = (...args: string[]): { [key: string]: DictDataOption[] } => {
const res = ref<{
[key: string]: DictDataOption[];
}>({});
const res = reactive<{ [key: string]: DictDataOption[] }>({});
args.forEach(async dictType => {
res.value[dictType] = [];
res[dictType] = [];
const dicts = useDictStore().getDict(dictType);
if (dicts) {
res.value[dictType] = dicts;
res[dictType] = dicts;
} else {
await getDicts(dictType).then(resp => {
res.value[dictType] = resp.data.map(
(p): DictDataOption => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass
if (!pendingRequests.has(dictType)) {
const request = getDicts(dictType)
.then(resp => {
const data = resp.data.map(
(p): DictDataOption => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass
})
);
useDictStore().setDict(dictType, data);
return data;
})
);
useDictStore().setDict(dictType, res.value[dictType]);
});
.finally(() => pendingRequests.delete(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
* @return {*}
*/
export const debounce = (func: any, wait: number, immediate: boolean) => {
let timeout: any, args: any, context: any, timestamp: any, result: any;
export function debounce(func: (...args: any[]) => any, wait: number, immediate: boolean) {
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 () {
// 据上一次触发时间间隔
const last = +new Date() - timestamp;
function later() {
const last = Date.now() - timestamp;
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
result = func.apply(lastContext, lastArgs!);
if (!timeout) lastContext = lastArgs = null;
}
}
};
}
return (...args: any) => {
context = this;
timestamp = +new Date();
return function (this: any, ...args: any[]) {
lastContext = this;
lastArgs = args;
timestamp = Date.now();
const callNow = immediate && !timeout;
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
result = func.apply(lastContext, lastArgs);
lastContext = lastArgs = null;
}
return result;
};
};
}
/**
* This is just a simple version of deep copy