Commit d506034a by 高源

优化tab关闭后组件销毁

parent 6b244022
<!doctype html> <!doctype html>
<html lang="zh-cmn-Hans"> <html lang="zh-cmn-Hans">
<head> <head>
<meta name="buildTime" content="2025-02-20 16:06:48"> <meta name="buildTime" content="2025-03-06 10:17:29">
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" /> <link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<title>VueDashboard</title> <title>VueDashboard</title>
<script type="module" crossorigin src="/Content/VueDashboardUi/VueDashboard1/assets/index-BTK4dlWY.js"></script> <script type="module" crossorigin src="/Content/VueDashboardUi/VueDashboard1/assets/index-ClligkOZ.js"></script>
<link rel="stylesheet" crossorigin href="/Content/VueDashboardUi/VueDashboard1/assets/index-9k_B1ZU8.css"> <link rel="stylesheet" crossorigin href="/Content/VueDashboardUi/VueDashboard1/assets/index-9k_B1ZU8.css">
</head> </head>
<body> <body>
......
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, reactive, ref, watch, h } from 'vue'; import { h, nextTick, reactive, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useElementBounding } from '@vueuse/core'; import { useElementBounding, useEventBus } from '@vueuse/core';
import { PageTab } from '@sa/materials'; import { PageTab } from '@sa/materials';
import BetterScroll from '@/components/custom/better-scroll.vue'; import BetterScroll from '@/components/custom/better-scroll.vue';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
...@@ -31,6 +31,9 @@ type TabNamedNodeMap = NamedNodeMap & { ...@@ -31,6 +31,9 @@ type TabNamedNodeMap = NamedNodeMap & {
[TAB_DATA_ID]: Attr; [TAB_DATA_ID]: Attr;
}; };
// 创建事件总线
const tabCloseEventBus = useEventBus('tab-close');
async function scrollToActiveTab() { async function scrollToActiveTab() {
await nextTick(); await nextTick();
if (!tabRef.value) return; if (!tabRef.value) return;
...@@ -81,6 +84,13 @@ function getContextMenuDisabledKeys(tabId: string) { ...@@ -81,6 +84,13 @@ function getContextMenuDisabledKeys(tabId: string) {
} }
async function handleCloseTab(tab: App.Global.Tab) { async function handleCloseTab(tab: App.Global.Tab) {
// 发送关闭事件,传递要关闭的标签ID
tabCloseEventBus.emit(tab.id);
// 等待一小段时间,确保清理操作有时间执行
await new Promise(resolve => setTimeout(resolve, 50));
// 然后移除标签和缓存
await tabStore.removeTab(tab.id); await tabStore.removeTab(tab.id);
await routeStore.reCacheRoutesByKey(tab.routeKey); await routeStore.reCacheRoutesByKey(tab.routeKey);
} }
...@@ -139,7 +149,7 @@ async function handleContextMenu(e: MouseEvent, tabId: string) { ...@@ -139,7 +149,7 @@ async function handleContextMenu(e: MouseEvent, tabId: string) {
function init() { function init() {
tabStore.initTabStore(route); tabStore.initTabStore(route);
console.log(tabStore) console.log(tabStore);
} }
function removeFocus() { function removeFocus() {
......
<!-- eslint-disable no-console --> <!-- eslint-disable no-console -->
<!-- eslint-disable @typescript-eslint/no-shadow --> <!-- eslint-disable @typescript-eslint/no-shadow -->
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onActivated, onMounted, ref, shallowRef, watch } from 'vue'; import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { useEventBus } from '@vueuse/core'; // 添加事件总线
import { loadModule } from 'vue3-sfc-loader'; import { loadModule } from 'vue3-sfc-loader';
import { getSelectMenu } from '@/service/api'; import { getSelectMenu } from '@/service/api';
import NotFound from '@/views/_builtin/404/index.vue'; // 引入 404 组件 import NotFound from '@/views/_builtin/404/index.vue'; // 引入 404 组件
...@@ -9,6 +10,9 @@ import ExtJsComponent from './extJs.vue'; ...@@ -9,6 +10,9 @@ import ExtJsComponent from './extJs.vue';
import WebviewComponent from './webview.vue'; import WebviewComponent from './webview.vue';
import VueComponent from './vueComponent.vue'; // 添加新组件导入 import VueComponent from './vueComponent.vue'; // 添加新组件导入
// 添加组件选项
defineOptions({ name: 'IframePage' });
interface Props { interface Props {
url: string; url: string;
kvid: string; kvid: string;
...@@ -21,6 +25,65 @@ const selectTag = ref(''); ...@@ -21,6 +25,65 @@ const selectTag = ref('');
const extTag = ref(''); const extTag = ref('');
const hasError = ref(false); const hasError = ref(false);
// 组件引用
const webviewRef = ref<any>(null);
const extJsRef = ref<any>(null);
const vueComponentRef = ref<any>(null);
// 监听标签关闭事件
const tabCloseEventBus = useEventBus('tab-close');
// 清理所有资源的函数
const cleanupResources = () => {
console.log('清理iframe-page资源');
// 清理webview (iframe)
if (webviewRef.value && typeof webviewRef.value.cleanup === 'function') {
webviewRef.value.cleanup();
}
// 清理ExtJS组件
if (extJsRef.value && typeof extJsRef.value.cleanup === 'function') {
extJsRef.value.cleanup();
}
// 清理Vue组件
if (vueComponentRef.value && typeof vueComponentRef.value.cleanup === 'function') {
vueComponentRef.value.cleanup();
}
// 重置状态
asyncComponent.value = null;
selectTag.value = '';
extTag.value = '';
hasError.value = false;
};
// 监听标签关闭事件
tabCloseEventBus.on(() => {
cleanupResources();
});
// 组件卸载前清理
onBeforeUnmount(() => {
cleanupResources();
// 移除事件监听
tabCloseEventBus.off();
});
// 修复console[type]的类型错误
const safeConsoleLog = (type: string, ...args: any[]) => {
if (type === 'error') {
console.error(...args);
} else if (type === 'warn') {
console.warn(...args);
} else if (type === 'info') {
console.info(...args);
} else {
console.log(...args);
}
};
// 定义加载外部组件的函数 // 定义加载外部组件的函数
const loadExternalComponent = async (url: string) => { const loadExternalComponent = async (url: string) => {
const options = { const options = {
...@@ -41,7 +104,7 @@ const loadExternalComponent = async (url: string) => { ...@@ -41,7 +104,7 @@ const loadExternalComponent = async (url: string) => {
document.head.appendChild(style); document.head.appendChild(style);
}, },
log(type: string, ...args: any[]) { log(type: string, ...args: any[]) {
console[type](...args); safeConsoleLog(type, ...args);
} }
}; };
...@@ -53,6 +116,7 @@ const loadExternalComponent = async (url: string) => { ...@@ -53,6 +116,7 @@ const loadExternalComponent = async (url: string) => {
}) })
.catch(error => { .catch(error => {
console.error('Error loading component:', error); console.error('Error loading component:', error);
hasError.value = true;
}); });
}; };
...@@ -65,32 +129,27 @@ const handleMenuAccess = async () => { ...@@ -65,32 +129,27 @@ const handleMenuAccess = async () => {
console.log('Processing:', { url, kvid, type }); console.log('Processing:', { url, kvid, type });
const origin = window.location.origin; const origin = window.location.origin;
// 创建后端地址 - 假设后端在80端口
const backendOrigin = origin.replace(':8080', ':80');
if (type === 'System') { if (type === 'System') {
console.log(url); console.log(url);
if (url.startsWith('App')) { if (url.startsWith('App')) {
extTag.value = url; extTag.value = url;
} else if (url.endsWith('.vue')) { } else if (url.endsWith('.vue')) {
// 添加.vue文件判断 // 使用后端地址加载Vue文件
// selectTag.value = `${origin}/${url}`; loadExternalComponent(`${backendOrigin}/${url}`);
// asyncComponent.value = `${origin}/${url}`; // 存储vue文件的URL
loadExternalComponent(`${origin}/${url}`);
} else { } else {
selectTag.value = `${origin}/${url}`; selectTag.value = `${backendOrigin}/${url}`;
} }
console.log('url1:', url);
console.log('kvid1:', kvid);
console.log('type1:', type);
} else { } else {
console.log('url2:', url);
console.log('kvid2:', kvid);
console.log('type2:', type);
try { try {
const { data: selectMenu } = await getSelectMenu( const { data: selectMenu } = await getSelectMenu(
`/Restful/Kivii.Basic.Entities.Function/Access.json?MenuKvids=${kvid}` `/Restful/Kivii.Basic.Entities.Function/Access.json?MenuKvids=${kvid}`
); );
if (selectMenu?.Results?.length > 0) { // 修复类型错误
if (selectMenu?.Results && selectMenu.Results.length > 0) {
const handler = selectMenu.Results[0].Handler; const handler = selectMenu.Results[0].Handler;
// 确保在设置 extTag 之前 DOM 已准备好 // 确保在设置 extTag 之前 DOM 已准备好
if (!handler.startsWith('/') && !handler.endsWith('.vue')) { if (!handler.startsWith('/') && !handler.endsWith('.vue')) {
...@@ -100,11 +159,10 @@ const handleMenuAccess = async () => { ...@@ -100,11 +159,10 @@ const handleMenuAccess = async () => {
// 先判断是否以/开头 // 先判断是否以/开头
if (handler.endsWith('.vue')) { if (handler.endsWith('.vue')) {
// 如果以.vue结尾 // 如果以.vue结尾
// asyncComponent.value = `${origin}${handler}`; loadExternalComponent(`${backendOrigin}${handler}`);
loadExternalComponent(`${origin}${handler}`);
} else { } else {
// 如果不以.vue结尾 // 如果不以.vue结尾
selectTag.value = `${origin}${handler}`; selectTag.value = `${backendOrigin}${handler}`;
} }
} else { } else {
extTag.value = handler; extTag.value = handler;
...@@ -123,6 +181,9 @@ const handleMenuAccess = async () => { ...@@ -123,6 +181,9 @@ const handleMenuAccess = async () => {
watch( watch(
[() => type, () => kvid, () => url], [() => type, () => kvid, () => url],
() => { () => {
// 先清理旧资源
cleanupResources();
// 然后加载新内容
handleMenuAccess(); handleMenuAccess();
}, },
{ immediate: true } { immediate: true }
...@@ -147,11 +208,12 @@ onMounted(() => { ...@@ -147,11 +208,12 @@ onMounted(() => {
<WebviewComponent <WebviewComponent
v-if="selectTag && !hasError && !asyncComponent" v-if="selectTag && !hasError && !asyncComponent"
id="iframePage" id="iframePage"
ref="webviewRef"
class="size-full" class="size-full"
:url="selectTag" :url="selectTag"
></WebviewComponent> ></WebviewComponent>
<ExtJsComponent v-else-if="extTag && !hasError" :key="extTag" :url="extTag"></ExtJsComponent> <ExtJsComponent v-else-if="extTag && !hasError" ref="extJsRef" :key="extTag" :url="extTag"></ExtJsComponent>
<VueComponent v-else-if="asyncComponent && !hasError" :url="asyncComponent"></VueComponent> <VueComponent v-else-if="asyncComponent && !hasError" ref="vueComponentRef" :url="asyncComponent"></VueComponent>
<NotFound v-else /> <NotFound v-else />
</div> </div>
</template> </template>
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; import { nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
defineOptions({ name: 'ExtJsComponent' });
// 接收一个名为 url 的 prop // 接收一个名为 url 的 prop
const props = defineProps<{ const props = defineProps<{
url: string; url: string;
...@@ -11,6 +13,7 @@ let extComponent: any = null; ...@@ -11,6 +13,7 @@ let extComponent: any = null;
// 控制组件显示状态的变量 // 控制组件显示状态的变量
const isActive = ref(true); const isActive = ref(true);
const extjsContainerId = `extjs-${Math.random().toString(36).substr(2, 9)}`; const extjsContainerId = `extjs-${Math.random().toString(36).substr(2, 9)}`;
function initializeExtComponent() { function initializeExtComponent() {
extComponent = Ext.create('Ext.panel.Panel', { extComponent = Ext.create('Ext.panel.Panel', {
renderTo: extjsContainerId, renderTo: extjsContainerId,
...@@ -25,9 +28,6 @@ function initializeExtComponent() { ...@@ -25,9 +28,6 @@ function initializeExtComponent() {
function updateExtComponent(newUrl: string) { function updateExtComponent(newUrl: string) {
if (extComponent) { if (extComponent) {
// 更新 ExtJS 组件的内容,而不销毁整个组件 // 更新 ExtJS 组件的内容,而不销毁整个组件
// 这里假设您可以通过某种方式更新组件的内容
// 例如,替换面板中的子组件
// 获取当前的子组件 // 获取当前的子组件
const oldItem = extComponent.items.getAt(0); const oldItem = extComponent.items.getAt(0);
...@@ -42,6 +42,34 @@ function updateExtComponent(newUrl: string) { ...@@ -42,6 +42,34 @@ function updateExtComponent(newUrl: string) {
} }
} }
// 清理函数 - 完全销毁ExtJS组件
function cleanup() {
console.log('清理ExtJS资源');
if (extComponent) {
try {
// 销毁ExtJS组件
extComponent.destroy();
// 清空容器
const container = document.getElementById(extjsContainerId);
if (container) {
container.innerHTML = '';
}
} catch (e) {
console.error('清理ExtJS组件时出错:', e);
}
// 清空引用
extComponent = null;
}
}
// 暴露清理方法给父组件
defineExpose({
cleanup
});
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
if (!extComponent) { if (!extComponent) {
...@@ -54,24 +82,16 @@ onMounted(() => { ...@@ -54,24 +82,16 @@ onMounted(() => {
}); });
onActivated(() => { onActivated(() => {
// if (extComponent) {
// extComponent.show();
isActive.value = true; isActive.value = true;
// }
}); });
onDeactivated(() => { onDeactivated(() => {
// if (extComponent) {
// extComponent.hide();
isActive.value = false; isActive.value = false;
// }
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (extComponent) { // 组件卸载前清理
extComponent.destroy(); cleanup();
extComponent = null;
}
}); });
// 监听 props.url 的变化 // 监听 props.url 的变化
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, getCurrentInstance, onActivated, onDeactivated, onMounted, ref, shallowRef } from 'vue'; import {
computed,
getCurrentInstance,
onActivated,
onBeforeUnmount,
onDeactivated,
onMounted,
ref,
shallowRef
} from 'vue';
import { NConfigProvider, darkTheme } from 'naive-ui'; import { NConfigProvider, darkTheme } from 'naive-ui';
import * as naive from 'naive-ui'; import * as naive from 'naive-ui';
import axios from 'axios'; import axios from 'axios';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
defineOptions({ name: 'VueComponent' });
const props = defineProps<{ const props = defineProps<{
url: string; url: string;
}>(); }>();
...@@ -36,6 +47,29 @@ declare global { ...@@ -36,6 +47,29 @@ declare global {
// 全局注入axios // 全局注入axios
(window as any).$axios = axios; (window as any).$axios = axios;
// 清理函数 - 完全销毁Vue组件
function cleanup() {
console.log('清理Vue组件资源');
try {
// 清空组件引用
asyncComponent.value = null;
// 清空容器
const container = document.getElementById(containerId);
if (container) {
container.innerHTML = '';
}
} catch (e) {
console.error('清理Vue组件时出错:', e);
}
}
// 暴露清理方法给父组件
defineExpose({
cleanup
});
onMounted(async () => { onMounted(async () => {
try { try {
asyncComponent.value = props.url; asyncComponent.value = props.url;
...@@ -55,6 +89,11 @@ onActivated(() => { ...@@ -55,6 +89,11 @@ onActivated(() => {
onDeactivated(() => { onDeactivated(() => {
isActive.value = false; isActive.value = false;
}); });
onBeforeUnmount(() => {
// 组件卸载前清理
cleanup();
});
</script> </script>
<template> <template>
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; import { nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
defineOptions({ name: 'WebviewComponent' });
// 接收一个名为 url 的 prop // 接收一个名为 url 的 prop
const props = defineProps<{ const props = defineProps<{
url: string; url: string;
...@@ -36,6 +38,34 @@ function updateExtComponent(newUrl: string) { ...@@ -36,6 +38,34 @@ function updateExtComponent(newUrl: string) {
} }
} }
// 清理函数 - 完全销毁iframe
function cleanup() {
console.log('清理webview资源');
if (iframeEl) {
try {
// 先将src设为空白页,停止所有活动
iframeEl.src = 'about:blank';
// 从DOM中移除
const container = document.getElementById(extjsContainerId);
if (container && container.contains(iframeEl)) {
container.removeChild(iframeEl);
}
} catch (e) {
console.error('清理iframe时出错:', e);
}
// 清空引用
iframeEl = null;
}
}
// 暴露清理方法给父组件
defineExpose({
cleanup
});
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
// 初次挂载时创建 iframe // 初次挂载时创建 iframe
...@@ -57,11 +87,7 @@ onDeactivated(() => { ...@@ -57,11 +87,7 @@ onDeactivated(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 组件卸载前清理 // 组件卸载前清理
const container = document.getElementById(extjsContainerId); cleanup();
if (container && iframeEl) {
container.removeChild(iframeEl);
iframeEl = null;
}
}); });
// 监听 props.url 的变化,当地址变化时更新 iframe 的 src // 监听 props.url 的变化,当地址变化时更新 iframe 的 src
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment