From fb5b1ed1bd2a64f54e2374b046c4a99c8b4f7c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Thu, 9 Apr 2026 11:24:02 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96=20loadView=20?= =?UTF-8?q?=E9=A2=84=E5=BB=BA=20Map=20=E6=9F=A5=E6=89=BE=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E7=BB=84=E4=BB=B6=E8=A7=A3=E6=9E=90=E4=BB=8E?= =?UTF-8?q?=20O(n)=20=E9=99=8D=E4=B8=BA=20O(1)=20update=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20generateRoutes=20=E4=B8=89=E6=AC=A1=20JSON=20?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E6=94=B9=E4=B8=BA=20structuredClone?= =?UTF-8?q?=20fix=20=E4=BF=AE=E5=A4=8D=20layout=20watchEffect=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20watch(width)=20=E6=B6=88=E9=99=A4=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=20fix=20=E4=BF=AE=E5=A4=8D=20useDict=20?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=20reactive=20=E5=AF=B9=E8=B1=A1=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E5=93=8D=E5=BA=94=E5=BC=8F=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E8=AF=B7=E6=B1=82=E5=8E=BB=E9=87=8D=20fix=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20FileUpload/ImageUpload=20headers=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20computed=20=E9=81=BF=E5=85=8D=20token=20?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=20update=20=E4=BC=98=E5=8C=96=20debounce=20?= =?UTF-8?q?=E7=AE=AD=E5=A4=B4=E5=87=BD=E6=95=B0=E6=94=B9=E4=B8=BA=20functi?= =?UTF-8?q?on=20=E5=A3=B0=E6=98=8E=E4=BF=AE=E5=A4=8D=20this=20=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=E4=B8=A2=E5=A4=B1=20fix=20=E4=BF=AE=E5=A4=8D=20duplic?= =?UTF-8?q?ateRouteChecker=20=E4=BF=AE=E5=A4=8D=20route.name=20=E4=B8=BA?= =?UTF-8?q?=E7=A9=BA=E6=97=B6=E7=9A=84=E7=A9=BA=E6=8C=87=E9=92=88=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=20fix=20=E4=BF=AE=E5=A4=8D=20AppMain=20=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E9=9A=8F=E6=9C=BA=E9=80=89=E6=8B=A9=20Math.round=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20Math.floor=20=E9=98=B2=E6=AD=A2=E8=B6=8A?= =?UTF-8?q?=E7=95=8C=20fix=20=E4=BF=AE=E5=A4=8D=20TagsView=20=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E9=A1=B5=E7=AD=BE=E6=97=B6=E5=A7=8B=E7=BB=88=E6=B8=85?= =?UTF-8?q?=E9=99=A4=E7=BC=93=E5=AD=98=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=B7=AF=E7=94=B1=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/FileUpload/index.vue | 2 +- src/components/ImageUpload/index.vue | 2 +- src/layout/components/AppMain.vue | 2 +- src/layout/index.vue | 9 +++---- src/store/modules/permission.ts | 35 ++++++++++++------------ src/store/modules/tagsView.ts | 8 +----- src/types/env.d.ts | 3 +++ src/utils/dict.ts | 40 +++++++++++++++++----------- src/utils/index.ts | 35 ++++++++++++------------ 9 files changed, 70 insertions(+), 66 deletions(-) diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue index d517845..6e9669a 100644 --- a/src/components/FileUpload/index.vue +++ b/src/components/FileUpload/index.vue @@ -75,7 +75,7 @@ const uploadList = ref([]); 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([]); const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize)); diff --git a/src/components/ImageUpload/index.vue b/src/components/ImageUpload/index.vue index dbca1db..b9788d8 100644 --- a/src/components/ImageUpload/index.vue +++ b/src/components/ImageUpload/index.vue @@ -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([]); const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize)); diff --git a/src/layout/components/AppMain.vue b/src/layout/components/AppMain.vue index 82fb400..6bfaffa 100644 --- a/src/layout/components/AppMain.vue +++ b/src/layout/components/AppMain.vue @@ -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; diff --git a/src/layout/index.vue b/src/layout/index.vue index 3fd53ec..142ed67 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -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>(); diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts index 706cbf8..01ec796 100644 --- a/src/store/modules/permission.ts +++ b/src/store/modules/permission.ts @@ -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 Promise>(); +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); +} export const usePermissionStore = defineStore('permission', () => { const routes = ref([]); const addRoutes = ref([]); @@ -48,10 +55,9 @@ export const usePermissionStore = defineStore('permission', () => { const generateRoutes = async (): Promise => { 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); }); } diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts index f934ec9..7ed523e 100644 --- a/src/store/modules/tagsView.ts +++ b/src/store/modules/tagsView.ts @@ -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, diff --git a/src/types/env.d.ts b/src/types/env.d.ts index e6262d5..df733df 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -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; diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 5cf7cf7..b2bf87d 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -1,31 +1,39 @@ import { getDicts } from '@/api/system/dict/data'; import { useDictStore } from '@/store/modules/dict'; + +const pendingRequests = new Map>(); + /** * 获取字典数据 */ 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; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index e9222fc..be2be50 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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 | 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