531 lines
13 KiB
Vue
531 lines
13 KiB
Vue
<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>
|