Browse Source

内嵌`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
otis 1 year ago
parent
commit
b13d745474

+ 6 - 0
mock/asyncRoutes.ts

@@ -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"]
           }
           }
         }
         }

+ 66 - 54
src/layout/components/appMain.vue

@@ -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
-          v-if="props.fixedHeader"
-          :wrap-style="{
-            display: 'flex',
-            'flex-wrap': 'wrap'
-          }"
-          :view-style="{
-            display: 'flex',
-            flex: 'auto',
-            overflow: 'auto',
-            'flex-direction': 'column'
-          }"
-        >
-          <el-backtop title="回到顶部" target=".app-main .el-scrollbar__wrap">
-            <backTop />
-          </el-backtop>
-          <div class="grow">
-            <transitionMain :route="route">
-              <keep-alive
-                v-if="isKeepAlive"
-                :include="usePermissionStoreHook().cachePageList"
+        <KeepAliveFrame :currComp="Component" :currRoute="route">
+          <template #default="{ Comp, fullPath, frameInfo }">
+            <el-scrollbar
+              v-if="props.fixedHeader"
+              :wrap-style="{
+                display: 'flex',
+                'flex-wrap': 'wrap'
+              }"
+              :view-style="{
+                display: 'flex',
+                flex: 'auto',
+                overflow: 'auto',
+                'flex-direction': 'column'
+              }"
+            >
+              <el-backtop
+                title="回到顶部"
+                target=".app-main .el-scrollbar__wrap"
               >
               >
+                <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"
-                  :key="route.fullPath"
+                  :is="Comp"
+                  v-else
+                  :key="fullPath"
+                  :frameInfo="frameInfo"
                   class="main-content"
                   class="main-content"
                 />
                 />
-              </keep-alive>
-              <component
-                :is="Component"
-                v-else
-                :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>
+              </transitionMain>
+            </div>
+          </template>
+        </KeepAliveFrame>
       </template>
       </template>
     </router-view>
     </router-view>
 
 

+ 79 - 0
src/layout/components/keepAliveFrame/index.vue

@@ -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 - 0
src/layout/components/keepAliveFrame/useMultiFrame.ts

@@ -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
+  };
+};

+ 22 - 2
src/layout/frameView.vue

@@ -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();
 });
 });