feat: 重构数据中枢工作台与接入管理
- 新增统一的数据源、目录、纳管表与 Excel 处理后端能力 - 重建管理端数据中枢工作台并替换旧表管理页面 - 补充数据中枢迁移脚本、连接器底座与说明字段支持
This commit is contained in:
@@ -1,154 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadFile } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||
import { downloadFileFromBlob } from '@easyflow/utils';
|
||||
|
||||
import { ElButton, ElMessage, ElMessageBox, ElUpload } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import uploadIcon from '#/assets/datacenter/upload.png';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = withDefaults(defineProps<BatchImportModalProps>(), {
|
||||
title: 'title',
|
||||
});
|
||||
const emit = defineEmits(['reload']);
|
||||
export interface BatchImportModalProps {
|
||||
tableId: any;
|
||||
title?: string;
|
||||
}
|
||||
// vue
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const downloadLoading = ref(false);
|
||||
const fileList = ref<any[]>([]);
|
||||
const currentFile = ref<File | null>();
|
||||
const btnLoading = ref(false);
|
||||
// functions
|
||||
function openDialog() {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function closeDialog() {
|
||||
fileList.value = [];
|
||||
currentFile.value = null;
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
function onFileChange(uploadFile: UploadFile) {
|
||||
currentFile.value = uploadFile.raw;
|
||||
return false;
|
||||
}
|
||||
function downloadTemplate() {
|
||||
downloadLoading.value = true;
|
||||
api
|
||||
.download(`/api/v1/datacenterTable/getTemplate?tableId=${props.tableId}`)
|
||||
.then((res) => {
|
||||
downloadLoading.value = false;
|
||||
downloadFileFromBlob({
|
||||
fileName: 'template.xlsx',
|
||||
source: res,
|
||||
});
|
||||
});
|
||||
}
|
||||
function handleUpload() {
|
||||
if (!currentFile.value) {
|
||||
ElMessage.warning($t('datacenterTable.uploadDesc'));
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', currentFile.value);
|
||||
formData.append('tableId', props.tableId);
|
||||
btnLoading.value = true;
|
||||
api.postFile('/api/v1/datacenterTable/importData', formData).then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
const arr: any[] = res.data.errorRows;
|
||||
let html = '';
|
||||
for (const element of arr) {
|
||||
html += `<p>${JSON.stringify(element)}</p><br>`;
|
||||
}
|
||||
closeDialog();
|
||||
ElMessageBox.alert(
|
||||
`<strong>${$t('datacenterTable.totalNum')}:</strong>${res.data.totalCount}
|
||||
<strong>${$t('datacenterTable.successNum')}:</strong>${res.data.successCount}
|
||||
<strong>${$t('datacenterTable.failNum')}:</strong>${res.data.errorCount}<br>
|
||||
<strong>${$t('datacenterTable.failList')}:</strong>${html}`,
|
||||
$t('datacenterTable.importComplete'),
|
||||
{
|
||||
confirmButtonText: $t('message.ok'),
|
||||
dangerouslyUseHTMLString: true,
|
||||
callback: () => {
|
||||
emit('reload');
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EasyFlowPanelModal
|
||||
v-model:open="dialogVisible"
|
||||
:closable="!btnLoading"
|
||||
:title="props.title"
|
||||
:before-close="closeDialog"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<ElUpload
|
||||
:file-list="fileList"
|
||||
drag
|
||||
action="#"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
:auto-upload="false"
|
||||
:on-change="onFileChange"
|
||||
:limit="1"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<img alt="" :src="uploadIcon" class="h-12 w-12" />
|
||||
<div class="text-base font-medium">
|
||||
{{ $t('datacenterTable.uploadTitle') }}
|
||||
</div>
|
||||
<div class="desc text-[13px]">
|
||||
{{ $t('datacenterTable.uploadDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</ElUpload>
|
||||
<ElButton
|
||||
:disabled="downloadLoading"
|
||||
type="primary"
|
||||
link
|
||||
@click="downloadTemplate"
|
||||
>
|
||||
{{ $t('datacenterTable.downloadTemplate') }}
|
||||
</ElButton>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="closeDialog">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
:disabled="btnLoading"
|
||||
:loading="btnLoading"
|
||||
type="primary"
|
||||
@click="handleUpload"
|
||||
>
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</EasyFlowPanelModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desc {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,272 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {
|
||||
ArrowLeft,
|
||||
Delete,
|
||||
MoreFilled,
|
||||
Plus,
|
||||
Upload,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElIcon,
|
||||
ElImage,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import tableIcon from '#/assets/datacenter/table2x.png';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import { router } from '#/router';
|
||||
import { useDictStore } from '#/store';
|
||||
import BatchImportModal from '#/views/datacenter/BatchImportModal.vue';
|
||||
import RecordModal from '#/views/datacenter/RecordModal.vue';
|
||||
|
||||
const pageDataRef = ref();
|
||||
const route = useRoute();
|
||||
const tableId = ref(route.query.tableId);
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
getDetailInfo(tableId.value);
|
||||
getHeaders(tableId.value);
|
||||
});
|
||||
const detailInfo = ref<any>({});
|
||||
const fieldList = ref<any[]>([]);
|
||||
const headers = ref<any[]>([]);
|
||||
const activeMenu = ref('2');
|
||||
const recordModal = ref();
|
||||
const batchImportModal = ref();
|
||||
const dictStore = useDictStore();
|
||||
const categoryData = [
|
||||
{ key: '2', name: $t('datacenterTable.data') },
|
||||
{ key: '1', name: $t('datacenterTable.structure') },
|
||||
];
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('fieldType');
|
||||
dictStore.fetchDictionary('yesOrNo');
|
||||
}
|
||||
function getDetailInfo(id: any) {
|
||||
api.get(`/api/v1/datacenterTable/detailInfo?tableId=${id}`).then((res) => {
|
||||
detailInfo.value = res.data;
|
||||
fieldList.value = res.data.fields;
|
||||
});
|
||||
}
|
||||
function getHeaders(id: any) {
|
||||
api.get(`/api/v1/datacenterTable/getHeaders?tableId=${id}`).then((res) => {
|
||||
headers.value = res.data;
|
||||
});
|
||||
}
|
||||
function showDialog(row: any) {
|
||||
recordModal.value.openDialog({ ...row });
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post(
|
||||
`/api/v1/datacenterTable/removeValue`,
|
||||
{},
|
||||
{
|
||||
params: {
|
||||
tableId: tableId.value,
|
||||
id: row.id,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
refresh();
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function refresh() {
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function openImportModal() {
|
||||
batchImportModal.value.openDialog();
|
||||
}
|
||||
function changeTab(category: any) {
|
||||
activeMenu.value = category.key;
|
||||
if (category.key === '2' && pageDataRef.value) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-background-deep flex h-full flex-col gap-6 p-6">
|
||||
<BatchImportModal
|
||||
:table-id="tableId"
|
||||
:title="$t('button.batchImport')"
|
||||
ref="batchImportModal"
|
||||
@reload="refresh"
|
||||
/>
|
||||
<RecordModal
|
||||
ref="recordModal"
|
||||
:form-items="headers"
|
||||
:table-id="tableId"
|
||||
@reload="refresh"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<ElIcon class="cursor-pointer" @click="router.back()">
|
||||
<ArrowLeft />
|
||||
</ElIcon>
|
||||
<div class="flex items-center gap-3">
|
||||
<ElImage :src="tableIcon" class="h-9 w-9 rounded-full" />
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="text-base font-medium">{{ detailInfo.tableName }}</div>
|
||||
<div class="desc text-sm">{{ detailInfo.tableDesc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center" v-if="activeMenu === '2'">
|
||||
<ElButton type="primary" @click="showDialog({})">
|
||||
<ElIcon class="mr-1">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.addLine') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="openImportModal">
|
||||
<ElIcon class="mr-1">
|
||||
<Upload />
|
||||
</ElIcon>
|
||||
{{ $t('button.batchImport') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex h-full max-h-[calc(100vh-191px)] gap-3">
|
||||
<PageSide
|
||||
label-key="name"
|
||||
value-key="key"
|
||||
:menus="categoryData"
|
||||
default-selected="2"
|
||||
@change="changeTab"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="bg-background border-border flex-1 overflow-auto rounded-lg border p-5"
|
||||
>
|
||||
<ElTable v-show="activeMenu === '1'" :data="fieldList">
|
||||
<ElTableColumn
|
||||
prop="fieldName"
|
||||
:label="$t('datacenterTableFields.fieldName')"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="fieldDesc"
|
||||
:label="$t('datacenterTableFields.fieldDesc')"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="fieldType"
|
||||
:label="$t('datacenterTableFields.fieldType')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ dictStore.getDictLabel('fieldType', row.fieldType) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="required"
|
||||
:label="$t('datacenterTableFields.required')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ dictStore.getDictLabel('yesOrNo', row.required) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<PageData
|
||||
v-show="activeMenu === '2'"
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/datacenterTable/getPageData"
|
||||
:extra-query-params="{ tableId }"
|
||||
:page-size="10"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList">
|
||||
<ElTableColumn
|
||||
v-for="item in headers"
|
||||
:key="item.key"
|
||||
:prop="item.key"
|
||||
:label="item.title"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div v-if="item.fieldType === 5">
|
||||
{{
|
||||
1 === row[item.key] ? $t('common.yes') : $t('common.no')
|
||||
}}
|
||||
</div>
|
||||
<div v-else-if="item.fieldType === 4">
|
||||
{{ parseFloat(row[item.key]) }}
|
||||
</div>
|
||||
<div v-else>{{ row[item.key] }}</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('common.handle')"
|
||||
width="90"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="showDialog(row)">
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="remove(row)">
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desc {
|
||||
color: #75808d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,204 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import {markRaw, onMounted, ref} from 'vue';
|
||||
|
||||
import {Delete, MoreFilled, Plus} from '@element-plus/icons-vue';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import {$t} from '#/locales';
|
||||
import {router} from '#/router';
|
||||
import {useDictStore} from '#/store';
|
||||
|
||||
import DatacenterTableModal from './DatacenterTableModal.vue';
|
||||
|
||||
onMounted(() => {
|
||||
initDict();
|
||||
});
|
||||
|
||||
const pageDataRef = ref();
|
||||
const saveDialog = ref();
|
||||
const dictStore = useDictStore();
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'create',
|
||||
text: $t('button.add'),
|
||||
icon: markRaw(Plus),
|
||||
type: 'primary',
|
||||
data: { action: 'create' },
|
||||
permission: '/api/v1/datacenterTable/save',
|
||||
},
|
||||
];
|
||||
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ tableName: params, isQueryOr: true });
|
||||
};
|
||||
function reset(formEl?: FormInstance) {
|
||||
formEl?.resetFields();
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
function showDialog(row: any) {
|
||||
saveDialog.value.openDialog({ ...row });
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.get(`/api/v1/datacenterTable/removeTable?tableId=${row.id}`)
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset();
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function toDetailPage(row: any) {
|
||||
router.push({
|
||||
name: 'TableDetail',
|
||||
query: {
|
||||
tableId: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="datacenter-table-page flex h-full flex-col gap-6 p-6">
|
||||
<DatacenterTableModal ref="saveDialog" @reload="reset" />
|
||||
<ListPageShell>
|
||||
<template #filters>
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="showDialog({})"
|
||||
/>
|
||||
</template>
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/datacenterTable/page"
|
||||
:page-size="10"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<ElTable :data="pageList" border>
|
||||
<ElTableColumn
|
||||
prop="tableName"
|
||||
:label="$t('datacenterTable.tableName')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.tableName }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="tableDesc"
|
||||
:label="$t('datacenterTable.tableDesc')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.tableDesc }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="created"
|
||||
:label="$t('datacenterTable.created')"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.created }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('common.handle')"
|
||||
width="140"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center">
|
||||
<ElButton link type="primary" @click="toDetailPage(row)">
|
||||
{{ $t('button.view') }}
|
||||
</ElButton>
|
||||
<ElButton link type="primary" @click="showDialog(row)">
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem @click="remove(row)">
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</template>
|
||||
</PageData>
|
||||
</ListPageShell>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.datacenter-table-page {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.datacenter-table-page::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse 76% 34% at 2% 0%,
|
||||
hsl(var(--nav-ambient) / 0.16) 0%,
|
||||
transparent 64%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse 56% 22% at 24% 12%,
|
||||
hsl(var(--nav-flow-core) / 0.08) 0%,
|
||||
transparent 68%
|
||||
);
|
||||
}
|
||||
|
||||
.datacenter-table-page > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,272 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
// vue
|
||||
onMounted(() => {
|
||||
getFieldType();
|
||||
getYesOrNo();
|
||||
});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>({
|
||||
deptId: '',
|
||||
tableName: '',
|
||||
tableDesc: '',
|
||||
actualTable: '',
|
||||
status: '',
|
||||
options: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
tableName: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-z][a-z0-9_]*$/,
|
||||
message: $t('datacenterTable.nameRegx'),
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_: any, value: any, callback: any) => {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error($t('datacenterTable.noFieldError')));
|
||||
} else {
|
||||
// 检查字段数组中的fieldName和fieldDesc字段
|
||||
value.forEach((field: any) => {
|
||||
if (!field.fieldName || !field.fieldDesc) {
|
||||
callback(new Error($t('datacenterTable.fieldInfoError')));
|
||||
}
|
||||
if (!/^[a-z][a-z0-9_]*$/.test(field.fieldName)) {
|
||||
callback(new Error($t('datacenterTable.nameRegx')));
|
||||
}
|
||||
});
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
});
|
||||
const fieldsData = ref();
|
||||
const removeFields = ref<any[]>([]);
|
||||
const loadFields = ref(false);
|
||||
const fieldTypes = ref<any>([]);
|
||||
const yesOrNoDict = ref<any>([]);
|
||||
watch(
|
||||
() => fieldsData.value,
|
||||
(newVal) => {
|
||||
entity.value.fields = newVal;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// functions
|
||||
function getDetailInfo(tableId: any) {
|
||||
loadFields.value = true;
|
||||
api
|
||||
.get(`/api/v1/datacenterTable/detailInfo?tableId=${tableId}`)
|
||||
.then((res) => {
|
||||
loadFields.value = false;
|
||||
fieldsData.value = res.data.fields;
|
||||
});
|
||||
}
|
||||
function openDialog(row: any) {
|
||||
fieldsData.value = [];
|
||||
removeFields.value = [];
|
||||
if (row.id) {
|
||||
getDetailInfo(row.id);
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
if (fieldsData.value.length === 0) {
|
||||
ElMessage.error($t('message.required'));
|
||||
return;
|
||||
}
|
||||
const obj = {
|
||||
...entity.value,
|
||||
fields: [...fieldsData.value, ...removeFields.value],
|
||||
};
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.post('/api/v1/datacenterTable/saveTable', obj)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
function addField() {
|
||||
fieldsData.value.push({
|
||||
fieldName: '',
|
||||
fieldDesc: '',
|
||||
fieldType: 1,
|
||||
required: 0,
|
||||
handleDelete: false,
|
||||
rowKey: Date.now().toString(),
|
||||
});
|
||||
}
|
||||
function deleteField(row: any, $index: number) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, _, done) => {
|
||||
if (action === 'confirm') {
|
||||
if (row.id) {
|
||||
row.handleDelete = true;
|
||||
removeFields.value.push(row);
|
||||
}
|
||||
fieldsData.value.splice($index, 1);
|
||||
done();
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
function getFieldType() {
|
||||
api.get('/api/v1/dict/items/fieldType').then((res) => {
|
||||
fieldTypes.value = res.data;
|
||||
});
|
||||
}
|
||||
function getYesOrNo() {
|
||||
api.get('/api/v1/dict/items/yesOrNo').then((res) => {
|
||||
yesOrNoDict.value = res.data;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EasyFlowFormModal
|
||||
v-model:open="dialogVisible"
|
||||
:closable="!btnLoading"
|
||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||
:before-close="closeDialog"
|
||||
width="800px"
|
||||
:confirm-loading="btnLoading"
|
||||
:confirm-text="$t('button.save')"
|
||||
:submitting="btnLoading"
|
||||
@confirm="save"
|
||||
>
|
||||
<ElForm
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||
>
|
||||
<ElFormItem prop="tableName" :label="$t('datacenterTable.tableName')">
|
||||
<ElInput :disabled="!isAdd" v-model.trim="entity.tableName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="tableDesc" :label="$t('datacenterTable.tableDesc')">
|
||||
<ElInput v-model.trim="entity.tableDesc" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="fields" :label="$t('datacenterTable.fields')">
|
||||
<div v-loading="loadFields" class="w-full">
|
||||
<ElTable :data="fieldsData">
|
||||
<ElTableColumn :label="$t('datacenterTable.fieldName')">
|
||||
<template #default="{ row }">
|
||||
<ElInput v-model.trim="row.fieldName" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('datacenterTable.fieldDesc')">
|
||||
<template #default="{ row }">
|
||||
<ElInput v-model.trim="row.fieldDesc" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('datacenterTable.fieldType')">
|
||||
<template #default="{ row }">
|
||||
<ElSelect v-model.trim="row.fieldType" :disabled="!!row.id">
|
||||
<ElOption
|
||||
v-for="item in fieldTypes"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('datacenterTable.required')">
|
||||
<template #default="{ row }">
|
||||
<ElSelect v-model.trim="row.required">
|
||||
<ElOption
|
||||
v-for="item in yesOrNoDict"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('common.handle')" width="80">
|
||||
<template #default="{ row, $index }">
|
||||
<ElButton link type="danger" @click="deleteField(row, $index)">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<div class="mt-3">
|
||||
<ElButton plain type="primary" @click="addField">
|
||||
<ElIcon class="mr-1">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.add') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,539 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeNode } from './composables/use-connection-tree';
|
||||
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EasyFlowButton,
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from '@easyflow-core/shadcn-ui';
|
||||
|
||||
import {
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElEmpty,
|
||||
ElMessageBox,
|
||||
} from 'element-plus';
|
||||
|
||||
import ConnectionTree from './components/ConnectionTree.vue';
|
||||
import ExcelActionDrawer from './components/ExcelActionDrawer.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 { useDatacenterExcel } from './composables/use-datacenter-excel';
|
||||
import { useDatacenterSources } from './composables/use-datacenter-sources';
|
||||
import { useDatacenterTables } from './composables/use-datacenter-tables';
|
||||
|
||||
const sourceFormRef = ref<InstanceType<typeof SourceFormDrawer>>();
|
||||
const excelActionRef = ref<InstanceType<typeof ExcelActionDrawer>>();
|
||||
const sourceFormVisible = ref(false);
|
||||
const excelActionVisible = ref(false);
|
||||
const workspaceRef = ref<HTMLElement>();
|
||||
const workspaceHeight = ref('100%');
|
||||
|
||||
const {
|
||||
sources,
|
||||
selectedSourceId,
|
||||
selectedSource,
|
||||
loading,
|
||||
saving,
|
||||
testing,
|
||||
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) {
|
||||
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';
|
||||
}
|
||||
|
||||
const {
|
||||
actionLoading,
|
||||
pendingUploadFile,
|
||||
splitForm,
|
||||
mergeForm,
|
||||
deriveForm,
|
||||
exportForm,
|
||||
resetSplitForm,
|
||||
resetMergeForm,
|
||||
resetDeriveForm,
|
||||
resetExportForm,
|
||||
handleImport,
|
||||
handleSplit,
|
||||
handleMerge,
|
||||
handleDerive,
|
||||
handleExport,
|
||||
} = useDatacenterExcel(
|
||||
selectedSourceId,
|
||||
selectedCatalogId,
|
||||
selectedTableId,
|
||||
reloadAll,
|
||||
);
|
||||
|
||||
// 视图状态:empty | list | detail
|
||||
const selectedNodeKey = ref('');
|
||||
const viewMode = ref<'detail' | 'empty' | 'list'>('empty');
|
||||
|
||||
const isExcelContext = computed(() => {
|
||||
const st =
|
||||
selectedSource.value?.sourceType || schema.value?.source?.sourceType;
|
||||
return st === 'EXCEL' || st === 'EXCEL_MATERIALIZED';
|
||||
});
|
||||
const canImportExcel = computed(
|
||||
() => selectedSource.value?.sourceType === 'EXCEL',
|
||||
);
|
||||
const canExport = computed(() =>
|
||||
Boolean(selectedTable.value || selectedSource.value),
|
||||
);
|
||||
const canShowMoreActions = computed(
|
||||
() =>
|
||||
isExcelContext.value &&
|
||||
(Boolean(selectedTable.value) ||
|
||||
managedTables.value.length > 1 ||
|
||||
canExport.value),
|
||||
);
|
||||
const showToolbar = computed(
|
||||
() => canImportExcel.value || canShowMoreActions.value,
|
||||
);
|
||||
const { treeData, parseNodeKey } = useConnectionTree(sources);
|
||||
const selectedFieldOptions = computed(() =>
|
||||
(schema.value?.fields || []).map((f: any) => ({
|
||||
label: f.fieldName,
|
||||
value: f.fieldName,
|
||||
})),
|
||||
);
|
||||
|
||||
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 && 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>) {
|
||||
await saveSource(formData);
|
||||
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();
|
||||
}
|
||||
|
||||
function openExcelAction(
|
||||
action: 'derive' | 'export' | 'import' | 'merge' | 'split',
|
||||
) {
|
||||
switch (action) {
|
||||
case 'derive': {
|
||||
resetDeriveForm();
|
||||
break;
|
||||
}
|
||||
case 'export': {
|
||||
resetExportForm();
|
||||
break;
|
||||
}
|
||||
case 'merge': {
|
||||
resetMergeForm();
|
||||
break;
|
||||
}
|
||||
case 'split': {
|
||||
resetSplitForm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
excelActionRef.value?.open(action);
|
||||
excelActionVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleExcelAction(action: string) {
|
||||
let success = false;
|
||||
switch (action) {
|
||||
case 'derive': {
|
||||
success = await handleDerive();
|
||||
break;
|
||||
}
|
||||
case 'export': {
|
||||
success = await handleExport(loadTableRuntime);
|
||||
break;
|
||||
}
|
||||
case 'import': {
|
||||
success = await handleImport();
|
||||
break;
|
||||
}
|
||||
case 'merge': {
|
||||
success = await handleMerge();
|
||||
break;
|
||||
}
|
||||
case 'split': {
|
||||
success = await handleSplit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (success) excelActionVisible.value = false;
|
||||
}
|
||||
|
||||
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"
|
||||
>
|
||||
<!-- 顶部工具栏 -->
|
||||
<div v-if="showToolbar" class="workspace-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<EasyFlowButton
|
||||
v-if="canImportExcel"
|
||||
class="toolbar-button"
|
||||
variant="outline"
|
||||
@click="openExcelAction('import')"
|
||||
>
|
||||
导入 Excel
|
||||
</EasyFlowButton>
|
||||
<ElDropdown v-if="canShowMoreActions">
|
||||
<EasyFlowButton class="toolbar-button" variant="outline">
|
||||
更多操作
|
||||
</EasyFlowButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-if="canExport"
|
||||
@click="openExcelAction('export')"
|
||||
>
|
||||
导出
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="selectedTable"
|
||||
@click="openExcelAction('split')"
|
||||
>
|
||||
拆分
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="managedTables.length > 1"
|
||||
@click="openExcelAction('merge')"
|
||||
>
|
||||
合并
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
v-if="selectedTable"
|
||||
@click="openExcelAction('derive')"
|
||||
>
|
||||
生成新表
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 双栏主体 -->
|
||||
<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"
|
||||
/>
|
||||
|
||||
<ExcelActionDrawer
|
||||
ref="excelActionRef"
|
||||
v-model:visible="excelActionVisible"
|
||||
v-model:split-form="splitForm"
|
||||
v-model:merge-form="mergeForm"
|
||||
v-model:derive-form="deriveForm"
|
||||
v-model:export-form="exportForm"
|
||||
:action-loading="actionLoading"
|
||||
:managed-tables="managedTables"
|
||||
:field-options="selectedFieldOptions"
|
||||
:pending-upload-file="pendingUploadFile"
|
||||
@update:pending-upload-file="(f) => (pendingUploadFile = f)"
|
||||
@import="handleExcelAction('import')"
|
||||
@split="handleExcelAction('split')"
|
||||
@merge="handleExcelAction('merge')"
|
||||
@derive="handleExcelAction('derive')"
|
||||
@export="handleExcelAction('export')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.datacenter-workspace {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 12px;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workspace-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
min-width: 84px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.workspace-body {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
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) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
background: hsl(var(--border) / 0.32);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -1,145 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import {
|
||||
ElDatePicker,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = withDefaults(defineProps<RecordModalProps>(), {});
|
||||
const emit = defineEmits(['reload']);
|
||||
// vue
|
||||
export interface RecordModalProps {
|
||||
formItems: any[];
|
||||
tableId: any;
|
||||
}
|
||||
onMounted(() => {});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>({});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({});
|
||||
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
const data: Record<string, any> = {};
|
||||
for (const formItem of props.formItems) {
|
||||
data[formItem.key] = entity.value[formItem.key];
|
||||
}
|
||||
data.tableId = props.tableId;
|
||||
data.id = entity.value.id;
|
||||
btnLoading.value = true;
|
||||
api
|
||||
.postForm('/api/v1/datacenterTable/saveValue', data)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
entity.value = {};
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EasyFlowFormModal
|
||||
v-model:open="dialogVisible"
|
||||
:closable="!btnLoading"
|
||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||
:before-close="closeDialog"
|
||||
width="800px"
|
||||
:confirm-loading="btnLoading"
|
||||
:confirm-text="$t('button.save')"
|
||||
:submitting="btnLoading"
|
||||
@confirm="save"
|
||||
>
|
||||
<ElForm
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||
>
|
||||
<ElFormItem
|
||||
v-for="item in props.formItems"
|
||||
:rules="
|
||||
item.required
|
||||
? [
|
||||
{
|
||||
required: true,
|
||||
message: `${item.title}${$t('message.notEmpty')}`,
|
||||
},
|
||||
]
|
||||
: []
|
||||
"
|
||||
:key="item.key"
|
||||
:prop="item.key"
|
||||
:label="item.title"
|
||||
>
|
||||
<ElInput v-if="item.fieldType === 1" v-model.trim="entity[item.key]" />
|
||||
<ElInputNumber
|
||||
v-if="item.fieldType === 2"
|
||||
v-model.number="entity[item.key]"
|
||||
:precision="0"
|
||||
/>
|
||||
<ElDatePicker
|
||||
v-if="item.fieldType === 3"
|
||||
v-model="entity[item.key]"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
<ElInputNumber
|
||||
v-if="item.fieldType === 4"
|
||||
v-model="entity[item.key]"
|
||||
:precision="2"
|
||||
/>
|
||||
<DictSelect
|
||||
v-if="item.fieldType === 5"
|
||||
v-model="entity[item.key]"
|
||||
dict-code="yesOrNo"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,474 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeNode } from '../composables/use-connection-tree';
|
||||
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
|
||||
|
||||
import { Plus, RefreshRight, Search } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElInput, ElTooltip } from 'element-plus';
|
||||
|
||||
import { formatSourceType } from '../composables/datacenter-constants';
|
||||
import SourceBrandIcon from './SourceBrandIcon.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
selectedKey: string;
|
||||
treeData: TreeNode[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
create: [];
|
||||
edit: [node: TreeNode];
|
||||
refresh: [];
|
||||
remove: [node: TreeNode];
|
||||
select: [node: TreeNode];
|
||||
}>();
|
||||
|
||||
const searchText = ref('');
|
||||
const expandedKeys = ref<Set<string>>(new Set());
|
||||
const contextMenuVisible = ref(false);
|
||||
const contextMenuNode = ref<null | TreeNode>(null);
|
||||
const contextMenuX = ref(0);
|
||||
const contextMenuY = ref(0);
|
||||
|
||||
const filteredTree = computed(() => {
|
||||
const keyword = searchText.value.trim().toLowerCase();
|
||||
if (!keyword) return props.treeData;
|
||||
return props.treeData.filter((node) =>
|
||||
node.label.toLowerCase().includes(keyword),
|
||||
);
|
||||
});
|
||||
|
||||
function toggleExpand(nodeId: string) {
|
||||
if (expandedKeys.value.has(nodeId)) {
|
||||
expandedKeys.value.delete(nodeId);
|
||||
} else {
|
||||
expandedKeys.value.add(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(node: TreeNode) {
|
||||
if (node.children && node.children.length > 0) {
|
||||
toggleExpand(node.id);
|
||||
}
|
||||
emit('select', node);
|
||||
}
|
||||
|
||||
function handleContextMenu(event: MouseEvent, node: TreeNode) {
|
||||
event.preventDefault();
|
||||
if (node.type !== 'source' || node.meta?.builtinFlag) return;
|
||||
emit('select', node);
|
||||
contextMenuNode.value = node;
|
||||
contextMenuX.value = Math.min(event.clientX, window.innerWidth - 156);
|
||||
contextMenuY.value = Math.min(event.clientY, window.innerHeight - 108);
|
||||
contextMenuVisible.value = true;
|
||||
}
|
||||
|
||||
function isExpanded(nodeId: string) {
|
||||
return expandedKeys.value.has(nodeId);
|
||||
}
|
||||
|
||||
function closeContextMenu() {
|
||||
contextMenuVisible.value = false;
|
||||
contextMenuNode.value = null;
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
if (contextMenuNode.value) {
|
||||
emit('edit', contextMenuNode.value);
|
||||
}
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
if (contextMenuNode.value) {
|
||||
emit('remove', contextMenuNode.value);
|
||||
}
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
function getNodeIcon(node: TreeNode): string {
|
||||
if (node.type === 'source') {
|
||||
return node.icon === 'file'
|
||||
? 'i-lucide-file-spreadsheet'
|
||||
: 'i-lucide-database';
|
||||
}
|
||||
return 'i-lucide-table';
|
||||
}
|
||||
|
||||
function getSourceMetaLabel(node: TreeNode) {
|
||||
const sourceType = node.meta?.sourceType;
|
||||
const sourceName = (node.label || '').trim();
|
||||
const typeLabel = formatSourceType(sourceType);
|
||||
if (!typeLabel || typeLabel === sourceName) {
|
||||
return '';
|
||||
}
|
||||
return typeLabel;
|
||||
}
|
||||
|
||||
function isSourceUnavailable(node: TreeNode) {
|
||||
return Boolean(node.type === 'source' && node.meta?.runtimeUnavailable);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', closeContextMenu);
|
||||
window.addEventListener('resize', closeContextMenu);
|
||||
window.addEventListener('scroll', closeContextMenu, true);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', closeContextMenu);
|
||||
window.removeEventListener('resize', closeContextMenu);
|
||||
window.removeEventListener('scroll', closeContextMenu, true);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="connection-tree">
|
||||
<div class="tree-search">
|
||||
<div class="search-actions">
|
||||
<EasyFlowButton size="sm" class="create-action" @click="emit('create')">
|
||||
<ElIcon class="action-icon"><Plus /></ElIcon>
|
||||
新增连接
|
||||
</EasyFlowButton>
|
||||
<EasyFlowButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="refresh-action"
|
||||
aria-label="刷新"
|
||||
@click="emit('refresh')"
|
||||
>
|
||||
<ElIcon class="action-icon"><RefreshRight /></ElIcon>
|
||||
</EasyFlowButton>
|
||||
</div>
|
||||
<div class="search-row">
|
||||
<ElInput v-model="searchText" placeholder="搜索连接..." clearable>
|
||||
<template #prefix>
|
||||
<ElIcon><Search /></ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tree-scroll">
|
||||
<div v-if="filteredTree.length > 0" class="tree-list">
|
||||
<template v-for="source in filteredTree" :key="source.id">
|
||||
<div
|
||||
class="tree-node tree-node--source"
|
||||
:class="{ 'is-selected': selectedKey === source.id }"
|
||||
@click="handleClick(source)"
|
||||
@contextmenu="handleContextMenu($event, source)"
|
||||
>
|
||||
<span class="node-media">
|
||||
<SourceBrandIcon :source-type="source.meta?.sourceType" />
|
||||
</span>
|
||||
<span class="node-content">
|
||||
<span class="node-label">{{ source.label }}</span>
|
||||
<span v-if="getSourceMetaLabel(source)" class="node-meta">
|
||||
{{ getSourceMetaLabel(source) }}
|
||||
</span>
|
||||
</span>
|
||||
<ElTooltip
|
||||
v-if="isSourceUnavailable(source)"
|
||||
content="连接不可用"
|
||||
placement="top"
|
||||
>
|
||||
<span class="node-status-dot" aria-label="连接不可用"></span>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
|
||||
<!-- 表节点 -->
|
||||
<template v-if="isExpanded(source.id) && source.children">
|
||||
<div
|
||||
v-for="table in source.children"
|
||||
:key="table.id"
|
||||
class="tree-node tree-node--table"
|
||||
:class="{ 'is-selected': selectedKey === table.id }"
|
||||
@click="handleClick(table)"
|
||||
@contextmenu="handleContextMenu($event, table)"
|
||||
>
|
||||
<span class="node-icon" :class="[getNodeIcon(table)]"></span>
|
||||
<span class="node-label">{{ table.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-else class="tree-empty">
|
||||
{{ searchText ? '无匹配连接' : '还没有数据连接' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="contextMenuVisible && contextMenuNode"
|
||||
class="connection-context-menu"
|
||||
:style="{ left: `${contextMenuX}px`, top: `${contextMenuY}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<button type="button" class="context-menu-item" @click="handleEdit">
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="context-menu-item context-menu-item--danger"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.connection-tree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0 12px 0 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tree-search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.search-row :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-row :deep(.el-input__wrapper) {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.create-action {
|
||||
flex: 1;
|
||||
padding-inline: 12px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.refresh-action {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding-inline: 0;
|
||||
border-color: hsl(var(--border) / 0.9);
|
||||
background: hsl(var(--card));
|
||||
color: hsl(var(--text-strong));
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.refresh-action:hover {
|
||||
background: hsl(var(--surface-subtle));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.tree-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.tree-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 62px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid hsl(var(--border) / 0.52);
|
||||
border-radius: 14px;
|
||||
background: hsl(var(--card) / 0.92);
|
||||
box-shadow: 0 10px 22px -22px hsl(var(--foreground) / 0.22);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: hsl(var(--foreground));
|
||||
transition:
|
||||
transform 0.16s,
|
||||
background-color 0.16s,
|
||||
color 0.16s,
|
||||
border-color 0.16s,
|
||||
box-shadow 0.16s;
|
||||
}
|
||||
|
||||
.tree-node:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: hsl(var(--border) / 0.74);
|
||||
background: hsl(var(--card));
|
||||
box-shadow: 0 16px 30px -24px hsl(var(--foreground) / 0.22);
|
||||
}
|
||||
|
||||
.tree-node.is-selected {
|
||||
border-color: hsl(var(--primary) / 0.24);
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
color: hsl(var(--foreground));
|
||||
box-shadow:
|
||||
inset 0 0 0 1px hsl(var(--primary) / 0.12),
|
||||
0 18px 34px -28px hsl(var(--primary) / 0.24);
|
||||
}
|
||||
|
||||
.tree-node--source {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tree-node--table {
|
||||
margin-left: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.node-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.node-arrow.is-expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.node-media {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
border-radius: 11px;
|
||||
background: hsl(var(--surface-subtle) / 0.92);
|
||||
}
|
||||
|
||||
.tree-node.is-selected .node-media {
|
||||
background: hsl(var(--primary) / 0.12);
|
||||
}
|
||||
|
||||
.node-media :deep(svg) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
flex-shrink: 0;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.tree-node.is-selected .node-icon {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: normal;
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.node-meta {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.node-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
border-radius: 999px;
|
||||
background: hsl(var(--destructive));
|
||||
box-shadow: 0 0 0 3px hsl(var(--destructive) / 0.12);
|
||||
}
|
||||
|
||||
.tree-empty {
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.connection-context-menu {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
min-width: 136px;
|
||||
padding: 6px;
|
||||
border: 1px solid hsl(var(--border) / 0.7);
|
||||
border-radius: 12px;
|
||||
background: hsl(var(--popover));
|
||||
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 0.26);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: hsl(var(--accent) / 0.86);
|
||||
}
|
||||
|
||||
.context-menu-item--danger {
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.context-menu-item--danger:hover {
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadUserFile } from 'element-plus';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDrawer,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
|
||||
defineProps<{
|
||||
actionLoading: boolean;
|
||||
managedTables: any[];
|
||||
fieldOptions: { label: string; value: string }[];
|
||||
pendingUploadFile: File | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'import'): void;
|
||||
(e: 'split'): void;
|
||||
(e: 'merge'): void;
|
||||
(e: 'derive'): void;
|
||||
(e: 'export'): void;
|
||||
(e: 'update:pendingUploadFile', file: File | null): void;
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
const activeAction = ref<'derive' | 'export' | 'import' | 'merge' | 'split'>('import');
|
||||
|
||||
const splitForm = defineModel<any>('splitForm', { required: true });
|
||||
const mergeForm = defineModel<any>('mergeForm', { required: true });
|
||||
const deriveForm = defineModel<any>('deriveForm', { required: true });
|
||||
const exportForm = defineModel<any>('exportForm', { required: true });
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
const titles: Record<string, string> = {
|
||||
import: '导入 Excel',
|
||||
split: '拆分数据',
|
||||
merge: '合并数据',
|
||||
derive: '生成新表',
|
||||
export: '导出数据',
|
||||
};
|
||||
return titles[activeAction.value] || '数据操作';
|
||||
});
|
||||
|
||||
function open(action: 'derive' | 'export' | 'import' | 'merge' | 'split') {
|
||||
activeAction.value = action;
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function handleUploadChange(file: UploadUserFile) {
|
||||
emit('update:pendingUploadFile', file.raw || null);
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
const action = activeAction.value;
|
||||
if (action === 'import') emit('import');
|
||||
else if (action === 'split') emit('split');
|
||||
else if (action === 'merge') emit('merge');
|
||||
else if (action === 'derive') emit('derive');
|
||||
else if (action === 'export') emit('export');
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElDrawer
|
||||
v-model="visible"
|
||||
:title="drawerTitle"
|
||||
size="480px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="action-form">
|
||||
<!-- 导入 -->
|
||||
<template v-if="activeAction === 'import'">
|
||||
<ElUpload
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
:on-change="handleUploadChange"
|
||||
>
|
||||
<div class="upload-hint">拖拽或点击上传 Excel 文件</div>
|
||||
</ElUpload>
|
||||
</template>
|
||||
|
||||
<!-- 拆分 -->
|
||||
<template v-if="activeAction === 'split'">
|
||||
<ElSelect v-model="splitForm.splitMode">
|
||||
<ElOption label="按行数" value="BY_ROW_COUNT" />
|
||||
<ElOption label="按字段值" value="BY_FIELD_VALUE" />
|
||||
<ElOption label="按工作表" value="BY_SHEET" />
|
||||
</ElSelect>
|
||||
<ElInput v-model="splitForm.targetNamePrefix" placeholder="新表名前缀" />
|
||||
<ElInput
|
||||
v-if="splitForm.splitMode === 'BY_ROW_COUNT'"
|
||||
v-model="splitForm.rowBatchSize"
|
||||
placeholder="每份行数"
|
||||
/>
|
||||
<ElSelect
|
||||
v-if="splitForm.splitMode === 'BY_FIELD_VALUE'"
|
||||
v-model="splitForm.fieldName"
|
||||
placeholder="选择拆分字段"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in fieldOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</template>
|
||||
|
||||
<!-- 合并 -->
|
||||
<template v-if="activeAction === 'merge'">
|
||||
<ElSelect v-model="mergeForm.mergeMode">
|
||||
<ElOption label="纵向合并" value="VERTICAL" />
|
||||
<ElOption label="横向合并" value="HORIZONTAL" />
|
||||
</ElSelect>
|
||||
<ElSelect
|
||||
v-model="mergeForm.tableIds"
|
||||
multiple
|
||||
collapse-tags
|
||||
placeholder="选择要合并的表"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in managedTables"
|
||||
:key="item.id"
|
||||
:label="item.tableName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElInput v-model="mergeForm.targetTableName" placeholder="新表名称" />
|
||||
<ElInput
|
||||
v-if="mergeForm.mergeMode === 'HORIZONTAL'"
|
||||
v-model="mergeForm.joinKey"
|
||||
placeholder="关联字段"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 生成新表 -->
|
||||
<template v-if="activeAction === 'derive'">
|
||||
<ElInput v-model="deriveForm.targetTableName" placeholder="新表名称" />
|
||||
<ElInput
|
||||
v-model="deriveForm.selectedColumnsText"
|
||||
placeholder="保留字段,多个字段用逗号分隔"
|
||||
/>
|
||||
<ElInput
|
||||
v-model="deriveForm.renameMappingsText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='字段重命名 JSON,例如 {"old":"new"}'
|
||||
/>
|
||||
<ElInput
|
||||
v-model="deriveForm.filtersText"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder='筛选条件 JSON,例如 [{"column":"status","operator":"EQ","value":"OPEN"}]'
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 导出 -->
|
||||
<template v-if="activeAction === 'export'">
|
||||
<ElSelect v-model="exportForm.exportScope">
|
||||
<ElOption label="当前表" value="TABLE" />
|
||||
<ElOption label="当前库" value="WORKBOOK" />
|
||||
</ElSelect>
|
||||
<ElInput v-model="exportForm.fileName" placeholder="导出文件名,可选" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<ElButton @click="visible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="actionLoading" @click="handleConfirm">
|
||||
{{ activeAction === 'import' ? '开始导入' :
|
||||
activeAction === 'split' ? '开始拆分' :
|
||||
activeAction === 'merge' ? '开始合并' :
|
||||
activeAction === 'derive' ? '开始生成' :
|
||||
'生成并下载' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.action-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
padding: 24px;
|
||||
color: hsl(var(--text-muted));
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import huaweiIcon from '#/assets/datacenter/huawei-icon.svg';
|
||||
import mysqlIcon from '#/assets/datacenter/mysql-icon.svg';
|
||||
import postgresqlIcon from '#/assets/datacenter/postgresql-icon.svg';
|
||||
|
||||
const props = defineProps<{
|
||||
sourceType?: string;
|
||||
}>();
|
||||
|
||||
const sourceLogoMap: Record<string, string> = {
|
||||
EXCEL: 'excel',
|
||||
EXCEL_MATERIALIZED: 'excel',
|
||||
GAUSSDB_NATIVE: 'gaussdb',
|
||||
GBASE_8A: 'gbase',
|
||||
GBASE_8S: 'gbase',
|
||||
MYSQL: 'mysql',
|
||||
ORACLE: 'oracle',
|
||||
POSTGRESQL: 'postgresql',
|
||||
PROJECT_MYSQL: 'mysql',
|
||||
};
|
||||
|
||||
const logoType = computed(
|
||||
() => sourceLogoMap[props.sourceType || ''] || 'default',
|
||||
);
|
||||
const assetLogoMap: Record<string, string> = {
|
||||
gaussdb: huaweiIcon,
|
||||
mysql: mysqlIcon,
|
||||
postgresql: postgresqlIcon,
|
||||
};
|
||||
const assetLogo = computed(() => assetLogoMap[logoType.value] || '');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
v-if="assetLogo"
|
||||
class="brand-logo-image"
|
||||
:src="assetLogo"
|
||||
:alt="`${props.sourceType || 'database'} logo`"
|
||||
/>
|
||||
|
||||
<svg
|
||||
v-else-if="logoType === 'oracle'"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="4.6" y="7.2" width="14.8" height="9.6" rx="4.8" fill="#ea4335" />
|
||||
<rect x="7.1" y="9.35" width="9.8" height="5.3" rx="2.65" fill="white" />
|
||||
<rect x="8.4" y="10.65" width="7.2" height="2.7" rx="1.35" fill="#ea4335" />
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
v-else-if="logoType === 'gbase'"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="8" fill="#10b981" />
|
||||
<path
|
||||
d="M15.85 9.2C15.1 8.02 13.8 7.3 12.38 7.3C9.94 7.3 8.1 9.14 8.1 11.98C8.1 14.87 10.05 16.7 12.56 16.7C13.88 16.7 15.03 16.18 15.88 15.15V12.55H12.45"
|
||||
stroke="white"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.7"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
v-else-if="logoType === 'excel'"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="4.4" y="4.2" width="15.2" height="15.6" rx="3.2" fill="#16a34a" />
|
||||
<path
|
||||
d="M9 9L11.1 12L9 15M15 9L12.9 12L15 15"
|
||||
stroke="white"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.65"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg v-else viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<ellipse cx="12" cy="6.5" rx="5.75" ry="2.5" fill="#94a3b8" />
|
||||
<path
|
||||
d="M6.25 6.5V12.2C6.25 13.58 8.82 14.7 12 14.7C15.18 14.7 17.75 13.58 17.75 12.2V6.5"
|
||||
fill="#cbd5e1"
|
||||
/>
|
||||
<path
|
||||
d="M6.25 12.2V17.35C6.25 18.72 8.82 19.85 12 19.85C15.18 19.85 17.75 18.72 17.75 17.35V12.2"
|
||||
fill="#e2e8f0"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.brand-logo-image {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,364 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
|
||||
|
||||
import { CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
sourceTypeLabels,
|
||||
sourceTypeOptions,
|
||||
} from '../composables/datacenter-constants';
|
||||
import { useSourceForm } from '../composables/use-source-form';
|
||||
import SourceBrandIcon from './SourceBrandIcon.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
saving: boolean;
|
||||
testing: boolean;
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [form: Record<string, any>];
|
||||
test: [form: Record<string, any>];
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
const formRef = ref<FormInstance>();
|
||||
const testResult = ref<any>(null);
|
||||
const showAdvanced = ref(false);
|
||||
|
||||
const {
|
||||
form,
|
||||
rules,
|
||||
supportsExternalConnection,
|
||||
resetForm: baseResetForm,
|
||||
} = useSourceForm();
|
||||
|
||||
const modalTitle = computed(() => (form.id ? '编辑连接' : '新建连接'));
|
||||
const testButtonLabel = computed(() => {
|
||||
if (props.testing) return '测试中...';
|
||||
if (testResult.value?.success === true) return '连接成功';
|
||||
if (testResult.value?.success === false) return '连接失败';
|
||||
return '测试连接';
|
||||
});
|
||||
const testButtonClass = computed(() => ({
|
||||
'is-test-success': testResult.value?.success === true,
|
||||
'is-test-failed': testResult.value?.success === false,
|
||||
}));
|
||||
const selectedSourceTypeLabel = computed(
|
||||
() => sourceTypeLabels[form.sourceType] || form.sourceType,
|
||||
);
|
||||
|
||||
function resetDialogState() {
|
||||
testResult.value = null;
|
||||
showAdvanced.value = false;
|
||||
}
|
||||
|
||||
function open(row?: any) {
|
||||
baseResetForm(row);
|
||||
resetDialogState();
|
||||
emit('update:visible', true);
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false);
|
||||
resetDialogState();
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
await formRef.value?.validate();
|
||||
emit('save', { ...form });
|
||||
}
|
||||
|
||||
async function handleTest() {
|
||||
await formRef.value?.validate();
|
||||
testResult.value = null;
|
||||
emit('test', { ...form });
|
||||
}
|
||||
|
||||
function handleBeforeClose() {
|
||||
resetDialogState();
|
||||
}
|
||||
|
||||
function setTestResult(result: any) {
|
||||
testResult.value = result;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
form.sourceName,
|
||||
form.sourceType,
|
||||
form.host,
|
||||
form.port,
|
||||
form.databaseName,
|
||||
form.username,
|
||||
form.password,
|
||||
form.jdbcUrl,
|
||||
form.driverClassName,
|
||||
form.configJson?.serviceName,
|
||||
form.configJson?.informixServer,
|
||||
],
|
||||
() => {
|
||||
if (!props.testing && testResult.value) {
|
||||
testResult.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
defineExpose({ close, open, setTestResult });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EasyFlowFormModal
|
||||
:open="visible"
|
||||
:title="modalTitle"
|
||||
:before-close="handleBeforeClose"
|
||||
:confirm-loading="saving"
|
||||
confirm-text="保存"
|
||||
:submitting="saving"
|
||||
width="800px"
|
||||
@update:open="emit('update:visible', $event)"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="easyflow-modal-form easyflow-modal-form--compact source-form"
|
||||
>
|
||||
<div class="form-grid">
|
||||
<ElFormItem label="连接名称" prop="sourceName">
|
||||
<ElInput v-model="form.sourceName" placeholder="例如:华东 MySQL" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="连接类型" prop="sourceType">
|
||||
<ElSelect
|
||||
v-model="form.sourceType"
|
||||
class="w-full"
|
||||
:disabled="Boolean(form.builtinFlag)"
|
||||
>
|
||||
<template #label>
|
||||
<span class="source-type-select-label">
|
||||
<span class="source-type-select-icon">
|
||||
<SourceBrandIcon :source-type="form.sourceType" />
|
||||
</span>
|
||||
<span>{{ selectedSourceTypeLabel }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<ElOption
|
||||
v-for="item in sourceTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="source-type-option">
|
||||
<span class="source-type-option__icon">
|
||||
<SourceBrandIcon :source-type="item.value" />
|
||||
</span>
|
||||
<span class="source-type-option__label">{{ item.label }}</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<template v-if="supportsExternalConnection">
|
||||
<div class="form-grid">
|
||||
<ElFormItem label="主机">
|
||||
<ElInput v-model="form.host" placeholder="db.example.com" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="端口">
|
||||
<ElInput v-model="form.port" placeholder="5432" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="库名" prop="databaseName">
|
||||
<ElInput v-model="form.databaseName" placeholder="例如:easyflow" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="用户名">
|
||||
<ElInput v-model="form.username" placeholder="username" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="密码" class="span-2">
|
||||
<ElInput
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="password"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-advanced"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
>
|
||||
{{ showAdvanced ? '收起高级设置' : '高级设置' }}
|
||||
</button>
|
||||
|
||||
<div v-if="showAdvanced" class="form-grid form-grid--advanced">
|
||||
<ElFormItem label="连接地址" class="span-2">
|
||||
<ElInput v-model="form.jdbcUrl" placeholder="留空由系统自动生成" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="驱动类" class="span-2">
|
||||
<ElInput
|
||||
v-model="form.driverClassName"
|
||||
placeholder="留空由系统自动生成"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="form.sourceType === 'ORACLE'"
|
||||
label="Oracle 服务名"
|
||||
class="span-2"
|
||||
>
|
||||
<ElInput
|
||||
v-model="form.configJson.serviceName"
|
||||
placeholder="可选,未填写时默认使用数据库名"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="form.sourceType === 'GBASE_8S'"
|
||||
label="GBase 实例名"
|
||||
class="span-2"
|
||||
>
|
||||
<ElInput
|
||||
v-model="form.configJson.informixServer"
|
||||
placeholder="默认 gbasedbt_server"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</div>
|
||||
</template>
|
||||
</ElForm>
|
||||
|
||||
<template #footer-extra>
|
||||
<EasyFlowButton
|
||||
variant="outline"
|
||||
:loading="testing"
|
||||
:disabled="saving"
|
||||
class="test-button"
|
||||
:class="testButtonClass"
|
||||
@click="handleTest"
|
||||
>
|
||||
<ElIcon v-if="!testing && testResult?.success === true">
|
||||
<CircleCheckFilled />
|
||||
</ElIcon>
|
||||
<ElIcon v-else-if="!testing && testResult?.success === false">
|
||||
<CircleCloseFilled />
|
||||
</ElIcon>
|
||||
{{ testButtonLabel }}
|
||||
</EasyFlowButton>
|
||||
</template>
|
||||
</EasyFlowFormModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.source-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 16px;
|
||||
}
|
||||
|
||||
.form-grid--advanced {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.source-type-select-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.source-type-select-icon,
|
||||
.source-type-option__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.source-type-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.source-type-option__label {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.toggle-advanced {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: hsl(var(--primary));
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-advanced:hover {
|
||||
color: hsl(var(--primary) / 0.86);
|
||||
}
|
||||
|
||||
.test-button {
|
||||
min-width: 112px;
|
||||
transition:
|
||||
color 0.16s,
|
||||
border-color 0.16s,
|
||||
background-color 0.16s;
|
||||
}
|
||||
|
||||
.test-button :deep(.el-icon) {
|
||||
margin-right: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.test-button.is-test-success {
|
||||
color: hsl(var(--success));
|
||||
border-color: hsl(var(--success) / 0.24);
|
||||
background: hsl(var(--success) / 0.08);
|
||||
}
|
||||
|
||||
.test-button.is-test-success:hover {
|
||||
color: hsl(var(--success));
|
||||
border-color: hsl(var(--success) / 0.28);
|
||||
background: hsl(var(--success) / 0.12);
|
||||
}
|
||||
|
||||
.test-button.is-test-failed {
|
||||
color: hsl(var(--destructive));
|
||||
border-color: hsl(var(--destructive) / 0.22);
|
||||
background: hsl(var(--destructive) / 0.08);
|
||||
}
|
||||
|
||||
.test-button.is-test-failed:hover {
|
||||
color: hsl(var(--destructive));
|
||||
border-color: hsl(var(--destructive) / 0.28);
|
||||
background: hsl(var(--destructive) / 0.12);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,422 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { ArrowLeft, EditPen } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElEmpty,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import {
|
||||
formatJobStatus,
|
||||
formatJobType,
|
||||
formatRelationType,
|
||||
} from '../composables/datacenter-constants';
|
||||
|
||||
const props = defineProps<{
|
||||
jobs: any[];
|
||||
loading: boolean;
|
||||
previewRows: any[];
|
||||
saveDescriptions: (payload: {
|
||||
fields?: Array<{ fieldDesc: string; fieldId: number | string }>;
|
||||
tableDesc?: string;
|
||||
tableId: number | string;
|
||||
}) => Promise<any>;
|
||||
schema: any;
|
||||
sourceUnavailable?: boolean;
|
||||
table: any;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
back: [];
|
||||
}>();
|
||||
|
||||
const activeTab = ref('data');
|
||||
const editingFieldId = ref<null | number | string>(null);
|
||||
const editingFieldDesc = ref('');
|
||||
const savingFieldId = ref<null | number | string>(null);
|
||||
|
||||
function startFieldEdit(field: any) {
|
||||
editingFieldId.value = field.id;
|
||||
editingFieldDesc.value = field.fieldDesc || '';
|
||||
}
|
||||
|
||||
function cancelFieldEdit() {
|
||||
editingFieldId.value = null;
|
||||
editingFieldDesc.value = '';
|
||||
}
|
||||
|
||||
async function saveFieldDescription(field: any) {
|
||||
const tableId = props.schema?.table?.id;
|
||||
if (!tableId || !field?.id || savingFieldId.value) return;
|
||||
savingFieldId.value = field.id;
|
||||
try {
|
||||
await props.saveDescriptions({
|
||||
tableDesc: props.schema?.table?.tableDesc || '',
|
||||
tableId,
|
||||
fields: [
|
||||
{
|
||||
fieldDesc: editingFieldDesc.value,
|
||||
fieldId: field.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
cancelFieldEdit();
|
||||
} finally {
|
||||
savingFieldId.value = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="table-detail-view" v-loading="loading">
|
||||
<!-- 表头信息 -->
|
||||
<div class="detail-header">
|
||||
<div class="header-main">
|
||||
<button class="back-btn" @click="emit('back')">
|
||||
<ElIcon class="back-icon"><ArrowLeft /></ElIcon>
|
||||
</button>
|
||||
<div class="header-info">
|
||||
<h3 class="header-title">{{ table?.tableName }}</h3>
|
||||
<p v-if="schema?.table?.tableDesc" class="header-desc">
|
||||
{{ schema.table.tableDesc }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs 内容 -->
|
||||
<ElTabs v-model="activeTab" class="detail-tabs">
|
||||
<ElTabPane label="数据信息" name="data">
|
||||
<div class="detail-scroll-region">
|
||||
<ElEmpty v-if="previewRows.length === 0" description="暂无数据" />
|
||||
<ElTable v-else :data="previewRows" size="small" class="flat-table">
|
||||
<ElTableColumn
|
||||
v-for="field in schema?.fields || []"
|
||||
:key="field.fieldName"
|
||||
:prop="field.fieldName"
|
||||
:label="field.fieldName"
|
||||
min-width="140"
|
||||
/>
|
||||
</ElTable>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="字段结构" name="schema">
|
||||
<div class="detail-scroll-region">
|
||||
<ElTable :data="schema?.fields || []" size="small" class="flat-table">
|
||||
<ElTableColumn label="字段名" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<div class="field-name-cell">
|
||||
<span class="field-name-text">{{ row.fieldName }}</span>
|
||||
<template v-if="editingFieldId === row.id">
|
||||
<ElInput
|
||||
v-model="editingFieldDesc"
|
||||
size="small"
|
||||
clearable
|
||||
maxlength="200"
|
||||
class="description-input"
|
||||
/>
|
||||
<div class="inline-actions">
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="savingFieldId === row.id"
|
||||
@click="saveFieldDescription(row)"
|
||||
>
|
||||
保存
|
||||
</ElButton>
|
||||
<ElButton
|
||||
link
|
||||
size="small"
|
||||
:disabled="savingFieldId === row.id"
|
||||
@click="cancelFieldEdit"
|
||||
>
|
||||
取消
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="description-inline">{{
|
||||
row.fieldDesc || ''
|
||||
}}</span>
|
||||
<ElButton
|
||||
class="icon-action icon-action--edit"
|
||||
link
|
||||
size="small"
|
||||
@click="startFieldEdit(row)"
|
||||
>
|
||||
<ElIcon><EditPen /></ElIcon>
|
||||
</ElButton>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="jdbcType" label="类型" width="120" />
|
||||
</ElTable>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="版本记录" name="version">
|
||||
<div class="detail-scroll-region">
|
||||
<div class="tag-flow">
|
||||
<ElTag
|
||||
v-for="version in schema?.versions || []"
|
||||
:key="version.id"
|
||||
effect="plain"
|
||||
>
|
||||
#{{ version.versionNo }}
|
||||
{{ version.versionLabel || '未命名版本' }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<ElEmpty
|
||||
v-if="(schema?.versions || []).length === 0"
|
||||
description="暂无版本记录"
|
||||
/>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="来源关系" name="lineage">
|
||||
<div class="detail-scroll-region">
|
||||
<div class="tag-flow">
|
||||
<ElTag
|
||||
v-for="item in schema?.upstreamLineage || []"
|
||||
:key="`up-${item.id}`"
|
||||
effect="plain"
|
||||
type="info"
|
||||
>
|
||||
上游 {{ formatRelationType(item.relationType) }} /
|
||||
{{ item.sourceTableId }}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
v-for="item in schema?.downstreamLineage || []"
|
||||
:key="`down-${item.id}`"
|
||||
effect="plain"
|
||||
type="success"
|
||||
>
|
||||
下游 {{ formatRelationType(item.relationType) }} /
|
||||
{{ item.derivedTableId }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<ElEmpty
|
||||
v-if="
|
||||
(schema?.upstreamLineage || []).length === 0 &&
|
||||
(schema?.downstreamLineage || []).length === 0
|
||||
"
|
||||
description="暂无来源关系"
|
||||
/>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
|
||||
<ElTabPane label="处理记录" name="job">
|
||||
<div class="detail-scroll-region">
|
||||
<ElEmpty v-if="jobs.length === 0" description="暂无处理记录" />
|
||||
<ElTable v-else :data="jobs" size="small" class="flat-table">
|
||||
<ElTableColumn label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatJobType(row.jobType) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag
|
||||
size="small"
|
||||
:type="
|
||||
row.status === 'SUCCESS'
|
||||
? 'success'
|
||||
: row.status === 'FAILED'
|
||||
? 'danger'
|
||||
: 'info'
|
||||
"
|
||||
effect="plain"
|
||||
>
|
||||
{{ formatJobStatus(row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="created" label="时间" min-width="180" />
|
||||
</ElTable>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.table-detail-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0 0 0 12px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 0 4px;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid hsl(var(--border) / 0.7);
|
||||
border-radius: 999px;
|
||||
background: hsl(var(--surface-subtle) / 0.75);
|
||||
cursor: pointer;
|
||||
color: hsl(var(--text-muted));
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: hsl(var(--accent));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--text-strong));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-desc {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--text-muted));
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.detail-tabs :deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid hsl(var(--border) / 0.42);
|
||||
}
|
||||
|
||||
.detail-tabs :deep(.el-tabs__nav-wrap::after) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.detail-tabs :deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detail-tabs :deep(.el-tab-pane) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.detail-scroll-region {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.tag-flow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.field-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 28px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field-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%);
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,163 @@
|
||||
export const sourceTypeOptions = [
|
||||
{ label: 'Excel 文件', value: 'EXCEL' },
|
||||
{ label: 'MySQL', value: 'MYSQL' },
|
||||
{ label: 'PostgreSQL', value: 'POSTGRESQL' },
|
||||
{ label: 'Oracle', value: 'ORACLE' },
|
||||
{ label: 'GaussDB', value: 'GAUSSDB_NATIVE' },
|
||||
{ label: 'GBase 8a', value: 'GBASE_8A' },
|
||||
{ label: 'GBase 8s', value: 'GBASE_8S' },
|
||||
];
|
||||
|
||||
export const sourceConnectionDefaults: Record<
|
||||
string,
|
||||
{
|
||||
buildJdbcUrl: (form: Record<string, any>) => string;
|
||||
defaultDriver: string;
|
||||
defaultPort: number;
|
||||
}
|
||||
> = {
|
||||
MYSQL: {
|
||||
defaultPort: 3306,
|
||||
defaultDriver: 'com.mysql.cj.jdbc.Driver',
|
||||
buildJdbcUrl: (p) =>
|
||||
p.host && p.port && p.databaseName
|
||||
? `jdbc:mysql://${p.host}:${p.port}/${p.databaseName}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false`
|
||||
: '',
|
||||
},
|
||||
POSTGRESQL: {
|
||||
defaultPort: 5432,
|
||||
defaultDriver: 'org.postgresql.Driver',
|
||||
buildJdbcUrl: (p) =>
|
||||
p.host && p.port && p.databaseName
|
||||
? `jdbc:postgresql://${p.host}:${p.port}/${p.databaseName}`
|
||||
: '',
|
||||
},
|
||||
ORACLE: {
|
||||
defaultPort: 1521,
|
||||
defaultDriver: 'oracle.jdbc.OracleDriver',
|
||||
buildJdbcUrl: (p) => {
|
||||
const serviceName = p?.configJson?.serviceName || p.databaseName;
|
||||
return p.host && p.port && serviceName
|
||||
? `jdbc:oracle:thin:@//${p.host}:${p.port}/${serviceName}`
|
||||
: '';
|
||||
},
|
||||
},
|
||||
GAUSSDB_NATIVE: {
|
||||
defaultPort: 5432,
|
||||
defaultDriver: 'org.postgresql.Driver',
|
||||
buildJdbcUrl: (p) =>
|
||||
p.host && p.port && p.databaseName
|
||||
? `jdbc:postgresql://${p.host}:${p.port}/${p.databaseName}`
|
||||
: '',
|
||||
},
|
||||
GBASE_8A: {
|
||||
defaultPort: 5258,
|
||||
defaultDriver: 'com.gbase.jdbc.Driver',
|
||||
buildJdbcUrl: (p) =>
|
||||
p.host && p.port && p.databaseName
|
||||
? `jdbc:gbase://${p.host}:${p.port}/${p.databaseName}`
|
||||
: '',
|
||||
},
|
||||
GBASE_8S: {
|
||||
defaultPort: 9088,
|
||||
defaultDriver: 'com.gbasedbt.jdbc.Driver',
|
||||
buildJdbcUrl: (p) => {
|
||||
const informixServer = p?.configJson?.informixServer || 'gbasedbt_server';
|
||||
return p.host && p.port && p.databaseName
|
||||
? `jdbc:gbasedbt-sqli://${p.host}:${p.port}/${p.databaseName}:INFORMIXSERVER=${informixServer}`
|
||||
: '';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sourceTypeLabels: Record<string, string> = {
|
||||
PROJECT_MYSQL: '项目 MySQL',
|
||||
EXCEL: 'Excel 文件',
|
||||
MYSQL: 'MySQL',
|
||||
POSTGRESQL: 'PostgreSQL',
|
||||
ORACLE: 'Oracle',
|
||||
GAUSSDB_NATIVE: 'GaussDB',
|
||||
GBASE_8A: 'GBase 8a',
|
||||
GBASE_8S: 'GBase 8s',
|
||||
};
|
||||
|
||||
export const accessModeLabels: Record<string, string> = {
|
||||
READ_ONLY: '只读',
|
||||
READ_WRITE: '可编辑',
|
||||
};
|
||||
|
||||
export const tableKindLabels: Record<string, string> = {
|
||||
SOURCE_TABLE: '表',
|
||||
SOURCE_VIEW: '视图',
|
||||
TABLE: '表',
|
||||
VIEW: '视图',
|
||||
EXCEL_SHEET: '工作表',
|
||||
DERIVED_TABLE: '生成表',
|
||||
MATERIALIZED_TABLE: '缓存表',
|
||||
};
|
||||
|
||||
export const jobTypeLabels: Record<string, string> = {
|
||||
IMPORT: '导入',
|
||||
SPLIT: '拆分',
|
||||
MERGE: '合并',
|
||||
DERIVE: '生成新表',
|
||||
EXPORT: '导出',
|
||||
};
|
||||
|
||||
export const jobStatusLabels: Record<string, string> = {
|
||||
SUCCESS: '完成',
|
||||
FAILED: '失败',
|
||||
RUNNING: '处理中',
|
||||
PENDING: '等待中',
|
||||
CREATED: '已创建',
|
||||
};
|
||||
|
||||
export const relationTypeLabels: Record<string, string> = {
|
||||
SPLIT: '拆分生成',
|
||||
MERGE: '合并生成',
|
||||
DERIVE: '生成新表',
|
||||
};
|
||||
|
||||
export function formatSourceType(value?: string) {
|
||||
return value ? sourceTypeLabels[value] || value : '-';
|
||||
}
|
||||
|
||||
export function formatAccessMode(value?: string) {
|
||||
return value ? accessModeLabels[value] || value : '-';
|
||||
}
|
||||
|
||||
export function formatTestStatus(value?: string) {
|
||||
if (!value) return '未测试';
|
||||
return value === 'SUCCESS' ? '连接正常' : '连接失败';
|
||||
}
|
||||
|
||||
export function formatTableKind(value?: string) {
|
||||
return value ? tableKindLabels[value] || value : '-';
|
||||
}
|
||||
|
||||
export function formatJobType(value?: string) {
|
||||
return value ? jobTypeLabels[value] || value : value || '-';
|
||||
}
|
||||
|
||||
export function formatJobStatus(value?: string) {
|
||||
return value ? jobStatusLabels[value] || value : '-';
|
||||
}
|
||||
|
||||
export function formatRelationType(value?: string) {
|
||||
return value ? relationTypeLabels[value] || value : '关联';
|
||||
}
|
||||
|
||||
export function formatFieldFlag(value?: null | number) {
|
||||
return value === null || value === undefined || value === 1 ? '是' : '否';
|
||||
}
|
||||
|
||||
export function mergeConfigJsonBySourceType(
|
||||
sourceType: string,
|
||||
configJson?: Record<string, any>,
|
||||
) {
|
||||
const merged = { ...configJson };
|
||||
if (sourceType === 'GBASE_8S' && !merged.informixServer) {
|
||||
merged.informixServer = 'gbasedbt_server';
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { formatSourceType, formatTestStatus } from './datacenter-constants';
|
||||
|
||||
export interface TreeNode {
|
||||
id: string;
|
||||
label: string;
|
||||
type: 'source' | 'table';
|
||||
icon?: string;
|
||||
meta?: any;
|
||||
children?: TreeNode[];
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
|
||||
export function useConnectionTree(sources: Ref<any[]>) {
|
||||
const treeData = computed<TreeNode[]>(() => {
|
||||
return sources.value.map((source) => {
|
||||
return {
|
||||
id: `source-${source.id}`,
|
||||
label: source.sourceName,
|
||||
type: 'source' as const,
|
||||
icon: getSourceIcon(source.sourceType),
|
||||
meta: {
|
||||
...source,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function getSourceIcon(sourceType: string): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
MYSQL: 'db',
|
||||
POSTGRESQL: 'db',
|
||||
ORACLE: 'db',
|
||||
GAUSSDB_NATIVE: 'db',
|
||||
GBASE_8A: 'db',
|
||||
GBASE_8S: 'db',
|
||||
EXCEL: 'file',
|
||||
PROJECT_MYSQL: 'db',
|
||||
};
|
||||
return iconMap[sourceType] || 'db';
|
||||
}
|
||||
|
||||
function parseNodeKey(key: string) {
|
||||
const [type, ...idParts] = key.split('-');
|
||||
const id = idParts.join('-');
|
||||
return {
|
||||
type: type as 'source' | 'table',
|
||||
id: Number(id),
|
||||
};
|
||||
}
|
||||
|
||||
function getStatusColor(source: any): string {
|
||||
if (!source.lastTestStatus) return 'muted';
|
||||
return source.lastTestStatus === 'SUCCESS' ? 'success' : 'danger';
|
||||
}
|
||||
|
||||
return {
|
||||
treeData,
|
||||
parseNodeKey,
|
||||
getSourceIcon,
|
||||
getStatusColor,
|
||||
formatSourceType,
|
||||
formatTestStatus,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { downloadFileFromBlob } from '@easyflow/utils';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
|
||||
export function useDatacenterExcel(
|
||||
selectedSourceId: Ref<null | number>,
|
||||
selectedCatalogId: Ref<null | number>,
|
||||
selectedTableId: Ref<null | number>,
|
||||
reloadAll: () => Promise<void>,
|
||||
) {
|
||||
const actionLoading = ref(false);
|
||||
const pendingUploadFile = ref<File | null>(null);
|
||||
|
||||
const splitForm = reactive({
|
||||
splitMode: 'BY_ROW_COUNT',
|
||||
rowBatchSize: 1000,
|
||||
fieldName: '',
|
||||
targetNamePrefix: '',
|
||||
});
|
||||
|
||||
const mergeForm = reactive({
|
||||
mergeMode: 'VERTICAL',
|
||||
tableIds: [] as number[],
|
||||
targetTableName: '',
|
||||
joinKey: '',
|
||||
});
|
||||
|
||||
const deriveForm = reactive({
|
||||
targetTableName: '',
|
||||
selectedColumnsText: '',
|
||||
renameMappingsText: '{}',
|
||||
filtersText: '[]',
|
||||
});
|
||||
|
||||
const exportForm = reactive({
|
||||
fileName: '',
|
||||
exportScope: 'TABLE',
|
||||
});
|
||||
|
||||
function resetSplitForm() {
|
||||
splitForm.splitMode = 'BY_ROW_COUNT';
|
||||
splitForm.rowBatchSize = 1000;
|
||||
splitForm.fieldName = '';
|
||||
splitForm.targetNamePrefix = '';
|
||||
}
|
||||
|
||||
function resetMergeForm() {
|
||||
mergeForm.mergeMode = 'VERTICAL';
|
||||
mergeForm.tableIds = [];
|
||||
mergeForm.targetTableName = '';
|
||||
mergeForm.joinKey = '';
|
||||
}
|
||||
|
||||
function resetDeriveForm() {
|
||||
deriveForm.targetTableName = '';
|
||||
deriveForm.selectedColumnsText = '';
|
||||
deriveForm.renameMappingsText = '{}';
|
||||
deriveForm.filtersText = '[]';
|
||||
}
|
||||
|
||||
function resetExportForm() {
|
||||
exportForm.fileName = '';
|
||||
exportForm.exportScope = 'TABLE';
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
if (!pendingUploadFile.value) {
|
||||
ElMessage.warning('请先选择 Excel 文件');
|
||||
return false;
|
||||
}
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', pendingUploadFile.value);
|
||||
await api.postFile('/api/v1/datacenterExcel/import', formData, {
|
||||
timeout: 10 * 60 * 1000,
|
||||
});
|
||||
ElMessage.success('Excel 已导入');
|
||||
pendingUploadFile.value = null;
|
||||
await reloadAll();
|
||||
return true;
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSplit() {
|
||||
if (!selectedTableId.value) return false;
|
||||
if (
|
||||
splitForm.splitMode === 'BY_ROW_COUNT' &&
|
||||
Number(splitForm.rowBatchSize) <= 0
|
||||
) {
|
||||
ElMessage.warning('每份行数必须大于 0');
|
||||
return false;
|
||||
}
|
||||
if (splitForm.splitMode === 'BY_FIELD_VALUE' && !splitForm.fieldName) {
|
||||
ElMessage.warning('请选择拆分字段');
|
||||
return false;
|
||||
}
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
await api.post('/api/v1/datacenterExcel/split', {
|
||||
datasetRef: { tableId: selectedTableId.value },
|
||||
sourceId: selectedSourceId.value,
|
||||
catalogId: selectedCatalogId.value,
|
||||
splitMode: splitForm.splitMode,
|
||||
rowBatchSize: Number(splitForm.rowBatchSize),
|
||||
fieldName: splitForm.fieldName,
|
||||
targetNamePrefix: splitForm.targetNamePrefix,
|
||||
});
|
||||
ElMessage.success('拆分已完成');
|
||||
await reloadAll();
|
||||
return true;
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMerge() {
|
||||
if (mergeForm.tableIds.length < 2) {
|
||||
ElMessage.warning('请至少选择两个已接入表');
|
||||
return false;
|
||||
}
|
||||
if (!mergeForm.targetTableName.trim()) {
|
||||
ElMessage.warning('请输入新表名称');
|
||||
return false;
|
||||
}
|
||||
if (mergeForm.mergeMode === 'HORIZONTAL' && !mergeForm.joinKey.trim()) {
|
||||
ElMessage.warning('横向合并需要填写关联字段');
|
||||
return false;
|
||||
}
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
await api.post('/api/v1/datacenterExcel/merge', {
|
||||
datasetRefs: mergeForm.tableIds.map((tableId) => ({ tableId })),
|
||||
mergeMode: mergeForm.mergeMode,
|
||||
targetTableName: mergeForm.targetTableName.trim(),
|
||||
joinKey: mergeForm.joinKey.trim(),
|
||||
});
|
||||
ElMessage.success('合并已完成');
|
||||
await reloadAll();
|
||||
return true;
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDerive() {
|
||||
if (!selectedTableId.value) return false;
|
||||
if (!deriveForm.targetTableName.trim()) {
|
||||
ElMessage.warning('请输入新表名称');
|
||||
return false;
|
||||
}
|
||||
const selectedColumns = deriveForm.selectedColumnsText
|
||||
? deriveForm.selectedColumnsText
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
let renameMappings: Record<string, string>;
|
||||
let filters: any[];
|
||||
try {
|
||||
renameMappings = deriveForm.renameMappingsText
|
||||
? JSON.parse(deriveForm.renameMappingsText)
|
||||
: {};
|
||||
filters = deriveForm.filtersText
|
||||
? JSON.parse(deriveForm.filtersText)
|
||||
: [];
|
||||
} catch {
|
||||
ElMessage.error('生成参数格式不正确,请检查输入内容');
|
||||
return false;
|
||||
}
|
||||
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
await api.post('/api/v1/datacenterExcel/derive', {
|
||||
datasetRef: { tableId: selectedTableId.value },
|
||||
targetTableName: deriveForm.targetTableName.trim(),
|
||||
selectedColumns,
|
||||
renameMappings,
|
||||
filters,
|
||||
});
|
||||
ElMessage.success('新表已生成');
|
||||
await reloadAll();
|
||||
return true;
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExport(loadTableRuntime: () => Promise<void>) {
|
||||
if (exportForm.exportScope === 'TABLE' && !selectedTableId.value) {
|
||||
ElMessage.warning('请先选择已接入表');
|
||||
return false;
|
||||
}
|
||||
actionLoading.value = true;
|
||||
try {
|
||||
const res = await api.post('/api/v1/datacenterExcel/export', {
|
||||
sourceId:
|
||||
exportForm.exportScope === 'WORKBOOK'
|
||||
? selectedSourceId.value
|
||||
: undefined,
|
||||
catalogId:
|
||||
exportForm.exportScope === 'WORKBOOK'
|
||||
? selectedCatalogId.value
|
||||
: undefined,
|
||||
datasetRefs:
|
||||
exportForm.exportScope === 'TABLE' && selectedTableId.value
|
||||
? [{ tableId: selectedTableId.value }]
|
||||
: [],
|
||||
fileName: exportForm.fileName.trim(),
|
||||
});
|
||||
const blob = await api.download('/api/v1/datacenterExcel/download', {
|
||||
params: { jobId: res.data.id },
|
||||
});
|
||||
downloadFileFromBlob({
|
||||
fileName:
|
||||
res.data.fileName || exportForm.fileName || 'datacenter-export.xlsx',
|
||||
source: blob,
|
||||
});
|
||||
ElMessage.success('导出文件已生成');
|
||||
await loadTableRuntime();
|
||||
return true;
|
||||
} finally {
|
||||
actionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
actionLoading,
|
||||
pendingUploadFile,
|
||||
splitForm,
|
||||
mergeForm,
|
||||
deriveForm,
|
||||
exportForm,
|
||||
resetSplitForm,
|
||||
resetMergeForm,
|
||||
resetDeriveForm,
|
||||
resetExportForm,
|
||||
handleImport,
|
||||
handleSplit,
|
||||
handleMerge,
|
||||
handleDerive,
|
||||
handleExport,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
|
||||
import { mergeConfigJsonBySourceType } from './datacenter-constants';
|
||||
|
||||
export function useDatacenterSources() {
|
||||
const sources = ref<any[]>([]);
|
||||
const selectedSourceId = ref<null | number>(null);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const testing = ref(false);
|
||||
|
||||
const selectedSource = computed(
|
||||
() =>
|
||||
sources.value.find((item) => item.id === selectedSourceId.value) || null,
|
||||
);
|
||||
|
||||
function normalizeSource(record: any, previous?: any) {
|
||||
return {
|
||||
...previous,
|
||||
...record,
|
||||
runtimeUnavailable: Boolean(
|
||||
record?.runtimeUnavailable ?? previous?.runtimeUnavailable,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function syncSelectedSourceAfterMutation() {
|
||||
if (sources.value.length === 0) {
|
||||
selectedSourceId.value = null;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!selectedSourceId.value ||
|
||||
!sources.value.some((item) => item.id === selectedSourceId.value)
|
||||
) {
|
||||
selectedSourceId.value = sources.value[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
function upsertSource(record?: any) {
|
||||
if (!record?.id) return;
|
||||
const previous = sources.value.find((item) => item.id === record.id);
|
||||
const next = dedupeBuiltinSources(
|
||||
sources.value.some((item) => item.id === record.id)
|
||||
? sources.value.map((item) =>
|
||||
item.id === record.id ? normalizeSource(record, previous) : item,
|
||||
)
|
||||
: [normalizeSource(record), ...sources.value],
|
||||
);
|
||||
sources.value = next;
|
||||
selectedSourceId.value = record.id;
|
||||
}
|
||||
|
||||
function markSourceRuntimeUnavailable(sourceId?: null | number | string) {
|
||||
if (!sourceId) return;
|
||||
sources.value = sources.value.map((item) =>
|
||||
String(item.id) === String(sourceId)
|
||||
? { ...item, runtimeUnavailable: true }
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function clearSourceRuntimeUnavailable(sourceId?: null | number | string) {
|
||||
if (!sourceId) return;
|
||||
sources.value = sources.value.map((item) =>
|
||||
String(item.id) === String(sourceId)
|
||||
? { ...item, runtimeUnavailable: false }
|
||||
: item,
|
||||
);
|
||||
}
|
||||
|
||||
function removeSourceFromState(sourceId: number) {
|
||||
sources.value = sources.value.filter((item) => item.id !== sourceId);
|
||||
syncSelectedSourceAfterMutation();
|
||||
}
|
||||
|
||||
function dedupeBuiltinSources(records: any[]) {
|
||||
const builtinSourceTypes = new Set(['PROJECT_MYSQL']);
|
||||
const seen = new Set<string>();
|
||||
|
||||
return (records || []).filter((item) => {
|
||||
const uniqueKey = `${item.sourceType}:${item.sourceName}`;
|
||||
if (!item.builtinFlag || !builtinSourceTypes.has(item.sourceType)) {
|
||||
return true;
|
||||
}
|
||||
if (seen.has(uniqueKey)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(uniqueKey);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSources() {
|
||||
const res = await api.get('/api/v1/datacenterSource/page', {
|
||||
params: { pageNumber: 1, pageSize: 200 },
|
||||
});
|
||||
const previousMap = new Map(
|
||||
sources.value.map((item) => [String(item.id), item]),
|
||||
);
|
||||
sources.value = dedupeBuiltinSources(
|
||||
(res.data?.records || []).map((record: any) =>
|
||||
normalizeSource(record, previousMap.get(String(record.id))),
|
||||
),
|
||||
);
|
||||
syncSelectedSourceAfterMutation();
|
||||
}
|
||||
|
||||
async function saveSource(form: Record<string, any>) {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
...form,
|
||||
configJson: {
|
||||
...mergeConfigJsonBySourceType(form.sourceType, form.configJson),
|
||||
password: form.password || undefined,
|
||||
},
|
||||
};
|
||||
const res = await api.post('/api/v1/datacenterSource/save', payload);
|
||||
upsertSource(res.data);
|
||||
return res.data;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testConnection(target: Record<string, any>) {
|
||||
testing.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
...target,
|
||||
configJson: {
|
||||
...mergeConfigJsonBySourceType(target.sourceType, target.configJson),
|
||||
password: target.password || undefined,
|
||||
},
|
||||
};
|
||||
const res = await api.post(
|
||||
'/api/v1/datacenterSource/testConnection',
|
||||
payload,
|
||||
);
|
||||
return res.data;
|
||||
} finally {
|
||||
testing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function quickTest(row: any) {
|
||||
testing.value = true;
|
||||
try {
|
||||
const res = await api.post(
|
||||
'/api/v1/datacenterSource/testConnection',
|
||||
row,
|
||||
);
|
||||
clearSourceRuntimeUnavailable(row?.id);
|
||||
await loadSources();
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
markSourceRuntimeUnavailable(row?.id);
|
||||
throw error;
|
||||
} finally {
|
||||
testing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSource(sourceId: number) {
|
||||
await api.post('/api/v1/datacenterSource/remove', { sourceId });
|
||||
removeSourceFromState(sourceId);
|
||||
}
|
||||
|
||||
return {
|
||||
sources,
|
||||
selectedSourceId,
|
||||
selectedSource,
|
||||
loading,
|
||||
saving,
|
||||
testing,
|
||||
loadSources,
|
||||
removeSource,
|
||||
removeSourceFromState,
|
||||
saveSource,
|
||||
syncSelectedSourceAfterMutation,
|
||||
testConnection,
|
||||
quickTest,
|
||||
upsertSource,
|
||||
markSourceRuntimeUnavailable,
|
||||
clearSourceRuntimeUnavailable,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
interface DatacenterTableRuntimeOptions {
|
||||
clearSourceRuntimeUnavailable?: (sourceId?: null | number | string) => void;
|
||||
markSourceRuntimeUnavailable?: (sourceId?: null | number | string) => void;
|
||||
}
|
||||
|
||||
export function useDatacenterTables(
|
||||
selectedSourceId: Ref<null | number>,
|
||||
options: DatacenterTableRuntimeOptions = {},
|
||||
) {
|
||||
const SOURCE_MISSING_MESSAGE = '连接不存在';
|
||||
const SOURCE_UNAVAILABLE_MESSAGE = '当前连接不可用,请检查连接配置后重试';
|
||||
const catalogs = ref<any[]>([]);
|
||||
const sourceTables = ref<any[]>([]);
|
||||
const managedTables = ref<any[]>([]);
|
||||
const schema = ref<any>(null);
|
||||
const previewRows = ref<any[]>([]);
|
||||
const jobs = ref<any[]>([]);
|
||||
const sourceUnavailable = ref(false);
|
||||
const selectedCatalogId = ref<null | number>(null);
|
||||
const selectedTableId = ref<null | number>(null);
|
||||
const previewLoading = ref(false);
|
||||
|
||||
const selectedCatalog = computed(
|
||||
() =>
|
||||
catalogs.value.find((item) => item.id === selectedCatalogId.value) ||
|
||||
null,
|
||||
);
|
||||
|
||||
const selectedTable = computed(
|
||||
() =>
|
||||
managedTables.value.find((item) => item.id === selectedTableId.value) ||
|
||||
null,
|
||||
);
|
||||
|
||||
function isSourceUnavailableError(error: any) {
|
||||
const responseData = error?.response?.data ?? {};
|
||||
const message = String(responseData?.message ?? error?.message ?? '');
|
||||
return message.includes(SOURCE_UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
|
||||
function isSourceMissingError(error: any) {
|
||||
const responseData = error?.response?.data ?? {};
|
||||
const message = String(responseData?.message ?? error?.message ?? '');
|
||||
return message.includes(SOURCE_MISSING_MESSAGE);
|
||||
}
|
||||
|
||||
function markSourceUnavailable() {
|
||||
options.markSourceRuntimeUnavailable?.(selectedSourceId.value);
|
||||
catalogs.value = [];
|
||||
sourceTables.value = [];
|
||||
managedTables.value = [];
|
||||
schema.value = null;
|
||||
previewRows.value = [];
|
||||
jobs.value = [];
|
||||
selectedCatalogId.value = null;
|
||||
selectedTableId.value = null;
|
||||
sourceUnavailable.value = true;
|
||||
}
|
||||
|
||||
async function loadCatalogs() {
|
||||
if (!selectedSourceId.value) {
|
||||
catalogs.value = [];
|
||||
selectedCatalogId.value = null;
|
||||
sourceUnavailable.value = false;
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const data = await requestClient.get(
|
||||
'/api/v1/datacenterSource/catalogs',
|
||||
{
|
||||
params: { sourceId: selectedSourceId.value },
|
||||
},
|
||||
);
|
||||
options.clearSourceRuntimeUnavailable?.(selectedSourceId.value);
|
||||
catalogs.value = data || [];
|
||||
sourceUnavailable.value = false;
|
||||
if (catalogs.value.length === 0) {
|
||||
selectedCatalogId.value = null;
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!selectedCatalogId.value ||
|
||||
!catalogs.value.some((item) => item.id === selectedCatalogId.value)
|
||||
) {
|
||||
selectedCatalogId.value = catalogs.value[0].id;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isSourceMissingError(error)) {
|
||||
markSourceUnavailable();
|
||||
return false;
|
||||
}
|
||||
if (!isSourceUnavailableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
markSourceUnavailable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSourceTables() {
|
||||
if (!selectedSourceId.value) {
|
||||
sourceTables.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await requestClient.get('/api/v1/datacenterSource/tables', {
|
||||
params: {
|
||||
sourceId: selectedSourceId.value,
|
||||
catalogName: selectedCatalog.value?.catalogName,
|
||||
},
|
||||
});
|
||||
options.clearSourceRuntimeUnavailable?.(selectedSourceId.value);
|
||||
sourceTables.value = data || [];
|
||||
sourceUnavailable.value = false;
|
||||
} catch (error) {
|
||||
if (!isSourceUnavailableError(error) && !isSourceMissingError(error)) {
|
||||
throw error;
|
||||
}
|
||||
markSourceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadManagedTables() {
|
||||
if (!selectedSourceId.value) {
|
||||
managedTables.value = [];
|
||||
selectedTableId.value = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await requestClient.get(
|
||||
'/api/v1/datacenterDataset/managedTables',
|
||||
{
|
||||
params: {
|
||||
sourceId: selectedSourceId.value,
|
||||
catalogId: selectedCatalogId.value,
|
||||
},
|
||||
},
|
||||
);
|
||||
managedTables.value = data || [];
|
||||
} catch (error) {
|
||||
if (!isSourceUnavailableError(error) && !isSourceMissingError(error)) {
|
||||
throw error;
|
||||
}
|
||||
markSourceUnavailable();
|
||||
return;
|
||||
}
|
||||
if (managedTables.value.length === 0) {
|
||||
selectedTableId.value = null;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!selectedTableId.value ||
|
||||
!managedTables.value.some((item) => item.id === selectedTableId.value)
|
||||
) {
|
||||
selectedTableId.value = managedTables.value[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTableRuntime() {
|
||||
if (!selectedTableId.value) {
|
||||
schema.value = null;
|
||||
previewRows.value = [];
|
||||
jobs.value = [];
|
||||
return;
|
||||
}
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const [schemaRes, previewRes, jobsRes] = await Promise.allSettled([
|
||||
requestClient.get('/api/v1/datacenterDataset/schema', {
|
||||
params: { tableId: selectedTableId.value },
|
||||
}),
|
||||
requestClient.post('/api/v1/datacenterDataset/queryPage', {
|
||||
datasetRef: { tableId: selectedTableId.value },
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
}),
|
||||
requestClient.get('/api/v1/datacenterExcel/job/list', {
|
||||
params: {
|
||||
sourceId: selectedSourceId.value,
|
||||
tableId: selectedTableId.value,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
if (schemaRes.status === 'fulfilled') {
|
||||
schema.value = schemaRes.value;
|
||||
} else if (
|
||||
isSourceUnavailableError(schemaRes.reason) ||
|
||||
isSourceMissingError(schemaRes.reason)
|
||||
) {
|
||||
markSourceUnavailable();
|
||||
return;
|
||||
} else {
|
||||
throw schemaRes.reason;
|
||||
}
|
||||
if (jobsRes.status === 'fulfilled') {
|
||||
jobs.value = jobsRes.value || [];
|
||||
} else if (
|
||||
isSourceUnavailableError(jobsRes.reason) ||
|
||||
isSourceMissingError(jobsRes.reason)
|
||||
) {
|
||||
markSourceUnavailable();
|
||||
return;
|
||||
} else {
|
||||
throw jobsRes.reason;
|
||||
}
|
||||
if (previewRes.status === 'fulfilled') {
|
||||
options.clearSourceRuntimeUnavailable?.(selectedSourceId.value);
|
||||
previewRows.value = previewRes.value?.records || [];
|
||||
sourceUnavailable.value = false;
|
||||
} else if (
|
||||
isSourceUnavailableError(previewRes.reason) ||
|
||||
isSourceMissingError(previewRes.reason)
|
||||
) {
|
||||
markSourceUnavailable();
|
||||
} else {
|
||||
throw previewRes.reason;
|
||||
}
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncSourceContext() {
|
||||
const catalogAvailable = await loadCatalogs();
|
||||
if (catalogAvailable) {
|
||||
await Promise.all([loadSourceTables(), loadManagedTables()]);
|
||||
} else {
|
||||
sourceTables.value = [];
|
||||
await loadManagedTables();
|
||||
}
|
||||
await loadTableRuntime();
|
||||
}
|
||||
|
||||
async function registerTable(row: any) {
|
||||
const data = await requestClient.get(
|
||||
'/api/v1/datacenterSource/tableDetail',
|
||||
{
|
||||
params: {
|
||||
sourceId: selectedSourceId.value,
|
||||
catalogName: selectedCatalog.value?.catalogName,
|
||||
tableName: row.tableName,
|
||||
register: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
ElMessage.success('已接入数据中心');
|
||||
await loadManagedTables();
|
||||
selectedTableId.value = data?.table?.id || selectedTableId.value;
|
||||
await loadTableRuntime();
|
||||
}
|
||||
|
||||
async function batchRegisterTables(rows: any[]) {
|
||||
const tableNames = (rows || [])
|
||||
.map((row) => row?.tableName)
|
||||
.filter(Boolean);
|
||||
if (tableNames.length === 0) return;
|
||||
await requestClient.post('/api/v1/datacenterSource/registerBatch', {
|
||||
sourceId: selectedSourceId.value,
|
||||
catalogName: selectedCatalog.value?.catalogName,
|
||||
tableNames,
|
||||
});
|
||||
ElMessage.success(`已接入 ${tableNames.length} 张表`);
|
||||
await loadManagedTables();
|
||||
}
|
||||
|
||||
async function batchRemoveTables(rows: any[]) {
|
||||
const tableIds = (rows || []).map((row) => row?.id).filter(Boolean);
|
||||
if (tableIds.length === 0) return;
|
||||
await requestClient.post('/api/v1/datacenterDataset/removeBatch', {
|
||||
tableIds,
|
||||
});
|
||||
ElMessage.success(`已去除 ${tableIds.length} 张表`);
|
||||
if (selectedTableId.value && tableIds.includes(selectedTableId.value)) {
|
||||
selectedTableId.value = null;
|
||||
schema.value = null;
|
||||
previewRows.value = [];
|
||||
jobs.value = [];
|
||||
}
|
||||
await Promise.all([loadSourceTables(), loadManagedTables()]);
|
||||
}
|
||||
|
||||
async function saveDescriptions(payload: {
|
||||
fields?: Array<{ fieldDesc: string; fieldId: number | string }>;
|
||||
tableDesc?: string;
|
||||
tableId: number | string;
|
||||
}) {
|
||||
const data = await requestClient.post(
|
||||
'/api/v1/datacenterDataset/saveDescriptions',
|
||||
{
|
||||
fields: payload.fields || [],
|
||||
tableDesc: payload.tableDesc ?? '',
|
||||
tableId: payload.tableId,
|
||||
},
|
||||
);
|
||||
await loadManagedTables();
|
||||
if (selectedTableId.value === payload.tableId) {
|
||||
schema.value = data || null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
return {
|
||||
catalogs,
|
||||
sourceTables,
|
||||
managedTables,
|
||||
schema,
|
||||
previewRows,
|
||||
jobs,
|
||||
sourceUnavailable,
|
||||
selectedCatalogId,
|
||||
selectedTableId,
|
||||
selectedCatalog,
|
||||
selectedTable,
|
||||
previewLoading,
|
||||
loadCatalogs,
|
||||
loadSourceTables,
|
||||
loadManagedTables,
|
||||
loadTableRuntime,
|
||||
syncSourceContext,
|
||||
registerTable,
|
||||
batchRegisterTables,
|
||||
batchRemoveTables,
|
||||
saveDescriptions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import type { FormRules } from 'element-plus';
|
||||
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
mergeConfigJsonBySourceType,
|
||||
sourceConnectionDefaults,
|
||||
sourceTypeLabels,
|
||||
} from './datacenter-constants';
|
||||
|
||||
export function useSourceForm() {
|
||||
const lastGeneratedJdbcUrl = ref('');
|
||||
const lastGeneratedDriverClassName = ref('');
|
||||
|
||||
const form = reactive<Record<string, any>>({
|
||||
id: undefined,
|
||||
sourceName: '',
|
||||
sourceCode: '',
|
||||
sourceType: 'MYSQL',
|
||||
accessMode: 'READ_ONLY',
|
||||
driverClassName: '',
|
||||
jdbcUrl: '',
|
||||
host: '',
|
||||
port: undefined,
|
||||
databaseName: '',
|
||||
schemaName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
builtinFlag: 0,
|
||||
configJson: {},
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
sourceName: [
|
||||
{ required: true, message: '请输入连接名称', trigger: 'blur' },
|
||||
],
|
||||
sourceType: [
|
||||
{ required: true, message: '请选择连接类型', trigger: 'change' },
|
||||
],
|
||||
databaseName: [
|
||||
{
|
||||
trigger: ['blur', 'change'],
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!sourceConnectionDefaults[form.sourceType]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (String(value || '').trim()) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
callback(new Error('请输入库名'));
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const selectedTypeDefaults = computed(
|
||||
() => sourceConnectionDefaults[form.sourceType] || null,
|
||||
);
|
||||
|
||||
const supportsExternalConnection = computed(() =>
|
||||
Boolean(selectedTypeDefaults.value),
|
||||
);
|
||||
|
||||
const connectionTypeLabel = computed(() =>
|
||||
form.sourceType ? sourceTypeLabels[form.sourceType] || form.sourceType : '',
|
||||
);
|
||||
|
||||
const connectionHelpText = computed(() => {
|
||||
if (form.sourceType === 'EXCEL') {
|
||||
return 'Excel 连接只需要名称,文件导入后会自动生成表。';
|
||||
}
|
||||
if (!supportsExternalConnection.value) {
|
||||
return '系统内置连接由平台自动维护,无需填写地址。';
|
||||
}
|
||||
return `填写主机、库名和账号即可,端口与连接地址会按 ${connectionTypeLabel.value} 自动补全。`;
|
||||
});
|
||||
|
||||
function generateJdbcUrl(payload: Record<string, any>) {
|
||||
return (
|
||||
sourceConnectionDefaults[payload.sourceType]?.buildJdbcUrl(payload) || ''
|
||||
);
|
||||
}
|
||||
|
||||
function syncGeneratedJdbcUrl(force = false) {
|
||||
const generated = generateJdbcUrl(form);
|
||||
if (!generated) {
|
||||
if (
|
||||
force ||
|
||||
!form.jdbcUrl ||
|
||||
form.jdbcUrl === lastGeneratedJdbcUrl.value
|
||||
) {
|
||||
form.jdbcUrl = '';
|
||||
}
|
||||
lastGeneratedJdbcUrl.value = '';
|
||||
return;
|
||||
}
|
||||
if (
|
||||
force ||
|
||||
!form.jdbcUrl ||
|
||||
form.jdbcUrl === lastGeneratedJdbcUrl.value
|
||||
) {
|
||||
form.jdbcUrl = generated;
|
||||
lastGeneratedJdbcUrl.value = generated;
|
||||
} else if (form.jdbcUrl === generated) {
|
||||
lastGeneratedJdbcUrl.value = generated;
|
||||
}
|
||||
}
|
||||
|
||||
function applySourceTypeDefaults(force = false) {
|
||||
const defaults = selectedTypeDefaults.value;
|
||||
form.configJson = mergeConfigJsonBySourceType(
|
||||
form.sourceType,
|
||||
form.configJson,
|
||||
);
|
||||
if (!defaults) {
|
||||
if (force) {
|
||||
form.port = undefined;
|
||||
form.driverClassName = '';
|
||||
form.jdbcUrl = '';
|
||||
lastGeneratedDriverClassName.value = '';
|
||||
lastGeneratedJdbcUrl.value = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (force || !form.port) {
|
||||
form.port = defaults.defaultPort;
|
||||
}
|
||||
if (
|
||||
force ||
|
||||
!form.driverClassName ||
|
||||
form.driverClassName === lastGeneratedDriverClassName.value
|
||||
) {
|
||||
form.driverClassName = defaults.defaultDriver;
|
||||
lastGeneratedDriverClassName.value = defaults.defaultDriver;
|
||||
}
|
||||
syncGeneratedJdbcUrl(force);
|
||||
}
|
||||
|
||||
function resetForm(row?: any) {
|
||||
form.id = row?.id;
|
||||
form.sourceName = row?.sourceName || '';
|
||||
form.sourceCode = row?.sourceCode || '';
|
||||
form.sourceType = row?.sourceType || 'MYSQL';
|
||||
form.accessMode = row?.accessMode || 'READ_ONLY';
|
||||
form.driverClassName = row?.driverClassName || '';
|
||||
form.jdbcUrl = row?.jdbcUrl || '';
|
||||
form.host = row?.host || '';
|
||||
form.port = row?.port;
|
||||
form.databaseName = row?.databaseName || '';
|
||||
form.schemaName = row?.schemaName || '';
|
||||
form.username = row?.username || '';
|
||||
form.password = '';
|
||||
form.builtinFlag = row?.builtinFlag || 0;
|
||||
form.configJson = mergeConfigJsonBySourceType(
|
||||
form.sourceType,
|
||||
row?.configJson || {},
|
||||
);
|
||||
lastGeneratedDriverClassName.value = '';
|
||||
lastGeneratedJdbcUrl.value = '';
|
||||
applySourceTypeDefaults(false);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => form.sourceType,
|
||||
() => applySourceTypeDefaults(true),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [
|
||||
form.host,
|
||||
form.port,
|
||||
form.databaseName,
|
||||
form.configJson?.informixServer,
|
||||
form.configJson?.serviceName,
|
||||
],
|
||||
() => syncGeneratedJdbcUrl(false),
|
||||
);
|
||||
|
||||
return {
|
||||
form,
|
||||
rules,
|
||||
supportsExternalConnection,
|
||||
connectionHelpText,
|
||||
resetForm,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user