feat: ✨ (keep-alive)优化页面缓存机制
- 重构 AppMain 组件,引入 KeepCache 组件实现统一缓存 - 新增 DemoDetail 组件作为缓存测试页面 - 更新 TagsView 组件,优化缓存路由逻辑 - 修改 permission.store.ts,增加 allCacheRoutes 状态管理 - 更新 tags-view.store.ts,实现缓存路由的动态设置 - 调整多级菜单示例,支持缓存功能
This commit is contained in:
@@ -1,23 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="app-main" :style="{ height: appMainHeight }">
|
<section class="app-main" :style="{ height: appMainHeight }">
|
||||||
<router-view>
|
<KeepCache />
|
||||||
<template #default="{ Component, route }">
|
|
||||||
<transition enter-active-class="animate__animated animate__fadeIn" mode="out-in">
|
|
||||||
<keep-alive :include="cachedViews">
|
|
||||||
<component :is="Component" :key="route.path" />
|
|
||||||
</keep-alive>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
</router-view>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSettingsStore, useTagsViewStore } from "@/store";
|
import { useSettingsStore } from "@/store";
|
||||||
import variables from "@/styles/variables.module.scss";
|
import variables from "@/styles/variables.module.scss";
|
||||||
|
import KeepCache from "@/components/KeepCache/index.vue";
|
||||||
|
|
||||||
// 缓存页面集合
|
|
||||||
const cachedViews = computed(() => useTagsViewStore().cachedViews);
|
|
||||||
const appMainHeight = computed(() => {
|
const appMainHeight = computed(() => {
|
||||||
if (useSettingsStore().showTagsView) {
|
if (useSettingsStore().showTagsView) {
|
||||||
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
|
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
|
||||||
|
|||||||
@@ -77,7 +77,17 @@ const permissionStore = usePermissionStore();
|
|||||||
const tagsViewStore = useTagsViewStore();
|
const tagsViewStore = useTagsViewStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
const { visitedViews } = storeToRefs(tagsViewStore);
|
// const { visitedViews } = storeToRefs(tagsViewStore);
|
||||||
|
|
||||||
|
const visitedViews = ref<TagView[]>([]);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
visitedViews.value = tagsViewStore.visitedViews;
|
||||||
|
const names = visitedViews.value.map((item) => item.name).filter(Boolean);
|
||||||
|
|
||||||
|
tagsViewStore.setCacheRoutes(names, permissionStore.allCacheRoutes);
|
||||||
|
});
|
||||||
|
|
||||||
const layout = computed(() => settingsStore.layout);
|
const layout = computed(() => settingsStore.layout);
|
||||||
|
|
||||||
// 当前选中的标签
|
// 当前选中的标签
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/system/notice/components/MyNotice.vue"),
|
component: () => import("@/views/system/notice/components/MyNotice.vue"),
|
||||||
meta: { title: "我的通知", icon: "user", hidden: true },
|
meta: { title: "我的通知", icon: "user", hidden: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/detail/:id(\\d+)",
|
||||||
|
name: "DemoDetail",
|
||||||
|
component: () => import("@/views/demo/detail.vue"),
|
||||||
|
meta: { title: "详情页缓存", icon: "user", hidden: true, keepAlive: true },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
// 动态路由是否已生成
|
// 动态路由是否已生成
|
||||||
const isDynamicRoutesGenerated = ref(false);
|
const isDynamicRoutesGenerated = ref(false);
|
||||||
|
|
||||||
|
const allCacheRoutes = ref<string[][]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成动态路由
|
* 生成动态路由
|
||||||
*/
|
*/
|
||||||
@@ -24,6 +26,8 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
const dynamicRoutes = parseDynamicRoutes(data);
|
const dynamicRoutes = parseDynamicRoutes(data);
|
||||||
|
|
||||||
routes.value = [...constantRoutes, ...dynamicRoutes];
|
routes.value = [...constantRoutes, ...dynamicRoutes];
|
||||||
|
|
||||||
|
setAllCacheRoutes(routes.value);
|
||||||
isDynamicRoutesGenerated.value = true;
|
isDynamicRoutesGenerated.value = true;
|
||||||
|
|
||||||
return dynamicRoutes;
|
return dynamicRoutes;
|
||||||
@@ -60,10 +64,33 @@ 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,
|
||||||
@@ -100,6 +127,28 @@ const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
|
|||||||
return parsedRoutes;
|
return parsedRoutes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历路由树收集缓存路由
|
||||||
|
* @param nodes 路由节点
|
||||||
|
* @param path 当前路径
|
||||||
|
* @param result 结果数组
|
||||||
|
*/
|
||||||
|
const traverseRoutes = (nodes: RouteRecordRaw[], path: string[], result: string[][]) => {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const newPath: string[] = node.name ? [...path, String(node.name)] : [...path];
|
||||||
|
|
||||||
|
// 叶子节点且需要缓存
|
||||||
|
if (!node.children?.length && node.meta?.keepAlive) {
|
||||||
|
result.push(newPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子节点
|
||||||
|
if (node.children?.length) {
|
||||||
|
traverseRoutes(node.children, newPath, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出此hook函数用于在非组件环境(如其他store、工具函数等)中获取权限store实例
|
* 导出此hook函数用于在非组件环境(如其他store、工具函数等)中获取权限store实例
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
if (view.path.startsWith("/redirect")) {
|
if (view.path.startsWith("/redirect")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (visitedViews.value.some((v) => v.name === view.name)) {
|
if (visitedViews.value.some((v) => v.path === view.path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 如果视图是固定的(affix),则在已访问的视图列表的开头添加
|
// 如果视图是固定的(affix),则在已访问的视图列表的开头添加
|
||||||
@@ -232,6 +232,37 @@ 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,
|
||||||
@@ -253,5 +284,6 @@ export const useTagsViewStore = defineStore("tagsView", () => {
|
|||||||
closeCurrentView,
|
closeCurrentView,
|
||||||
isActive,
|
isActive,
|
||||||
toLastView,
|
toLastView,
|
||||||
|
setCacheRoutes,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
17
src/views/demo/detail.vue
Normal file
17
src/views/demo/detail.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div p-5 flex flex-col gap="20px">
|
||||||
|
<div>params: {{ route.params }}</div>
|
||||||
|
<div>query: {{ route.query }}</div>
|
||||||
|
<el-input v-model="value" placeholder="缓存测试" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({
|
||||||
|
name: "DemoDetail",
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
console.log(route);
|
||||||
|
|
||||||
|
const value = ref("");
|
||||||
|
</script>
|
||||||
34
src/views/demo/multi-level/children/children/detail.ts
Normal file
34
src/views/demo/multi-level/children/children/detail.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import router from "@/router";
|
||||||
|
import { ElButton } from "element-plus";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ToDetail",
|
||||||
|
setup() {
|
||||||
|
// 跳转详情
|
||||||
|
const navigateToDetail = async (id: number) => {
|
||||||
|
await router.push({
|
||||||
|
path: "/detail/" + id,
|
||||||
|
query: { mes: `msg${id}` },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return () =>
|
||||||
|
h("div", null, [
|
||||||
|
h(
|
||||||
|
ElButton,
|
||||||
|
{
|
||||||
|
type: "primary",
|
||||||
|
onClick: () => navigateToDetail(1),
|
||||||
|
},
|
||||||
|
"跳转详情1"
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
ElButton,
|
||||||
|
{
|
||||||
|
type: "success",
|
||||||
|
onClick: () => navigateToDetail(2),
|
||||||
|
},
|
||||||
|
"跳转详情2"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="padding: 30px">
|
<div flex flex-col gap-20px>
|
||||||
<el-alert :closable="false" title="菜单三级-1" type="error" />
|
<el-alert :closable="false" title="菜单三级-1" type="error" />
|
||||||
|
<el-input v-model="value" placeholder="缓存测试" />
|
||||||
|
<ToDetail />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ToDetail from "./detail";
|
||||||
|
|
||||||
|
defineOptions({ name: "MultiLevel31" });
|
||||||
|
|
||||||
|
const value = ref("");
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="padding: 30px">
|
<div flex flex-col gap-20px>
|
||||||
<el-alert :closable="false" title="菜单三级-2" type="warning" />
|
<el-alert :closable="false" title="菜单三级-2" type="warning" />
|
||||||
|
<el-input v-model="value" placeholder="缓存测试" />
|
||||||
|
<ToDetail />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ToDetail from "./detail";
|
||||||
|
|
||||||
|
defineOptions({ name: "MultiLevel32" });
|
||||||
|
const value = ref("");
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="padding: 30px">
|
<div flex flex-col gap-20px>
|
||||||
<el-alert :closable="false" title="菜单二级" type="success">
|
<el-alert :closable="false" title="菜单二级" type="success" />
|
||||||
<router-view />
|
<KeepCache />
|
||||||
</el-alert>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import KeepCache from "@/components/KeepCache/index.vue";
|
||||||
|
defineOptions({ name: "MultiLevel2" });
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="padding: 30px">
|
<div p-30px>
|
||||||
<el-link
|
<el-link
|
||||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/multi-level/level1.vue"
|
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/multi-level/level1.vue"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -9,8 +9,13 @@
|
|||||||
示例源码 请点击>>>>
|
示例源码 请点击>>>>
|
||||||
</el-link>
|
</el-link>
|
||||||
|
|
||||||
<el-alert :closable="false" title="菜单一级">
|
<div flex flex-col gap-20px>
|
||||||
<router-view />
|
<el-alert :closable="false" title="菜单一级" />
|
||||||
</el-alert>
|
<KeepCache />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import KeepCache from "@/components/KeepCache/index.vue";
|
||||||
|
defineOptions({ name: "MultiLevel1" });
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user