Files
EasyFlow/easyflow-ui-admin/app/src/views/datacenter/DatacenterWorkspace.vue
陈子默 219e4f7eff feat: 优化数据中枢 Excel 连接创建交互
- 在新增连接中完成 Excel 上传并自动填写连接名称

- 限制上传格式并提供空表和无表头错误反馈

- 移除旧导入入口和无效测试连接操作
2026-08-03 11:39:40 +08:00

356 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>