refactor: 资源操作tab优化

This commit is contained in:
meilin.huang
2026-06-02 19:00:32 +08:00
parent 96ef4d2d6f
commit 4b3c98fd58
9 changed files with 99 additions and 70 deletions

View File

@@ -40,7 +40,7 @@ const SqlIcon = {
};
const getDbOpTab = async (params: any) => {
const tabKey = `db.${params.instCode}.${params.dbCode}.${params.db}`;
const tabKey = `${params.instCode}.${params.dbCode}.${params.db}`;
return await createResourceOpTab({
key: tabKey,
name: `${params.name}/${params.db}`,
@@ -85,7 +85,7 @@ export const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value)
return dbInstances?.map((x: any) => {
x.tagPath = tagPath;
x.instCode = x.code;
return TagTreeNode.new(parentNode, `db.${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
});
});

View File

@@ -14,7 +14,7 @@ const Icon = {
};
const getContainerOpTab = async (container: any) => {
const tabKey = `container.${container.code}`;
const tabKey = `${container.code}`;
return await createResourceOpTab({
key: tabKey,
name: container.name,
@@ -33,7 +33,7 @@ export const NodeTypeContainerTag = new NodeType(TagTreeNode.TagPath).withLoadNo
// 把list 根据name字段排序
return res?.list
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.map((x: any) => TagTreeNode.new(node, `container.${x.code}`, x.name, NodeTypeContainer).withIsLeaf(true).withParams(x).withIcon(Icon));
.map((x: any) => TagTreeNode.new(node, `${x.code}`, x.name, NodeTypeContainer).withIsLeaf(true).withParams(x).withIcon(Icon));
});
const NodeTypeContainer = new NodeType(11).withNodeClickFunc(async (node: TagTreeNode) => {

View File

@@ -27,14 +27,14 @@ const NodeTypeEsTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async
await sleep(100);
return insts?.map((x: any) => {
x.tagPath = parentNode.key;
return TagTreeNode.new(parentNode, `es.${x.code}`, x.name, NodeTypeInst).withNodeComponent(NodeEs).withIsLeaf(true).withParams(x);
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeInst).withNodeComponent(NodeEs).withIsLeaf(true).withParams(x);
});
});
// 加载实例列表
const NodeTypeInst = new NodeType(1).withNodeClickFunc(async (nodeData: TagTreeNode) => {
const inst = nodeData.params;
const tabKey = `es.${inst.code}`;
const tabKey = `${inst.code}`;
createResourceOpTab({
key: tabKey,
name: inst.name,

View File

@@ -31,7 +31,7 @@ export const NodeTypeMachineTag = new NodeType(TagTreeNode.TagPath).withLoadNode
return res?.list
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.map((x: any) =>
TagTreeNode.new(node, `machine.${x.code}`, x.name, NodeTypeMachine)
TagTreeNode.new(node, `${x.code}`, x.name, NodeTypeMachine)
.withParams(x)
.withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
.withIcon(MachineIcon)
@@ -107,7 +107,7 @@ export const NodeTypeAuthCert = new NodeType(12)
.withNodeDblclickFunc(async (node: TagTreeNode) => {
const m = node.params;
const key = `machine.${m.code}.${m.selectAuthCert.name}.${new Date().getTime()}`;
const key = `${m.code}.${m.selectAuthCert.name}.${new Date().getTime()}`;
createResourceOpTab({
key,
name: `${m.selectAuthCert.username}@${m.name}`,
@@ -134,7 +134,7 @@ export const NodeTypeAuthCert = new NodeType(12)
.withOnClick(async (node: TagTreeNode) => {
const m = node.params;
const key = `machine.${m.code}.${m.selectAuthCert.name}.${new Date().getTime()}`;
const key = `${m.code}.${m.selectAuthCert.name}.${new Date().getTime()}`;
createResourceOpTab({
key,
name: `${m.selectAuthCert.username}@${m.name}`,
@@ -188,7 +188,7 @@ export const NodeTypeAuthCert = new NodeType(12)
const acName = m.selectAuthCert.name;
// 直接打开文件管理 tabFileTab 内部会处理配置选择
const tabKey = `machine.${m.code}.${acName}`;
const tabKey = `${m.code}.${acName}`;
createResourceOpTab({
key: tabKey,
name: `${m.selectAuthCert.username}@${m.name}`,

View File

@@ -108,6 +108,7 @@ const blur = () => {
defineExpose({
onRefresh: handleReconnect,
onActivate: focus,
onResize: fitTerminal,
close,
fitTerminal,
focus,

View File

@@ -18,7 +18,7 @@ const NodeMongo = defineAsyncComponent(() => import('./NodeMongo.vue'));
const NodeMongoDb = defineAsyncComponent(() => import('./NodeMongoDb.vue'));
const getMongoOpTab = async (inst: any) => {
const tabKey = `mongo.${inst.code}`;
const tabKey = `${inst.code}`;
return await createResourceOpTab({
key: tabKey,
name: inst.instName || inst.name,
@@ -42,7 +42,7 @@ const NodeTypeMongoTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asy
await sleep(100);
return mongoInfos?.map((x: any) => {
x.tagPath = parentNode.key;
return TagTreeNode.new(parentNode, `mongo.${x.code}`, x.name, NodeTypeMongo).withParams(x).withNodeComponent(NodeMongo);
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeMongo).withParams(x).withNodeComponent(NodeMongo);
});
});

View File

@@ -28,7 +28,7 @@ const NodeTypeRedisTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asy
await sleep(100);
return redisInfos.map((x: any) => {
x.tagPath = parentNode.key;
return TagTreeNode.new(parentNode, `redis.${x.code}`, x.name, NodeTypeRedis).withParams(x).withNodeComponent(NodeRedis);
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeRedis).withParams(x).withNodeComponent(NodeRedis);
});
});
@@ -74,7 +74,7 @@ const NodeTypeRedis = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTr
const NodeTypeDb = new NodeType(21).withNodeClickFunc(async (node: TagTreeNode) => {
const params = node.params;
const key = `redis.${params.code}`;
const key = `${params.code}`;
const resourceOpTab = await createResourceOpTab({
key,
name: `${params.redisName}`,

View File

@@ -116,7 +116,7 @@
</div>
</div>
<div class="resource-tab-content">
<keep-alive>
<keep-alive :max="20">
<component
ref="activeCompRef"
:is="activeResourceTab?.component"
@@ -133,7 +133,7 @@
<Contextmenu :dropdown="tabDropdown" :items="tabContextmenuItems" ref="tabContextmenuRef" />
<!-- 渲染注册的非 tab 组件Overlay -->
<template v-for="overlay in Array.from(allResourceOpOverlays.values())" :key="overlay.key">
<template v-for="overlay in overlayList" :key="overlay.key">
<component v-if="overlay.visible" :is="overlay.component" v-bind="overlay.props" @update:visible="(val: boolean) => (overlay.visible = val)" />
</template>
</div>
@@ -205,6 +205,10 @@ const resourceTabs = computed(() => {
return Array.from(allResourceOpTabs.values());
});
const overlayList = computed(() => {
return Array.from(allResourceOpOverlays.values());
});
// Tab 右键菜单
const tabDropdown = reactive({ x: 0, y: 0 });
const tabContextmenuItems = ref<ContextmenuItem[]>([]);
@@ -242,6 +246,7 @@ onMounted(() => {
onUnmounted(() => {
document.removeEventListener('keydown', onFullscreenKeydown);
if (filterTimer) clearTimeout(filterTimer);
});
const activeCompRef = useTemplateRef<any>('activeCompRef');
@@ -259,13 +264,13 @@ const registerActiveComp = (tabKey: string) => {
// 解决 keep-alive 场景下 :ref 回调不可靠的问题(缓存组件激活时 ref 回调不重新触发)
watch(activeResourceOpTabKey, (tabKey: string) => {
if (!tabKey) return;
let attempts = 0;
const maxAttempts = 50; // 最多重试50次防止无限轮询
// 异步组件可能需要多轮 nextTick 才能拿到实例
const tryRegister = () => {
nextTick(() => {
if (!registerActiveComp(tabKey)) {
// 实例尚未就绪,继续轮询
setTimeout(tryRegister, 10);
}
if (registerActiveComp(tabKey) || ++attempts >= maxAttempts) return;
setTimeout(tryRegister, 50);
});
};
tryRegister();
@@ -283,8 +288,12 @@ const state = reactive({
const { filterText } = toRefs(state);
watch(filterText, (val) => {
treeRef.value?.filter(val);
let filterTimer: ReturnType<typeof setTimeout> | null = null;
watch(filterText, (val: string) => {
if (filterTimer) clearTimeout(filterTimer);
filterTimer = setTimeout(() => {
treeRef.value?.filter(val);
}, 300);
});
watch(
@@ -425,6 +434,9 @@ const onNodeContextmenu = (event: any, data: any) => {
// 激活指定标签页
const activateTab = (tabKey: string) => {
activateResourceOpTab(tabKey);
if (!tabKey) {
return;
}
// 定位到左侧资源树对应节点
if (resourceComponentsNodeKey.value[tabKey]) {
setCurrentKey(resourceComponentsNodeKey.value[tabKey]);
@@ -435,25 +447,6 @@ const activateTab = (tabKey: string) => {
});
};
// 关闭标签页
const closeTab = (tabKey: string) => {
// 清除组件实例和缓存
removeResourceOpTab(tabKey);
getComponentInstance<any>(tabKey)?.onClose?.();
// 如果关闭的是当前活动标签,切换到相邻标签
if (activeResourceOpTabKey.value === tabKey) {
const remainingTabs: string[] = Array.from(allResourceOpTabs.keys());
if (remainingTabs.length > 0) {
// 切换到最后一个tab
activateTab(remainingTabs[remainingTabs.length - 1]);
}else{
activeResourceOpTabKey.value = ''
}
}
};
// 刷新标签页(通过改变 key 强制重新渲染)
const refreshTab = (tabKey: string) => {
// 调用该 tab 注册的刷新回调
@@ -469,14 +462,36 @@ const onTabContextmenu = (event: MouseEvent, tab: ResourceOpTab) => {
tabContextmenuRef.value?.openContextmenu({ tabKey: tab.key });
};
// 关闭标签页
const closeTab = (tabKey: string, isChangeTab: boolean = true) => {
// 清除组件实例和缓存
removeResourceOpTab(tabKey);
// 清理节点映射关系
delete resourceComponentsNodeKey.value[tabKey];
// 调用该 tab 的关闭回调
getComponentInstance<any>(tabKey)?.onClose?.();
// 如果关闭的是当前活动标签,切换到相邻标签
if (activeResourceOpTabKey.value === tabKey) {
const remainingTabs: string[] = Array.from(allResourceOpTabs.keys());
if (remainingTabs.length > 0) {
// 切换到最后一个tab
activateTab(remainingTabs[remainingTabs.length - 1]);
} else {
activeResourceOpTabKey.value = '';
}
}
};
// 关闭所有标签
const closeAllTabs = () => {
const allKeys: string[] = Array.from(allResourceOpTabs.keys());
allKeys.forEach((key) => {
removeResourceOpTab(key);
closeTab(key, false);
});
allResourceOpTabs.clear();
activateResourceOpTab('');
resourceComponentsNodeKey.value = {};
activateTab('');
};
// 关闭左侧标签
@@ -486,11 +501,11 @@ const closeLeftTabs = (targetTabKey: string) => {
if (targetIndex <= 0) return;
const keysToClose = allKeys.slice(0, targetIndex);
keysToClose.forEach((key: string) => {
removeResourceOpTab(key);
closeTab(key, false);
});
// 如果当前激活的标签被关闭,切换到目标标签
if (keysToClose.includes(activeResourceOpTabKey.value)) {
activateResourceOpTab(targetTabKey);
activateTab(targetTabKey);
}
};
@@ -499,9 +514,9 @@ const closeOtherTabs = (targetTabKey: string) => {
const allKeys: string[] = Array.from(allResourceOpTabs.keys());
const keysToClose = allKeys.filter((key) => key !== targetTabKey);
keysToClose.forEach((key: string) => {
removeResourceOpTab(key);
closeTab(key, false);
});
activateResourceOpTab(targetTabKey);
activateTab(targetTabKey);
};
// 关闭右侧标签
@@ -511,11 +526,11 @@ const closeRightTabs = (targetTabKey: string) => {
if (targetIndex === -1 || targetIndex === allKeys.length - 1) return;
const keysToClose = allKeys.slice(targetIndex + 1);
keysToClose.forEach((key: string) => {
removeResourceOpTab(key);
closeTab(key, false);
});
// 如果当前激活的标签被关闭,切换到目标标签
if (keysToClose.includes(activeResourceOpTabKey.value)) {
activateResourceOpTab(targetTabKey);
activateTab(targetTabKey);
}
};
@@ -535,23 +550,25 @@ const getNode = (nodeKey: any) => {
const setCurrentKey = (nodeKey: any) => {
treeRef.value.setCurrentKey(nodeKey);
// 通过Id获取到对应的dom元素
const node = document.getElementById(nodeKey);
if (node) {
setTimeout(() => {
nextTick(() => {
// 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
node.scrollIntoView({ block: 'center' });
});
}, 100);
}
// 延迟查询 DOM确保节点已展开渲染再用 rAF 滚动避免强制同步布局
setTimeout(() => {
requestAnimationFrame(() => {
document.getElementById(nodeKey)?.scrollIntoView({ block: 'center' });
});
}, 100);
};
let resizeRAF = 0;
const onResizeOpPanel = () => {
for (const [tabKey] of allResourceOpTabs) {
getComponentInstance<any>(tabKey)?.onResize?.();
}
// 用 requestAnimationFrame 节流Splitter 拖拽时高频触发 resize 事件
if (resizeRAF) return;
resizeRAF = requestAnimationFrame(() => {
resizeRAF = 0;
const key = activeResourceOpTabKey.value;
if (key) {
getComponentInstance<any>(key)?.onResize?.();
}
});
};
const ctx: ResourceOpCtx = {

View File

@@ -94,17 +94,23 @@ export function createResourceOpTab(tab: ResourceOpTab): Promise<ResourceOpTab>
activeResourceOpTabKey.value = tab.key;
}
// 等待组件实例就绪后返回 tab 配置,超时 2000ms 后停止重试
return new Promise((resolve, reject) => {
let startTime = 0;
// 已有实例直接返回,避免无谓轮询
if (tab.componentInstance) {
return Promise.resolve(tab);
}
// 等待组件实例就绪后返回 tab 配置
return new Promise((resolve) => {
let attempts = 0;
const maxAttempts = 100; // 最多重试100次防止无限轮询
const checkInstance = () => {
startTime += 10 ;
if (tab.componentInstance) {
resolve(tab);
} else if (startTime < 2000) {
setTimeout(checkInstance, 10);
} else if (++attempts < maxAttempts) {
setTimeout(checkInstance, 50);
} else {
reject(new Error(`等待组件实例超时: ${tab.key}`));
// 超时仍返回 tab避免 Promise 永远 pending
resolve(tab);
}
};
nextTick().then(() => checkInstance());
@@ -116,6 +122,11 @@ export function createResourceOpTab(tab: ResourceOpTab): Promise<ResourceOpTab>
* @param key tab key
*/
export function removeResourceOpTab(key: string) {
const tab = allResourceOpTabs.get(key);
if (tab) {
// 清除实例引用,辅助 GC 回收组件实例
tab.componentInstance = undefined;
}
allResourceOpTabs.delete(key);
}