feat: 重构数据中枢工作台与接入管理
- 新增统一的数据源、目录、纳管表与 Excel 处理后端能力 - 重建管理端数据中枢工作台并替换旧表管理页面 - 补充数据中枢迁移脚本、连接器底座与说明字段支持
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user