feat: 重构数据中枢工作台与接入管理

- 新增统一的数据源、目录、纳管表与 Excel 处理后端能力

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

- 补充数据中枢迁移脚本、连接器底座与说明字段支持
This commit is contained in:
2026-04-02 18:55:31 +08:00
parent b6213d0933
commit 798effbd5b
117 changed files with 9739 additions and 1824 deletions

View File

@@ -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>