Files
EasyFlow/easyflow-ui-admin/app/src/views/datacenter/components/TableListView.vue
陈子默 798effbd5b feat: 重构数据中枢工作台与接入管理
- 新增统一的数据源、目录、纳管表与 Excel 处理后端能力

- 重建管理端数据中枢工作台并替换旧表管理页面

- 补充数据中枢迁移脚本、连接器底座与说明字段支持
2026-04-02 18:55:31 +08:00

415 lines
10 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue';
import { Delete, EditPen, Search } from '@element-plus/icons-vue';
import {
ElButton,
ElEmpty,
ElIcon,
ElInput,
ElTable,
ElTableColumn,
ElTag,
} from 'element-plus';
const props = defineProps<{
managedTables: any[];
saveTableDescription: (
tableId: number | string,
tableDesc: string,
) => Promise<any>;
sourceLabel: string;
sourceTables: any[];
sourceUnavailable?: boolean;
}>();
const emit = defineEmits<{
registerTables: [rows: any[]];
removeTables: [rows: any[]];
selectTable: [row: any];
}>();
const tableKeyword = ref('');
const selectedRows = ref<any[]>([]);
const editingTableId = ref<null | number | string>(null);
const editingTableDesc = ref('');
const savingTableId = ref<null | number | string>(null);
const managedTableMap = computed(
() => new Map(props.managedTables.map((item) => [item.tableName, item])),
);
function compareTableName(a?: string, b?: string) {
return String(a || '').localeCompare(String(b || ''), 'en', {
numeric: true,
sensitivity: 'base',
});
}
const tableRows = computed(() => {
const keyword = tableKeyword.value.trim().toLowerCase();
return props.sourceTables
.filter((item) => {
if (!keyword) return true;
return item.tableName?.toLowerCase().includes(keyword);
})
.map((item) => {
const managedTable = managedTableMap.value.get(item.tableName) || null;
return {
...item,
managedTable,
managedTableId: managedTable?.id || null,
managedStatus: managedTable ? 'MANAGED' : 'UNMANAGED',
};
})
.sort((a, b) => {
const managedDiff =
Number(Boolean(b.managedTable)) - Number(Boolean(a.managedTable));
if (managedDiff !== 0) {
return managedDiff;
}
return compareTableName(a.tableName, b.tableName);
});
});
const selectedManagedRows = computed(() =>
selectedRows.value.map((row) => row.managedTable).filter(Boolean),
);
const selectedPendingRows = computed(() =>
selectedRows.value.filter((row) => !row.managedTable),
);
const canBatchRegister = computed(() => selectedPendingRows.value.length > 0);
const canBatchRemove = computed(() => selectedManagedRows.value.length > 0);
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows;
}
function handleBatchRegister() {
emit('registerTables', selectedPendingRows.value);
}
function handleBatchRemove() {
emit('removeTables', selectedManagedRows.value);
}
function handleOpenDetail(row: any) {
if (row.managedTable) {
emit('selectTable', row.managedTable);
}
}
function startEdit(row: any) {
if (!row?.managedTable) return;
editingTableId.value = row.managedTable.id;
editingTableDesc.value = row.managedTable.tableDesc || '';
}
function cancelEdit() {
editingTableId.value = null;
editingTableDesc.value = '';
}
async function handleSaveTableDescription(row: any) {
if (!row?.managedTable || savingTableId.value) return;
savingTableId.value = row.managedTable.id;
try {
await props.saveTableDescription(
row.managedTable.id,
editingTableDesc.value,
);
cancelEdit();
} finally {
savingTableId.value = null;
}
}
</script>
<template>
<div class="table-list-view">
<div class="view-header">
<h3 class="view-title">{{ sourceLabel }}</h3>
</div>
<div class="view-toolbar">
<div class="table-search-row">
<ElInput v-model="tableKeyword" placeholder="搜索表" clearable>
<template #prefix>
<ElIcon><Search /></ElIcon>
</template>
</ElInput>
</div>
<div class="batch-actions">
<span v-if="selectedRows.length > 0" class="selection-text">
已选 {{ selectedRows.length }}
</span>
<ElButton
size="small"
:disabled="!canBatchRegister"
@click="handleBatchRegister"
>
批量接入
</ElButton>
<ElButton
size="small"
:disabled="!canBatchRemove"
@click="handleBatchRemove"
>
批量去除
</ElButton>
</div>
</div>
<div class="table-scroll-region">
<ElEmpty
v-if="sourceTables.length === 0"
description="当前连接下还没有可浏览的表"
/>
<ElEmpty v-else-if="tableRows.length === 0" description="没有匹配的表" />
<ElTable
v-else
:data="tableRows"
height="100%"
size="small"
row-key="tableName"
class="flat-table"
@selection-change="handleSelectionChange"
>
<ElTableColumn type="selection" width="52" />
<ElTableColumn
label="名称"
min-width="360"
class-name="table-name-column"
>
<template #default="{ row }">
<div class="name-cell">
<span class="table-name-text">{{ row.tableName }}</span>
<template v-if="row.managedTable">
<template v-if="editingTableId === row.managedTable.id">
<ElInput
v-model="editingTableDesc"
size="small"
clearable
maxlength="200"
class="description-input"
/>
<div class="inline-actions">
<ElButton
link
type="primary"
size="small"
:loading="savingTableId === row.managedTable.id"
@click="handleSaveTableDescription(row)"
>
保存
</ElButton>
<ElButton
link
size="small"
:disabled="savingTableId === row.managedTable.id"
@click="cancelEdit"
>
取消
</ElButton>
</div>
</template>
<template v-else>
<span class="description-inline">{{
row.managedTable.tableDesc || ''
}}</span>
<ElButton
class="icon-action icon-action--edit"
link
size="small"
@click="startEdit(row)"
>
<ElIcon><EditPen /></ElIcon>
</ElButton>
</template>
</template>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="状态" width="100">
<template #default="{ row }">
<ElTag
size="small"
effect="plain"
:type="row.managedTable ? 'primary' : 'info'"
>
{{ row.managedTable ? '已接入' : '未接入' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="180" fixed="right">
<template #default="{ row }">
<div class="row-actions">
<ElButton
v-if="row.managedTable"
link
type="primary"
size="small"
@click="handleOpenDetail(row)"
>
查看
</ElButton>
<ElButton
v-if="row.managedTable"
class="icon-action icon-action--danger"
link
size="small"
@click="emit('removeTables', [row.managedTable])"
>
<ElIcon><Delete /></ElIcon>
</ElButton>
<ElButton
v-else
link
type="primary"
size="small"
@click="emit('registerTables', [row])"
>
接入
</ElButton>
</div>
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
</template>
<style scoped>
.table-list-view {
display: flex;
flex-direction: column;
height: 100%;
padding: 0 0 0 12px;
background: transparent;
}
.view-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 10px;
padding: 0 0 4px;
}
.view-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: hsl(var(--text-strong));
}
.view-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.table-search-row {
width: min(320px, 100%);
}
.table-search-row :deep(.el-input__wrapper) {
min-height: 36px;
}
.batch-actions {
display: flex;
align-items: center;
gap: 8px;
}
.selection-text {
font-size: 13px;
color: hsl(var(--text-muted));
}
.table-scroll-region {
flex: 1;
min-height: 0;
overflow: hidden;
}
.flat-table {
--el-table-bg-color: transparent;
--el-table-tr-bg-color: transparent;
--el-table-header-bg-color: hsl(var(--surface-subtle) / 0.72);
--el-table-border-color: hsl(var(--table-row-border) / 0.7);
--el-table-current-row-bg-color: hsl(var(--table-row-hover));
--el-fill-color-blank: transparent;
height: 100%;
overflow: hidden;
border-radius: 0;
}
.flat-table :deep(.table-name-column .cell) {
font-size: 14px;
font-weight: 400;
color: hsl(var(--text-strong));
}
.name-cell {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.table-name-text {
flex-shrink: 0;
color: hsl(var(--text-strong));
}
.description-inline {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: hsl(var(--text-muted));
}
.description-input {
width: min(320px, 100%);
}
.row-actions {
display: flex;
align-items: center;
gap: 4px;
}
.inline-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.icon-action {
min-width: auto;
padding: 0 2px;
}
.icon-action :deep(.el-icon) {
font-size: 15px;
}
.icon-action--edit {
color: hsl(var(--primary));
}
.icon-action--danger {
color: hsl(var(--destructive));
}
</style>