mirror of
				https://github.com/pure-admin/vue-pure-admin.git
				synced 2025-11-03 13:44:47 +08:00 
			
		
		
		
	内嵌iframe页支持设置keepAlive,保持页面状态 (#873)
				
					
				
			* pref: keep alive iframe * pref: default maxCount is 10 item * pref: 渲染iframe时,移除默认的slot渲染frameView组件 * perf: fix frame reload error after hmr * perf: 通过路由配置keepAlive frame * perf: refresh keep alive iframe
This commit is contained in:
		
							parent
							
								
									0887dd46d5
								
							
						
					
					
						commit
						b13d745474
					
				@ -118,6 +118,7 @@ const frameRouter = {
 | 
				
			|||||||
          meta: {
 | 
					          meta: {
 | 
				
			||||||
            title: "menus.hsEpDocument",
 | 
					            title: "menus.hsEpDocument",
 | 
				
			||||||
            frameSrc: "https://element-plus.org/zh-CN/",
 | 
					            frameSrc: "https://element-plus.org/zh-CN/",
 | 
				
			||||||
 | 
					            keepAlive: true,
 | 
				
			||||||
            roles: ["admin", "common"]
 | 
					            roles: ["admin", "common"]
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -127,6 +128,7 @@ const frameRouter = {
 | 
				
			|||||||
          meta: {
 | 
					          meta: {
 | 
				
			||||||
            title: "menus.hsTailwindcssDocument",
 | 
					            title: "menus.hsTailwindcssDocument",
 | 
				
			||||||
            frameSrc: "https://tailwindcss.com/docs/installation",
 | 
					            frameSrc: "https://tailwindcss.com/docs/installation",
 | 
				
			||||||
 | 
					            keepAlive: true,
 | 
				
			||||||
            roles: ["admin", "common"]
 | 
					            roles: ["admin", "common"]
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -136,6 +138,7 @@ const frameRouter = {
 | 
				
			|||||||
          meta: {
 | 
					          meta: {
 | 
				
			||||||
            title: "menus.hsVueDocument",
 | 
					            title: "menus.hsVueDocument",
 | 
				
			||||||
            frameSrc: "https://cn.vuejs.org/",
 | 
					            frameSrc: "https://cn.vuejs.org/",
 | 
				
			||||||
 | 
					            keepAlive: true,
 | 
				
			||||||
            roles: ["admin", "common"]
 | 
					            roles: ["admin", "common"]
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -145,6 +148,7 @@ const frameRouter = {
 | 
				
			|||||||
          meta: {
 | 
					          meta: {
 | 
				
			||||||
            title: "menus.hsViteDocument",
 | 
					            title: "menus.hsViteDocument",
 | 
				
			||||||
            frameSrc: "https://cn.vitejs.dev/",
 | 
					            frameSrc: "https://cn.vitejs.dev/",
 | 
				
			||||||
 | 
					            keepAlive: true,
 | 
				
			||||||
            roles: ["admin", "common"]
 | 
					            roles: ["admin", "common"]
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -154,6 +158,7 @@ const frameRouter = {
 | 
				
			|||||||
          meta: {
 | 
					          meta: {
 | 
				
			||||||
            title: "menus.hsPiniaDocument",
 | 
					            title: "menus.hsPiniaDocument",
 | 
				
			||||||
            frameSrc: "https://pinia.vuejs.org/zh/index.html",
 | 
					            frameSrc: "https://pinia.vuejs.org/zh/index.html",
 | 
				
			||||||
 | 
					            keepAlive: true,
 | 
				
			||||||
            roles: ["admin", "common"]
 | 
					            roles: ["admin", "common"]
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -163,6 +168,7 @@ const frameRouter = {
 | 
				
			|||||||
          meta: {
 | 
					          meta: {
 | 
				
			||||||
            title: "menus.hsRouterDocument",
 | 
					            title: "menus.hsRouterDocument",
 | 
				
			||||||
            frameSrc: "https://router.vuejs.org/zh/",
 | 
					            frameSrc: "https://router.vuejs.org/zh/",
 | 
				
			||||||
 | 
					            keepAlive: true,
 | 
				
			||||||
            roles: ["admin", "common"]
 | 
					            roles: ["admin", "common"]
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import Footer from "./footer/index.vue";
 | 
					import Footer from "./footer/index.vue";
 | 
				
			||||||
import { useGlobal } from "@pureadmin/utils";
 | 
					import { useGlobal } from "@pureadmin/utils";
 | 
				
			||||||
 | 
					import KeepAliveFrame from "./keepAliveFrame/index.vue";
 | 
				
			||||||
import backTop from "@/assets/svg/back_top.svg?component";
 | 
					import backTop from "@/assets/svg/back_top.svg?component";
 | 
				
			||||||
import { h, computed, Transition, defineComponent } from "vue";
 | 
					import { h, computed, Transition, defineComponent } from "vue";
 | 
				
			||||||
import { usePermissionStoreHook } from "@/store/modules/permission";
 | 
					import { usePermissionStoreHook } from "@/store/modules/permission";
 | 
				
			||||||
@ -89,64 +90,75 @@ const transitionMain = defineComponent({
 | 
				
			|||||||
  >
 | 
					  >
 | 
				
			||||||
    <router-view>
 | 
					    <router-view>
 | 
				
			||||||
      <template #default="{ Component, route }">
 | 
					      <template #default="{ Component, route }">
 | 
				
			||||||
        <el-scrollbar
 | 
					        <KeepAliveFrame :currComp="Component" :currRoute="route">
 | 
				
			||||||
          v-if="props.fixedHeader"
 | 
					          <template #default="{ Comp, fullPath, frameInfo }">
 | 
				
			||||||
          :wrap-style="{
 | 
					            <el-scrollbar
 | 
				
			||||||
            display: 'flex',
 | 
					              v-if="props.fixedHeader"
 | 
				
			||||||
            'flex-wrap': 'wrap'
 | 
					              :wrap-style="{
 | 
				
			||||||
          }"
 | 
					                display: 'flex',
 | 
				
			||||||
          :view-style="{
 | 
					                'flex-wrap': 'wrap'
 | 
				
			||||||
            display: 'flex',
 | 
					              }"
 | 
				
			||||||
            flex: 'auto',
 | 
					              :view-style="{
 | 
				
			||||||
            overflow: 'auto',
 | 
					                display: 'flex',
 | 
				
			||||||
            'flex-direction': 'column'
 | 
					                flex: 'auto',
 | 
				
			||||||
          }"
 | 
					                overflow: 'auto',
 | 
				
			||||||
        >
 | 
					                'flex-direction': 'column'
 | 
				
			||||||
          <el-backtop title="回到顶部" target=".app-main .el-scrollbar__wrap">
 | 
					              }"
 | 
				
			||||||
            <backTop />
 | 
					            >
 | 
				
			||||||
          </el-backtop>
 | 
					              <el-backtop
 | 
				
			||||||
          <div class="grow">
 | 
					                title="回到顶部"
 | 
				
			||||||
            <transitionMain :route="route">
 | 
					                target=".app-main .el-scrollbar__wrap"
 | 
				
			||||||
              <keep-alive
 | 
					 | 
				
			||||||
                v-if="isKeepAlive"
 | 
					 | 
				
			||||||
                :include="usePermissionStoreHook().cachePageList"
 | 
					 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
 | 
					                <backTop />
 | 
				
			||||||
 | 
					              </el-backtop>
 | 
				
			||||||
 | 
					              <div class="grow">
 | 
				
			||||||
 | 
					                <transitionMain :route="route">
 | 
				
			||||||
 | 
					                  <keep-alive
 | 
				
			||||||
 | 
					                    v-if="isKeepAlive"
 | 
				
			||||||
 | 
					                    :include="usePermissionStoreHook().cachePageList"
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <component
 | 
				
			||||||
 | 
					                      :is="Comp"
 | 
				
			||||||
 | 
					                      :key="fullPath"
 | 
				
			||||||
 | 
					                      :frameInfo="frameInfo"
 | 
				
			||||||
 | 
					                      class="main-content"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  </keep-alive>
 | 
				
			||||||
 | 
					                  <component
 | 
				
			||||||
 | 
					                    :is="Comp"
 | 
				
			||||||
 | 
					                    v-else
 | 
				
			||||||
 | 
					                    :key="fullPath"
 | 
				
			||||||
 | 
					                    :frameInfo="frameInfo"
 | 
				
			||||||
 | 
					                    class="main-content"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </transitionMain>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <Footer v-if="!hideFooter" />
 | 
				
			||||||
 | 
					            </el-scrollbar>
 | 
				
			||||||
 | 
					            <div v-else class="grow">
 | 
				
			||||||
 | 
					              <transitionMain :route="route">
 | 
				
			||||||
 | 
					                <keep-alive
 | 
				
			||||||
 | 
					                  v-if="isKeepAlive"
 | 
				
			||||||
 | 
					                  :include="usePermissionStoreHook().cachePageList"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <component
 | 
				
			||||||
 | 
					                    :is="Comp"
 | 
				
			||||||
 | 
					                    :key="fullPath"
 | 
				
			||||||
 | 
					                    :frameInfo="frameInfo"
 | 
				
			||||||
 | 
					                    class="main-content"
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </keep-alive>
 | 
				
			||||||
                <component
 | 
					                <component
 | 
				
			||||||
                  :is="Component"
 | 
					                  :is="Comp"
 | 
				
			||||||
                  :key="route.fullPath"
 | 
					                  v-else
 | 
				
			||||||
 | 
					                  :key="fullPath"
 | 
				
			||||||
 | 
					                  :frameInfo="frameInfo"
 | 
				
			||||||
                  class="main-content"
 | 
					                  class="main-content"
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
              </keep-alive>
 | 
					              </transitionMain>
 | 
				
			||||||
              <component
 | 
					            </div>
 | 
				
			||||||
                :is="Component"
 | 
					          </template>
 | 
				
			||||||
                v-else
 | 
					        </KeepAliveFrame>
 | 
				
			||||||
                :key="route.fullPath"
 | 
					 | 
				
			||||||
                class="main-content"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </transitionMain>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <Footer v-if="!hideFooter" />
 | 
					 | 
				
			||||||
        </el-scrollbar>
 | 
					 | 
				
			||||||
        <div v-else class="grow">
 | 
					 | 
				
			||||||
          <transitionMain :route="route">
 | 
					 | 
				
			||||||
            <keep-alive
 | 
					 | 
				
			||||||
              v-if="isKeepAlive"
 | 
					 | 
				
			||||||
              :include="usePermissionStoreHook().cachePageList"
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <component
 | 
					 | 
				
			||||||
                :is="Component"
 | 
					 | 
				
			||||||
                :key="route.fullPath"
 | 
					 | 
				
			||||||
                class="main-content"
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </keep-alive>
 | 
					 | 
				
			||||||
            <component
 | 
					 | 
				
			||||||
              :is="Component"
 | 
					 | 
				
			||||||
              v-else
 | 
					 | 
				
			||||||
              :key="route.fullPath"
 | 
					 | 
				
			||||||
              class="main-content"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
          </transitionMain>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
    </router-view>
 | 
					    </router-view>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										79
									
								
								src/layout/components/keepAliveFrame/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/layout/components/keepAliveFrame/index.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { getConfig } from "@/config";
 | 
				
			||||||
 | 
					import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
 | 
				
			||||||
 | 
					import { type Component, shallowRef, watch, computed } from "vue";
 | 
				
			||||||
 | 
					import { type RouteRecordRaw, RouteLocationNormalizedLoaded } from "vue-router";
 | 
				
			||||||
 | 
					import { useMultiFrame } from "@/layout/components/keepAliveFrame/useMultiFrame";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  currRoute: RouteLocationNormalizedLoaded;
 | 
				
			||||||
 | 
					  currComp: Component;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const compList = shallowRef([]);
 | 
				
			||||||
 | 
					const { setMap, getMap, MAP, delMap } = useMultiFrame();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const keep = computed(() => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    getConfig().KeepAlive &&
 | 
				
			||||||
 | 
					    props.currRoute.meta?.keepAlive &&
 | 
				
			||||||
 | 
					    !!props.currRoute.meta?.frameSrc
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					// 避免重新渲染 frameView
 | 
				
			||||||
 | 
					const normalComp = computed(() => !keep.value && props.currComp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(useMultiTagsStoreHook().multiTags, (tags: any) => {
 | 
				
			||||||
 | 
					  if (!Array.isArray(tags) || !keep.value) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const iframeTags = tags.filter(i => i.meta?.frameSrc);
 | 
				
			||||||
 | 
					  // tags必须是小于MAP,才是做了关闭动作,因为MAP插入的顺序在tags变化后发生
 | 
				
			||||||
 | 
					  if (iframeTags.length < MAP.size) {
 | 
				
			||||||
 | 
					    for (const i of MAP.keys()) {
 | 
				
			||||||
 | 
					      if (!tags.some(s => s.path === i)) {
 | 
				
			||||||
 | 
					        delMap(i);
 | 
				
			||||||
 | 
					        compList.value = getMap();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  () => props.currRoute.fullPath,
 | 
				
			||||||
 | 
					  path => {
 | 
				
			||||||
 | 
					    const multiTags = useMultiTagsStoreHook().multiTags as RouteRecordRaw[];
 | 
				
			||||||
 | 
					    const iframeTags = multiTags.filter(i => i.meta?.frameSrc);
 | 
				
			||||||
 | 
					    if (keep.value) {
 | 
				
			||||||
 | 
					      if (iframeTags.length !== MAP.size) {
 | 
				
			||||||
 | 
					        const sameKey = [...MAP.keys()].find(i => path === i);
 | 
				
			||||||
 | 
					        if (!sameKey) {
 | 
				
			||||||
 | 
					          // 添加缓存
 | 
				
			||||||
 | 
					          setMap(path, props.currComp);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (MAP.size > 0) {
 | 
				
			||||||
 | 
					      compList.value = getMap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    immediate: true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <template v-for="[fullPath, Comp] in compList" :key="fullPath">
 | 
				
			||||||
 | 
					    <div v-show="fullPath === props.currRoute.fullPath" class="w-full h-full">
 | 
				
			||||||
 | 
					      <slot
 | 
				
			||||||
 | 
					        :fullPath="fullPath"
 | 
				
			||||||
 | 
					        :Comp="Comp"
 | 
				
			||||||
 | 
					        :frameInfo="{ frameSrc: currRoute.meta?.frameSrc, fullPath }"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </template>
 | 
				
			||||||
 | 
					  <div v-show="!keep" class="w-full h-full">
 | 
				
			||||||
 | 
					    <slot :Comp="normalComp" :fullPath="props.currRoute.fullPath" frameInfo />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/layout/components/keepAliveFrame/useMultiFrame.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/layout/components/keepAliveFrame/useMultiFrame.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					const MAP = new Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useMultiFrame = () => {
 | 
				
			||||||
 | 
					  function setMap(path, Comp) {
 | 
				
			||||||
 | 
					    MAP.set(path, Comp);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getMap(path?) {
 | 
				
			||||||
 | 
					    if (path) {
 | 
				
			||||||
 | 
					      return MAP.get(path);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return [...MAP.entries()];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function delMap(path) {
 | 
				
			||||||
 | 
					    MAP.delete(path);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    setMap,
 | 
				
			||||||
 | 
					    getMap,
 | 
				
			||||||
 | 
					    delMap,
 | 
				
			||||||
 | 
					    MAP
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,18 +1,24 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { useI18n } from "vue-i18n";
 | 
					import { useI18n } from "vue-i18n";
 | 
				
			||||||
import { useRoute } from "vue-router";
 | 
					import { useRoute } from "vue-router";
 | 
				
			||||||
import { ref, unref, onMounted, nextTick } from "vue";
 | 
					import { ref, unref, watch, onMounted, nextTick } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineOptions({
 | 
					defineOptions({
 | 
				
			||||||
  name: "FrameView"
 | 
					  name: "FrameView"
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  frameInfo?: {
 | 
				
			||||||
 | 
					    frameSrc?: string;
 | 
				
			||||||
 | 
					    fullPath?: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { t } = useI18n();
 | 
					const { t } = useI18n();
 | 
				
			||||||
const loading = ref(true);
 | 
					const loading = ref(true);
 | 
				
			||||||
const currentRoute = useRoute();
 | 
					const currentRoute = useRoute();
 | 
				
			||||||
const frameSrc = ref<string>("");
 | 
					const frameSrc = ref<string>("");
 | 
				
			||||||
const frameRef = ref<HTMLElement | null>(null);
 | 
					const frameRef = ref<HTMLElement | null>(null);
 | 
				
			||||||
 | 
					 | 
				
			||||||
if (unref(currentRoute.meta)?.frameSrc) {
 | 
					if (unref(currentRoute.meta)?.frameSrc) {
 | 
				
			||||||
  frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
 | 
					  frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -39,6 +45,20 @@ function init() {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  () => currentRoute.fullPath,
 | 
				
			||||||
 | 
					  path => {
 | 
				
			||||||
 | 
					    if (props.frameInfo?.fullPath === path) {
 | 
				
			||||||
 | 
					      frameSrc.value = props.frameInfo?.frameSrc;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // 重新加载
 | 
				
			||||||
 | 
					    if (path.indexOf("/redirect/") > -1) {
 | 
				
			||||||
 | 
					      frameSrc.value = props.frameInfo?.fullPath;
 | 
				
			||||||
 | 
					      loading.value = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  init();
 | 
					  init();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user