356 lines
8.9 KiB
Vue
356 lines
8.9 KiB
Vue
<script setup lang="ts">
|
||
import type { TreeNode } from './composables/use-connection-tree';
|
||
|
||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||
|
||
import {
|
||
ResizableHandle,
|
||
ResizablePanel,
|
||
ResizablePanelGroup,
|
||
} from '@easyflow-core/shadcn-ui';
|
||
|
||
import { ElEmpty, ElMessage, ElMessageBox } from 'element-plus';
|
||
|
||
import ConnectionTree from './components/ConnectionTree.vue';
|
||
import SourceFormDrawer from './components/SourceFormDrawer.vue';
|
||
import TableDetailView from './components/TableDetailView.vue';
|
||
import TableListView from './components/TableListView.vue';
|
||
import { useConnectionTree } from './composables/use-connection-tree';
|
||
import { useDatacenterSources } from './composables/use-datacenter-sources';
|
||
import { useDatacenterTables } from './composables/use-datacenter-tables';
|
||
|
||
const sourceFormRef = ref<InstanceType<typeof SourceFormDrawer>>();
|
||
const sourceFormVisible = ref(false);
|
||
const workspaceRef = ref<HTMLElement>();
|
||
const workspaceHeight = ref('100%');
|
||
|
||
const {
|
||
sources,
|
||
selectedSourceId,
|
||
selectedSource,
|
||
loading,
|
||
saving,
|
||
testing,
|
||
importExcelSource,
|
||
loadSources,
|
||
removeSource,
|
||
saveSource,
|
||
testConnection,
|
||
markSourceRuntimeUnavailable,
|
||
clearSourceRuntimeUnavailable,
|
||
} = useDatacenterSources();
|
||
|
||
const {
|
||
sourceTables,
|
||
managedTables,
|
||
schema,
|
||
previewRows,
|
||
jobs,
|
||
sourceUnavailable,
|
||
selectedCatalogId,
|
||
selectedTableId,
|
||
selectedTable,
|
||
previewLoading,
|
||
loadTableRuntime,
|
||
syncSourceContext,
|
||
batchRegisterTables,
|
||
batchRemoveTables,
|
||
saveDescriptions,
|
||
} = useDatacenterTables(selectedSourceId, {
|
||
markSourceRuntimeUnavailable,
|
||
clearSourceRuntimeUnavailable,
|
||
});
|
||
|
||
async function reloadAll(options?: {
|
||
focus?: 'source' | 'table';
|
||
resetTable?: boolean;
|
||
}) {
|
||
await loadSources();
|
||
if (selectedSourceId.value === null) {
|
||
selectedCatalogId.value = null;
|
||
selectedTableId.value = null;
|
||
selectedNodeKey.value = '';
|
||
viewMode.value = 'empty';
|
||
await syncSourceContext();
|
||
return;
|
||
}
|
||
if (options?.resetTable) {
|
||
selectedTableId.value = null;
|
||
}
|
||
await syncSourceContext();
|
||
if (options?.focus === 'table' && selectedTable.value) {
|
||
selectedNodeKey.value = `table-${selectedTable.value.id}`;
|
||
viewMode.value = 'detail';
|
||
return;
|
||
}
|
||
selectedNodeKey.value = `source-${selectedSourceId.value}`;
|
||
viewMode.value = 'list';
|
||
}
|
||
|
||
// 视图状态:empty | list | detail
|
||
const selectedNodeKey = ref('');
|
||
const viewMode = ref<'detail' | 'empty' | 'list'>('empty');
|
||
|
||
const { treeData, parseNodeKey } = useConnectionTree(sources);
|
||
|
||
async function handleNodeSelect(node: TreeNode) {
|
||
selectedNodeKey.value = node.id;
|
||
|
||
switch (node.type) {
|
||
case 'source': {
|
||
const { id } = parseNodeKey(node.id);
|
||
if (id !== selectedSourceId.value) {
|
||
selectedSourceId.value = id;
|
||
selectedCatalogId.value = null;
|
||
selectedTableId.value = null;
|
||
await syncSourceContext();
|
||
}
|
||
viewMode.value = 'list';
|
||
|
||
break;
|
||
}
|
||
case 'table': {
|
||
const { id } = parseNodeKey(node.id);
|
||
selectedTableId.value = id;
|
||
await loadTableRuntime();
|
||
viewMode.value = 'detail';
|
||
|
||
break;
|
||
}
|
||
// No default
|
||
}
|
||
}
|
||
|
||
async function handleBatchRegister(rows: any[]) {
|
||
await batchRegisterTables(rows);
|
||
}
|
||
|
||
async function handleBatchRemove(rows: any[]) {
|
||
const removedIds = rows.map((row) => row?.id).filter(Boolean);
|
||
await batchRemoveTables(rows);
|
||
if (
|
||
selectedTableId.value !== null &&
|
||
removedIds.includes(selectedTableId.value)
|
||
) {
|
||
viewMode.value = 'list';
|
||
}
|
||
}
|
||
|
||
async function handleSaveTableDescription(
|
||
tableId: number | string,
|
||
tableDesc: string,
|
||
) {
|
||
await saveDescriptions({
|
||
tableId,
|
||
tableDesc,
|
||
fields: [],
|
||
});
|
||
}
|
||
|
||
function handleSelectTable(row: any) {
|
||
selectedTableId.value = row.id;
|
||
selectedNodeKey.value = `table-${row.id}`;
|
||
viewMode.value = 'detail';
|
||
loadTableRuntime();
|
||
}
|
||
|
||
function handleBackToList() {
|
||
viewMode.value = 'list';
|
||
if (selectedSource.value) {
|
||
selectedNodeKey.value = `source-${selectedSource.value.id}`;
|
||
}
|
||
}
|
||
|
||
function openCreate() {
|
||
sourceFormRef.value?.open();
|
||
sourceFormVisible.value = true;
|
||
}
|
||
|
||
function openEdit(node: TreeNode) {
|
||
if (node.type !== 'source') return;
|
||
sourceFormRef.value?.open(node.meta);
|
||
sourceFormVisible.value = true;
|
||
}
|
||
|
||
async function handleRemoveSource(node: TreeNode) {
|
||
if (node.type !== 'source' || !node.meta?.id) return;
|
||
await ElMessageBox.confirm(`确认删除“${node.label}”吗?`, '删除连接', {
|
||
type: 'warning',
|
||
confirmButtonText: '删除',
|
||
cancelButtonText: '取消',
|
||
confirmButtonClass: 'el-button--danger',
|
||
});
|
||
const removingCurrent = selectedSourceId.value === node.meta.id;
|
||
await removeSource(node.meta.id);
|
||
if (removingCurrent) {
|
||
selectedTableId.value = null;
|
||
}
|
||
await reloadAll({ focus: 'source', resetTable: true });
|
||
}
|
||
|
||
async function handleSaveSource(
|
||
formData: Record<string, any>,
|
||
excelFile?: File,
|
||
) {
|
||
if (formData.sourceType === 'EXCEL' && !formData.id) {
|
||
if (!excelFile) return;
|
||
await importExcelSource(excelFile, formData.sourceName);
|
||
ElMessage.success('Excel 已导入');
|
||
} else {
|
||
await saveSource(formData);
|
||
ElMessage.success(formData.id ? '连接已更新' : '连接已创建');
|
||
}
|
||
sourceFormRef.value?.close();
|
||
sourceFormVisible.value = false;
|
||
await nextTick();
|
||
await reloadAll({ focus: 'source', resetTable: true });
|
||
}
|
||
|
||
async function handleTestConnection(formData: Record<string, any>) {
|
||
const result = await testConnection(formData);
|
||
sourceFormRef.value?.setTestResult(result);
|
||
if (formData.id) await loadSources();
|
||
}
|
||
|
||
async function loadAll() {
|
||
loading.value = true;
|
||
try {
|
||
await reloadAll({ focus: 'source', resetTable: true });
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function updateWorkspaceHeight() {
|
||
const element = workspaceRef.value;
|
||
if (!element) return;
|
||
const top = element.getBoundingClientRect().top;
|
||
workspaceHeight.value = `${Math.max(window.innerHeight - top, 480)}px`;
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadAll();
|
||
await nextTick();
|
||
updateWorkspaceHeight();
|
||
window.addEventListener('resize', updateWorkspaceHeight);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('resize', updateWorkspaceHeight);
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
ref="workspaceRef"
|
||
class="datacenter-workspace"
|
||
:style="{ height: workspaceHeight }"
|
||
v-loading="loading"
|
||
>
|
||
<!-- 双栏主体 -->
|
||
<ResizablePanelGroup direction="horizontal" class="workspace-body">
|
||
<ResizablePanel :default-size="20" :min-size="16" :max-size="30">
|
||
<ConnectionTree
|
||
:tree-data="treeData"
|
||
:selected-key="selectedNodeKey"
|
||
@create="openCreate"
|
||
@edit="openEdit"
|
||
@refresh="loadAll"
|
||
@remove="handleRemoveSource"
|
||
@select="handleNodeSelect"
|
||
/>
|
||
</ResizablePanel>
|
||
|
||
<ResizableHandle />
|
||
|
||
<ResizablePanel :default-size="80" :min-size="44">
|
||
<TableListView
|
||
v-if="viewMode === 'list'"
|
||
:source-tables="sourceTables"
|
||
:managed-tables="managedTables"
|
||
:save-table-description="handleSaveTableDescription"
|
||
:source-label="selectedSource?.sourceName || ''"
|
||
:source-unavailable="sourceUnavailable"
|
||
@register-tables="handleBatchRegister"
|
||
@remove-tables="handleBatchRemove"
|
||
@select-table="handleSelectTable"
|
||
/>
|
||
<TableDetailView
|
||
v-else-if="viewMode === 'detail' && selectedTable"
|
||
:table="selectedTable"
|
||
:schema="schema"
|
||
:preview-rows="previewRows"
|
||
:jobs="jobs"
|
||
:loading="previewLoading"
|
||
:save-descriptions="saveDescriptions"
|
||
:source-unavailable="sourceUnavailable"
|
||
@back="handleBackToList"
|
||
/>
|
||
<div v-else class="empty-state">
|
||
<ElEmpty description="从左侧选择连接或表开始浏览" />
|
||
</div>
|
||
</ResizablePanel>
|
||
</ResizablePanelGroup>
|
||
|
||
<!-- Drawers -->
|
||
<SourceFormDrawer
|
||
ref="sourceFormRef"
|
||
v-model:visible="sourceFormVisible"
|
||
:saving="saving"
|
||
:testing="testing"
|
||
@save="handleSaveSource"
|
||
@test="handleTestConnection"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.datacenter-workspace {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
min-height: 0;
|
||
padding-top: 12px;
|
||
margin-top: 0;
|
||
overflow: hidden;
|
||
background: transparent;
|
||
}
|
||
|
||
.workspace-body {
|
||
flex: 1;
|
||
width: 100%;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
background: transparent;
|
||
}
|
||
|
||
.workspace-body :deep([data-panel-group-direction='horizontal']) {
|
||
height: 100%;
|
||
}
|
||
|
||
.workspace-body :deep([data-panel-resize-handle-enabled]) {
|
||
position: relative;
|
||
width: 10px;
|
||
background: transparent;
|
||
}
|
||
|
||
.workspace-body :deep([data-panel-resize-handle-enabled]::before) {
|
||
position: absolute;
|
||
top: 20px;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
width: 1px;
|
||
content: '';
|
||
background: hsl(var(--border) / 32%);
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
background: transparent;
|
||
}
|
||
</style>
|