mirror of
https://gitee.com/dromara/mayfly-go
synced 2026-06-04 17:25:21 +08:00
refactor: 资源操作tab优化
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 直接打开文件管理 tab,FileTab 内部会处理配置选择
|
||||
const tabKey = `machine.${m.code}.${acName}`;
|
||||
const tabKey = `${m.code}.${acName}`;
|
||||
createResourceOpTab({
|
||||
key: tabKey,
|
||||
name: `${m.selectAuthCert.username}@${m.name}`,
|
||||
|
||||
@@ -108,6 +108,7 @@ const blur = () => {
|
||||
defineExpose({
|
||||
onRefresh: handleReconnect,
|
||||
onActivate: focus,
|
||||
onResize: fitTerminal,
|
||||
close,
|
||||
fitTerminal,
|
||||
focus,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user