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

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

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

531 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { FormInstance, UploadFile, UploadInstance } 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,
UploadFilled,
} from '@element-plus/icons-vue';
import {
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElMessage,
ElOption,
ElSelect,
ElUpload,
} from 'element-plus';
import {
sourceTypeLabels,
sourceTypeOptions,
} from '../composables/datacenter-constants';
import {
EXCEL_FILE_ACCEPT,
isSupportedExcelFileName,
sourceNameFromExcelFileName,
} from '../composables/datacenter-excel-upload';
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>, excelFile?: File];
test: [form: Record<string, any>];
'update:visible': [value: boolean];
}>();
const formRef = ref<FormInstance>();
const excelUploadRef = ref<UploadInstance>();
const testResult = ref<any>(null);
const showAdvanced = ref(false);
const pendingExcelFile = ref<File | null>(null);
const excelFileError = ref('');
const lastGeneratedSourceName = ref('');
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,
);
const isExcelCreate = computed(() => form.sourceType === 'EXCEL' && !form.id);
const confirmDisabled = computed(
() =>
isExcelCreate.value &&
(!pendingExcelFile.value || !String(form.sourceName || '').trim()),
);
function resetExcelFile() {
excelUploadRef.value?.clearFiles();
pendingExcelFile.value = null;
excelFileError.value = '';
lastGeneratedSourceName.value = '';
}
function resetDialogState() {
testResult.value = null;
showAdvanced.value = false;
resetExcelFile();
}
function open(row?: any) {
baseResetForm(row);
resetDialogState();
emit('update:visible', true);
}
function close() {
emit('update:visible', false);
resetDialogState();
}
async function handleSave() {
if (isExcelCreate.value && !pendingExcelFile.value) {
excelFileError.value = '请选择需要导入的 Excel 文件';
return;
}
await formRef.value?.validate();
emit(
'save',
{ ...form },
isExcelCreate.value ? pendingExcelFile.value || undefined : undefined,
);
}
async function handleTest() {
await formRef.value?.validate();
testResult.value = null;
emit('test', { ...form });
}
function handleBeforeClose() {
resetDialogState();
}
function setTestResult(result: any) {
testResult.value = result;
}
function handleUploadChange(file: UploadFile) {
if (!isSupportedExcelFileName(file.name)) {
excelUploadRef.value?.clearFiles();
pendingExcelFile.value = null;
excelFileError.value = '仅支持 .xls 和 .xlsx 格式的 Excel 文件';
ElMessage.warning(excelFileError.value);
return;
}
const generatedSourceName = sourceNameFromExcelFileName(file.name);
if (
!String(form.sourceName || '').trim() ||
form.sourceName === lastGeneratedSourceName.value
) {
form.sourceName = generatedSourceName;
formRef.value?.clearValidate('sourceName');
}
lastGeneratedSourceName.value = generatedSourceName;
pendingExcelFile.value = file.raw || null;
excelFileError.value = '';
}
function handleUploadRemove() {
pendingExcelFile.value = null;
excelFileError.value = '';
}
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;
}
},
);
watch(
() => form.sourceType,
(_sourceType, previousSourceType) => {
if (previousSourceType && previousSourceType !== form.sourceType) {
resetExcelFile();
}
},
);
defineExpose({ close, open, setTestResult });
</script>
<template>
<EasyFlowFormModal
:open="visible"
:title="modalTitle"
:before-close="handleBeforeClose"
:confirm-disabled="confirmDisabled"
: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"
maxlength="100"
:placeholder="
isExcelCreate ? '选择文件后自动填写' : '例如:华东 MySQL'
"
/>
</ElFormItem>
<ElFormItem label="连接类型" prop="sourceType">
<ElSelect
v-model="form.sourceType"
class="w-full"
:disabled="Boolean(form.builtinFlag || form.id)"
>
<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>
<ElFormItem
v-if="isExcelCreate"
label="表格文件"
required
:error="excelFileError"
class="excel-file-field"
>
<ElUpload
ref="excelUploadRef"
class="excel-upload"
drag
:accept="EXCEL_FILE_ACCEPT"
:auto-upload="false"
:show-file-list="true"
:limit="1"
:disabled="saving"
:on-change="handleUploadChange"
:on-remove="handleUploadRemove"
>
<ElIcon class="excel-upload__icon">
<UploadFilled />
</ElIcon>
<div class="excel-upload__title">拖拽或点击选择 Excel 文件</div>
<div class="excel-upload__description">
仅支持 .xls.xlsx工作表首行需包含表头
</div>
</ElUpload>
</ElFormItem>
<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
v-if="supportsExternalConnection"
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;
gap: 10px;
align-items: center;
}
.source-type-select-icon,
.source-type-option__icon {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
}
.source-type-option {
display: inline-flex;
gap: 10px;
align-items: center;
}
.source-type-option__label {
line-height: 1.2;
}
.span-2 {
grid-column: 1 / -1;
}
.excel-file-field {
margin-bottom: 0;
}
.excel-upload {
width: 100%;
}
.excel-upload :deep(.el-upload) {
width: 100%;
}
.excel-upload :deep(.el-upload-dragger) {
width: 100%;
padding: 24px;
background: hsl(var(--surface-subtle) / 72%);
border-color: hsl(var(--border) / 76%);
border-radius: var(--radius-panel);
transition:
background-color 0.16s,
border-color 0.16s;
}
.excel-upload :deep(.el-upload-dragger:hover),
.excel-upload :deep(.el-upload-dragger:focus-visible) {
background: hsl(var(--primary) / 5%);
border-color: hsl(var(--primary) / 56%);
}
.excel-upload__icon {
margin-bottom: 8px;
font-size: 28px;
color: hsl(var(--primary));
}
.excel-upload__title {
font-size: 14px;
font-weight: 600;
color: hsl(var(--foreground));
}
.excel-upload__description {
margin-top: 6px;
font-size: 12px;
line-height: 1.5;
color: hsl(var(--muted-foreground));
}
.excel-upload :deep(.el-upload-list) {
margin-top: 8px;
}
.excel-upload :deep(.el-upload-list__item) {
border-radius: var(--radius-control);
}
.toggle-advanced {
display: inline-flex;
align-items: center;
width: fit-content;
padding: 0;
font-size: 13px;
font-weight: 500;
color: hsl(var(--primary));
cursor: pointer;
background: transparent;
border: 0;
}
.toggle-advanced:hover {
color: hsl(var(--primary) / 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));
background: hsl(var(--success) / 8%);
border-color: hsl(var(--success) / 24%);
}
.test-button.is-test-success:hover {
color: hsl(var(--success));
background: hsl(var(--success) / 12%);
border-color: hsl(var(--success) / 28%);
}
.test-button.is-test-failed {
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 8%);
border-color: hsl(var(--destructive) / 22%);
}
.test-button.is-test-failed:hover {
color: hsl(var(--destructive));
background: hsl(var(--destructive) / 12%);
border-color: hsl(var(--destructive) / 28%);
}
</style>