fix: 🐛 (keep-alive)重构缓存机制
- 移除 KeepCache 组件,直接在 AppMain 中实现缓存逻辑 - 将 fullPath 做为组件缓存的 name
This commit is contained in:
@@ -1,18 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view>
|
|
||||||
<template #default="{ Component, route }">
|
|
||||||
<transition enter-active-class="animate__animated animate__fadeIn" mode="out-in">
|
|
||||||
<keep-alive :include="tagsViewStore.cachedViews">
|
|
||||||
<component :is="Component" :key="route.path" />
|
|
||||||
</keep-alive>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
</router-view>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({ name: "KeepCache" });
|
|
||||||
|
|
||||||
import { useTagsViewStore } from "@/store";
|
|
||||||
|
|
||||||
const tagsViewStore = useTagsViewStore();
|
|
||||||
</script>
|
|
||||||
@@ -1,13 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="app-main" :style="{ height: appMainHeight }">
|
<section class="app-main" :style="{ height: appMainHeight }">
|
||||||
<KeepCache />
|
<router-view>
|
||||||
|
<template #default="{ Component, route }">
|
||||||
|
<transition enter-active-class="animate__animated animate__fadeIn" mode="out-in">
|
||||||
|
<keep-alive :include="cachedViews">
|
||||||
|
<component :is="currentComponent(Component, route)" :key="route.fullPath" />
|
||||||
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
</router-view>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSettingsStore } from "@/store";
|
import { type RouteLocationNormalized } from "vue-router";
|
||||||
|
import { useSettingsStore, useTagsViewStore } from "@/store";
|
||||||
import variables from "@/styles/variables.module.scss";
|
import variables from "@/styles/variables.module.scss";
|
||||||
import KeepCache from "@/components/KeepCache/index.vue";
|
import Error404 from "@/views/error/404.vue";
|
||||||
|
|
||||||
|
const { cachedViews } = toRefs(useTagsViewStore());
|
||||||
|
|
||||||
|
// 当前组件
|
||||||
|
const wrapperMap = new Map<string, Component>();
|
||||||
|
const currentComponent = (component: Component, route: RouteLocationNormalized) => {
|
||||||
|
if (!component) return;
|
||||||
|
|
||||||
|
const { fullPath: componentName } = route; // 使用路由路径作为组件名称
|
||||||
|
let wrapper = wrapperMap.get(componentName);
|
||||||
|
|
||||||
|
if (!wrapper) {
|
||||||
|
wrapper = {
|
||||||
|
name: componentName,
|
||||||
|
render: () => {
|
||||||
|
try {
|
||||||
|
return h(component);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error rendering component for route: ${componentName}`, error);
|
||||||
|
return h(Error404);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
wrapperMap.set(componentName, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加组件数量限制
|
||||||
|
if (wrapperMap.size > 100) {
|
||||||
|
const firstKey = wrapperMap.keys().next().value;
|
||||||
|
if (firstKey) {
|
||||||
|
wrapperMap.delete(firstKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(wrapper);
|
||||||
|
};
|
||||||
|
|
||||||
const appMainHeight = computed(() => {
|
const appMainHeight = computed(() => {
|
||||||
if (useSettingsStore().showTagsView) {
|
if (useSettingsStore().showTagsView) {
|
||||||
|
|||||||
@@ -86,14 +86,7 @@ const route = useRoute();
|
|||||||
const permissionStore = usePermissionStore();
|
const permissionStore = usePermissionStore();
|
||||||
const tagsViewStore = useTagsViewStore();
|
const tagsViewStore = useTagsViewStore();
|
||||||
|
|
||||||
const visitedViews = ref<TagView[]>([]);
|
const { visitedViews } = storeToRefs(tagsViewStore);
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
visitedViews.value = tagsViewStore.visitedViews;
|
|
||||||
const names = visitedViews.value.map((item) => item.name).filter(Boolean);
|
|
||||||
|
|
||||||
tagsViewStore.setCacheRoutes(names, permissionStore.allCacheRoutes);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 当前选中的标签
|
// 当前选中的标签
|
||||||
const selectedTag = ref<TagView | null>(null);
|
const selectedTag = ref<TagView | null>(null);
|
||||||
|
|||||||
@@ -15,21 +15,20 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
// 动态路由是否已生成
|
// 动态路由是否已生成
|
||||||
const isDynamicRoutesGenerated = ref(false);
|
const isDynamicRoutesGenerated = ref(false);
|
||||||
|
|
||||||
const allCacheRoutes = ref<string[][]>([]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成动态路由
|
* 生成动态路由
|
||||||
*/
|
*/
|
||||||
async function generateRoutes(): Promise<RouteRecordRaw[]> {
|
async function generateRoutes(): Promise<RouteRecordRaw[]> {
|
||||||
try {
|
try {
|
||||||
const data = await MenuAPI.getRoutes(); // 获取当前登录人拥有的菜单路由
|
const data = await MenuAPI.getRoutes(); // 获取当前登录人拥有的菜单路由
|
||||||
const dynamicRoutes = parseDynamicRoutes(data);
|
const dynamicRoutes = parseDynamicRoutes(processRoutes(data));
|
||||||
|
|
||||||
routes.value = [...constantRoutes, ...dynamicRoutes];
|
routes.value = [...constantRoutes, ...dynamicRoutes];
|
||||||
|
|
||||||
setAllCacheRoutes(routes.value);
|
|
||||||
isDynamicRoutesGenerated.value = true;
|
isDynamicRoutesGenerated.value = true;
|
||||||
|
|
||||||
|
console.log("dynamicRoutes", dynamicRoutes);
|
||||||
|
|
||||||
return dynamicRoutes;
|
return dynamicRoutes;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to generate routes:", error);
|
console.error("❌ Failed to generate routes:", error);
|
||||||
@@ -64,33 +63,10 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
isDynamicRoutesGenerated.value = false;
|
isDynamicRoutesGenerated.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有的缓存路由
|
|
||||||
* @param userRoutes 用户路由配置
|
|
||||||
*/
|
|
||||||
const setAllCacheRoutes = (userRoutes: RouteRecordRaw[]) => {
|
|
||||||
if (!userRoutes?.length) {
|
|
||||||
allCacheRoutes.value = [];
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: string[][] = [];
|
|
||||||
|
|
||||||
userRoutes.forEach((route) => {
|
|
||||||
if (route.children?.length) {
|
|
||||||
traverseRoutes(route.children, [], result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
allCacheRoutes.value = result;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
routes,
|
routes,
|
||||||
mixLayoutSideMenus,
|
mixLayoutSideMenus,
|
||||||
isDynamicRoutesGenerated,
|
isDynamicRoutesGenerated,
|
||||||
allCacheRoutes,
|
|
||||||
generateRoutes,
|
generateRoutes,
|
||||||
setMixLayoutSideMenus,
|
setMixLayoutSideMenus,
|
||||||
resetRouter,
|
resetRouter,
|
||||||
@@ -109,12 +85,13 @@ const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
|
|||||||
rawRoutes.forEach((route) => {
|
rawRoutes.forEach((route) => {
|
||||||
const normalizedRoute = { ...route } as RouteRecordRaw;
|
const normalizedRoute = { ...route } as RouteRecordRaw;
|
||||||
|
|
||||||
|
// console.log();
|
||||||
|
|
||||||
// 处理组件路径
|
// 处理组件路径
|
||||||
normalizedRoute.component =
|
normalizedRoute.component =
|
||||||
normalizedRoute.component?.toString() === "Layout"
|
normalizedRoute.component?.toString() === "Layout"
|
||||||
? Layout
|
? Layout
|
||||||
: modules[`../../views/${normalizedRoute.component}.vue`] ||
|
: modules[`../../views/${normalizedRoute.component}.vue`];
|
||||||
modules["../../views/error-page/404.vue"];
|
|
||||||
|
|
||||||
// 递归解析子路由
|
// 递归解析子路由
|
||||||
if (normalizedRoute.children) {
|
if (normalizedRoute.children) {
|
||||||
@@ -128,24 +105,31 @@ const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 遍历路由树收集缓存路由
|
* 去除中间层的路由 `component` 属性
|
||||||
* @param nodes 路由节点
|
* @param routes 路由数组
|
||||||
* @param path 当前路径
|
* @param isTopLevel 是否是顶层路由
|
||||||
* @param result 结果数组
|
|
||||||
*/
|
*/
|
||||||
const traverseRoutes = (nodes: RouteRecordRaw[], path: string[], result: string[][]) => {
|
const processRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteVO[] => {
|
||||||
nodes.forEach((node) => {
|
return routes.map((route) => {
|
||||||
const newPath: string[] = node.name ? [...path, String(node.name)] : [...path];
|
// 创建新对象
|
||||||
|
const newRoute: RouteVO = {
|
||||||
|
path: route.path,
|
||||||
|
name: route.name,
|
||||||
|
children: route.children,
|
||||||
|
meta: { ...route.meta },
|
||||||
|
};
|
||||||
|
|
||||||
// 叶子节点且需要缓存
|
// 如果是顶层或者component不是"Layout",保留component属性
|
||||||
if (!node.children?.length && node.meta?.keepAlive) {
|
if (isTopLevel || route.component !== "Layout") {
|
||||||
result.push(newPath);
|
newRoute.component = route.component;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归处理子节点
|
// 递归处理children,标记为非顶层
|
||||||
if (node.children?.length) {
|
if (route.children && route.children.length > 0) {
|
||||||
traverseRoutes(node.children, newPath, result);
|
newRoute.children = processRoutes(route.children, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return newRoute;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,16 +27,15 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
/**
|
/**
|
||||||
* 添加缓存视图到缓存视图列表中
|
* 添加缓存视图到缓存视图列表中
|
||||||
*/
|
*/
|
||||||
function addCachedView(view: TagView) {
|
function addCachedView({ fullPath, keepAlive }: TagView) {
|
||||||
const viewName = view.name;
|
|
||||||
// 如果缓存视图名称已经存在于缓存视图列表中,则不再添加
|
// 如果缓存视图名称已经存在于缓存视图列表中,则不再添加
|
||||||
if (cachedViews.value.includes(viewName)) {
|
if (cachedViews.value.includes(fullPath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
|
// 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中
|
||||||
if (view.keepAlive) {
|
if (keepAlive) {
|
||||||
cachedViews.value.push(viewName);
|
cachedViews.value.push(fullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +56,9 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function delCachedView(view: TagView) {
|
function delCachedView(view: TagView) {
|
||||||
const viewName = view.name;
|
const { fullPath } = view;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const index = cachedViews.value.indexOf(viewName);
|
const index = cachedViews.value.indexOf(fullPath);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
cachedViews.value.splice(index, 1);
|
cachedViews.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -76,9 +75,9 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function delOtherCachedViews(view: TagView) {
|
function delOtherCachedViews(view: TagView) {
|
||||||
const viewName = view.name as string;
|
const { fullPath } = view;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const index = cachedViews.value.indexOf(viewName);
|
const index = cachedViews.value.indexOf(fullPath);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
cachedViews.value = cachedViews.value.slice(index, index + 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -136,7 +135,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheIndex = cachedViews.value.indexOf(item.name);
|
const cacheIndex = cachedViews.value.indexOf(item.fullPath);
|
||||||
if (cacheIndex > -1) {
|
if (cacheIndex > -1) {
|
||||||
cachedViews.value.splice(cacheIndex, 1);
|
cachedViews.value.splice(cacheIndex, 1);
|
||||||
}
|
}
|
||||||
@@ -158,6 +157,11 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
if (index <= currIndex || item?.affix) {
|
if (index <= currIndex || item?.affix) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const cacheIndex = cachedViews.value.indexOf(item.fullPath);
|
||||||
|
if (cacheIndex > -1) {
|
||||||
|
cachedViews.value.splice(cacheIndex, 1);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
resolve({
|
resolve({
|
||||||
visitedViews: [...visitedViews.value],
|
visitedViews: [...visitedViews.value],
|
||||||
@@ -232,37 +236,6 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setCacheRoutes = (names: string[], allCacheRoutes: string[][]) => {
|
|
||||||
if (!names?.length) {
|
|
||||||
cachedViews.value = [];
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedViews.value = findAndMergeRouteArrays(allCacheRoutes, names);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查找并合并路由数组
|
|
||||||
* @param data 所有缓存路由数据
|
|
||||||
* @param elements 目标元素
|
|
||||||
* @returns 合并后的路由数组
|
|
||||||
*/
|
|
||||||
const findAndMergeRouteArrays = (data: string[][], elements: string[]): string[] => {
|
|
||||||
const foundArrays = elements
|
|
||||||
.map((element) => data.find((arr) => arr.includes(element)))
|
|
||||||
.filter(Boolean) as string[][];
|
|
||||||
|
|
||||||
// 使用Set去重并合并
|
|
||||||
const mergedSet = new Set<string>();
|
|
||||||
|
|
||||||
foundArrays.forEach((arr) => {
|
|
||||||
arr.forEach((item) => mergedSet.add(item));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(mergedSet);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
visitedViews,
|
visitedViews,
|
||||||
cachedViews,
|
cachedViews,
|
||||||
@@ -284,6 +257,5 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
closeCurrentView,
|
closeCurrentView,
|
||||||
isActive,
|
isActive,
|
||||||
toLastView,
|
toLastView,
|
||||||
setCacheRoutes,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div flex flex-col gap-20px>
|
<div class="app-container">
|
||||||
<el-alert :closable="false" title="菜单三级-1" type="error" />
|
<div flex flex-col gap-20px>
|
||||||
<el-input v-model="value" placeholder="缓存测试" />
|
<MultiLevel1 />
|
||||||
<ToDetail />
|
<MultiLevel2 />
|
||||||
|
<el-alert :closable="false" title="菜单三级-1" type="error" />
|
||||||
|
<el-input v-model="value" placeholder="缓存测试" />
|
||||||
|
<ToDetail />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ToDetail from "./detail";
|
import ToDetail from "./detail";
|
||||||
|
import MultiLevel1 from "../../level1.vue";
|
||||||
|
import MultiLevel2 from "../level2.vue";
|
||||||
|
|
||||||
defineOptions({ name: "MultiLevel31" });
|
defineOptions({ name: "MultiLevel31" });
|
||||||
|
|
||||||
const value = ref("");
|
const value = ref("");
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("Multilevel31 onMounted");
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div flex flex-col gap-20px>
|
<div class="app-container">
|
||||||
<el-alert :closable="false" title="菜单三级-2" type="warning" />
|
<div flex flex-col gap-20px>
|
||||||
<el-input v-model="value" placeholder="缓存测试" />
|
<MultiLevel1 />
|
||||||
<ToDetail />
|
<MultiLevel2 />
|
||||||
|
<el-alert :closable="false" title="菜单三级-2" type="warning" />
|
||||||
|
<el-input v-model="value" placeholder="缓存测试" />
|
||||||
|
<ToDetail />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ToDetail from "./detail";
|
import ToDetail from "./detail";
|
||||||
|
import MultiLevel1 from "../../level1.vue";
|
||||||
|
import MultiLevel2 from "../level2.vue";
|
||||||
|
|
||||||
defineOptions({ name: "MultiLevel32" });
|
defineOptions({ name: "MultiLevel32" });
|
||||||
const value = ref("");
|
const value = ref("");
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("Multilevel32 onMounted");
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div flex flex-col gap-20px>
|
<el-alert :closable="false" title="菜单二级" type="success" />
|
||||||
<el-alert :closable="false" title="菜单二级" type="success" />
|
<el-input v-model="value" placeholder="缓存测试" />
|
||||||
<KeepCache />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import KeepCache from "@/components/KeepCache/index.vue";
|
|
||||||
defineOptions({ name: "MultiLevel2" });
|
defineOptions({ name: "MultiLevel2" });
|
||||||
|
|
||||||
|
const value = ref("");
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("MultiLevel2 mounted");
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div p-30px>
|
<el-alert :closable="false" title="菜单一级" />
|
||||||
<el-link
|
|
||||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/multi-level/level1.vue"
|
|
||||||
type="primary"
|
|
||||||
target="_blank"
|
|
||||||
class="mb-10"
|
|
||||||
>
|
|
||||||
示例源码 请点击>>>>
|
|
||||||
</el-link>
|
|
||||||
|
|
||||||
<div flex flex-col gap-20px>
|
|
||||||
<el-alert :closable="false" title="菜单一级" />
|
|
||||||
<KeepCache />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import KeepCache from "@/components/KeepCache/index.vue";
|
|
||||||
defineOptions({ name: "MultiLevel1" });
|
defineOptions({ name: "MultiLevel1" });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("MultiLevel1 mounted");
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user