发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
11 changed files with 301 additions and 233 deletions
Showing only changes of commit 219e4f7eff - Show all commits

View File

@@ -1,37 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TreeNode } from './composables/use-connection-tree'; import type { TreeNode } from './composables/use-connection-tree';
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'; import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import { import {
EasyFlowButton,
ResizableHandle, ResizableHandle,
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from '@easyflow-core/shadcn-ui'; } from '@easyflow-core/shadcn-ui';
import { import { ElEmpty, ElMessage, ElMessageBox } from 'element-plus';
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElEmpty,
ElMessageBox,
} from 'element-plus';
import ConnectionTree from './components/ConnectionTree.vue'; import ConnectionTree from './components/ConnectionTree.vue';
import ExcelActionDrawer from './components/ExcelActionDrawer.vue';
import SourceFormDrawer from './components/SourceFormDrawer.vue'; import SourceFormDrawer from './components/SourceFormDrawer.vue';
import TableDetailView from './components/TableDetailView.vue'; import TableDetailView from './components/TableDetailView.vue';
import TableListView from './components/TableListView.vue'; import TableListView from './components/TableListView.vue';
import { useConnectionTree } from './composables/use-connection-tree'; import { useConnectionTree } from './composables/use-connection-tree';
import { useDatacenterExcel } from './composables/use-datacenter-excel';
import { useDatacenterSources } from './composables/use-datacenter-sources'; import { useDatacenterSources } from './composables/use-datacenter-sources';
import { useDatacenterTables } from './composables/use-datacenter-tables'; import { useDatacenterTables } from './composables/use-datacenter-tables';
const sourceFormRef = ref<InstanceType<typeof SourceFormDrawer>>(); const sourceFormRef = ref<InstanceType<typeof SourceFormDrawer>>();
const excelActionRef = ref<InstanceType<typeof ExcelActionDrawer>>();
const sourceFormVisible = ref(false); const sourceFormVisible = ref(false);
const excelActionVisible = ref(false);
const workspaceRef = ref<HTMLElement>(); const workspaceRef = ref<HTMLElement>();
const workspaceHeight = ref('100%'); const workspaceHeight = ref('100%');
@@ -42,6 +31,7 @@ const {
loading, loading,
saving, saving,
testing, testing,
importExcelSource,
loadSources, loadSources,
removeSource, removeSource,
saveSource, saveSource,
@@ -76,7 +66,7 @@ async function reloadAll(options?: {
resetTable?: boolean; resetTable?: boolean;
}) { }) {
await loadSources(); await loadSources();
if (!selectedSourceId.value) { if (selectedSourceId.value === null) {
selectedCatalogId.value = null; selectedCatalogId.value = null;
selectedTableId.value = null; selectedTableId.value = null;
selectedNodeKey.value = ''; selectedNodeKey.value = '';
@@ -97,61 +87,11 @@ async function reloadAll(options?: {
viewMode.value = 'list'; 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 // 视图状态empty | list | detail
const selectedNodeKey = ref(''); const selectedNodeKey = ref('');
const viewMode = ref<'detail' | 'empty' | 'list'>('empty'); 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 { treeData, parseNodeKey } = useConnectionTree(sources);
const selectedFieldOptions = computed(() =>
(schema.value?.fields || []).map((f: any) => ({
label: f.fieldName,
value: f.fieldName,
})),
);
async function handleNodeSelect(node: TreeNode) { async function handleNodeSelect(node: TreeNode) {
selectedNodeKey.value = node.id; selectedNodeKey.value = node.id;
@@ -188,7 +128,10 @@ async function handleBatchRegister(rows: any[]) {
async function handleBatchRemove(rows: any[]) { async function handleBatchRemove(rows: any[]) {
const removedIds = rows.map((row) => row?.id).filter(Boolean); const removedIds = rows.map((row) => row?.id).filter(Boolean);
await batchRemoveTables(rows); await batchRemoveTables(rows);
if (selectedTableId.value && removedIds.includes(selectedTableId.value)) { if (
selectedTableId.value !== null &&
removedIds.includes(selectedTableId.value)
) {
viewMode.value = 'list'; viewMode.value = 'list';
} }
} }
@@ -245,8 +188,18 @@ async function handleRemoveSource(node: TreeNode) {
await reloadAll({ focus: 'source', resetTable: true }); await reloadAll({ focus: 'source', resetTable: true });
} }
async function handleSaveSource(formData: Record<string, any>) { async function handleSaveSource(
await saveSource(formData); formData: Record<string, any>,
excelFile?: File,
) {
if (formData.sourceType === 'EXCEL' && !formData.id) {
if (!excelFile) return;
await importExcelSource(excelFile, formData.sourceName);
ElMessage.success('Excel 已导入');
} else {
await saveSource(formData);
ElMessage.success(formData.id ? '连接已更新' : '连接已创建');
}
sourceFormRef.value?.close(); sourceFormRef.value?.close();
sourceFormVisible.value = false; sourceFormVisible.value = false;
await nextTick(); await nextTick();
@@ -259,58 +212,6 @@ async function handleTestConnection(formData: Record<string, any>) {
if (formData.id) await loadSources(); 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() { async function loadAll() {
loading.value = true; loading.value = true;
try { try {
@@ -346,53 +247,6 @@ onBeforeUnmount(() => {
:style="{ height: workspaceHeight }" :style="{ height: workspaceHeight }"
v-loading="loading" 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"> <ResizablePanelGroup direction="horizontal" class="workspace-body">
<ResizablePanel :default-size="20" :min-size="16" :max-size="30"> <ResizablePanel :default-size="20" :min-size="16" :max-size="30">
@@ -447,25 +301,6 @@ onBeforeUnmount(() => {
@save="handleSaveSource" @save="handleSaveSource"
@test="handleTestConnection" @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> </div>
</template> </template>
@@ -481,25 +316,6 @@ onBeforeUnmount(() => {
background: transparent; background: transparent;
} }
.workspace-toolbar {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-start;
padding: 0 0 12px;
}
.toolbar-left {
display: flex;
gap: 8px;
align-items: center;
}
.toolbar-button {
min-width: 84px;
box-shadow: none;
}
.workspace-body { .workspace-body {
flex: 1; flex: 1;
width: 100%; width: 100%;

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'element-plus'; import type { FormInstance, UploadFile, UploadInstance } from 'element-plus';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
@@ -7,20 +7,31 @@ import { EasyFlowFormModal } from '@easyflow/common-ui';
import { EasyFlowButton } from '@easyflow-core/shadcn-ui'; import { EasyFlowButton } from '@easyflow-core/shadcn-ui';
import { CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'; import {
CircleCheckFilled,
CircleCloseFilled,
UploadFilled,
} from '@element-plus/icons-vue';
import { import {
ElForm, ElForm,
ElFormItem, ElFormItem,
ElIcon, ElIcon,
ElInput, ElInput,
ElMessage,
ElOption, ElOption,
ElSelect, ElSelect,
ElUpload,
} from 'element-plus'; } from 'element-plus';
import { import {
sourceTypeLabels, sourceTypeLabels,
sourceTypeOptions, sourceTypeOptions,
} from '../composables/datacenter-constants'; } from '../composables/datacenter-constants';
import {
EXCEL_FILE_ACCEPT,
isSupportedExcelFileName,
sourceNameFromExcelFileName,
} from '../composables/datacenter-excel-upload';
import { useSourceForm } from '../composables/use-source-form'; import { useSourceForm } from '../composables/use-source-form';
import SourceBrandIcon from './SourceBrandIcon.vue'; import SourceBrandIcon from './SourceBrandIcon.vue';
@@ -31,13 +42,17 @@ const props = defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
save: [form: Record<string, any>]; save: [form: Record<string, any>, excelFile?: File];
test: [form: Record<string, any>]; test: [form: Record<string, any>];
'update:visible': [value: boolean]; 'update:visible': [value: boolean];
}>(); }>();
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const excelUploadRef = ref<UploadInstance>();
const testResult = ref<any>(null); const testResult = ref<any>(null);
const showAdvanced = ref(false); const showAdvanced = ref(false);
const pendingExcelFile = ref<File | null>(null);
const excelFileError = ref('');
const lastGeneratedSourceName = ref('');
const { const {
form, form,
@@ -60,10 +75,24 @@ const testButtonClass = computed(() => ({
const selectedSourceTypeLabel = computed( const selectedSourceTypeLabel = computed(
() => sourceTypeLabels[form.sourceType] || form.sourceType, () => 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() { function resetDialogState() {
testResult.value = null; testResult.value = null;
showAdvanced.value = false; showAdvanced.value = false;
resetExcelFile();
} }
function open(row?: any) { function open(row?: any) {
@@ -78,8 +107,16 @@ function close() {
} }
async function handleSave() { async function handleSave() {
if (isExcelCreate.value && !pendingExcelFile.value) {
excelFileError.value = '请选择需要导入的 Excel 文件';
return;
}
await formRef.value?.validate(); await formRef.value?.validate();
emit('save', { ...form }); emit(
'save',
{ ...form },
isExcelCreate.value ? pendingExcelFile.value || undefined : undefined,
);
} }
async function handleTest() { async function handleTest() {
@@ -96,6 +133,33 @@ function setTestResult(result: any) {
testResult.value = result; 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( watch(
() => [ () => [
form.sourceName, form.sourceName,
@@ -117,6 +181,15 @@ watch(
}, },
); );
watch(
() => form.sourceType,
(_sourceType, previousSourceType) => {
if (previousSourceType && previousSourceType !== form.sourceType) {
resetExcelFile();
}
},
);
defineExpose({ close, open, setTestResult }); defineExpose({ close, open, setTestResult });
</script> </script>
@@ -125,6 +198,7 @@ defineExpose({ close, open, setTestResult });
:open="visible" :open="visible"
:title="modalTitle" :title="modalTitle"
:before-close="handleBeforeClose" :before-close="handleBeforeClose"
:confirm-disabled="confirmDisabled"
:confirm-loading="saving" :confirm-loading="saving"
confirm-text="保存" confirm-text="保存"
:submitting="saving" :submitting="saving"
@@ -141,13 +215,19 @@ defineExpose({ close, open, setTestResult });
> >
<div class="form-grid"> <div class="form-grid">
<ElFormItem label="连接名称" prop="sourceName"> <ElFormItem label="连接名称" prop="sourceName">
<ElInput v-model="form.sourceName" placeholder="例如:华东 MySQL" /> <ElInput
v-model="form.sourceName"
maxlength="100"
:placeholder="
isExcelCreate ? '选择文件后自动填写' : '例如:华东 MySQL'
"
/>
</ElFormItem> </ElFormItem>
<ElFormItem label="连接类型" prop="sourceType"> <ElFormItem label="连接类型" prop="sourceType">
<ElSelect <ElSelect
v-model="form.sourceType" v-model="form.sourceType"
class="w-full" class="w-full"
:disabled="Boolean(form.builtinFlag)" :disabled="Boolean(form.builtinFlag || form.id)"
> >
<template #label> <template #label>
<span class="source-type-select-label"> <span class="source-type-select-label">
@@ -174,6 +254,35 @@ defineExpose({ close, open, setTestResult });
</ElFormItem> </ElFormItem>
</div> </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"> <template v-if="supportsExternalConnection">
<div class="form-grid"> <div class="form-grid">
<ElFormItem label="主机"> <ElFormItem label="主机">
@@ -242,6 +351,7 @@ defineExpose({ close, open, setTestResult });
<template #footer-extra> <template #footer-extra>
<EasyFlowButton <EasyFlowButton
v-if="supportsExternalConnection"
variant="outline" variant="outline"
:loading="testing" :loading="testing"
:disabled="saving" :disabled="saving"
@@ -308,6 +418,62 @@ defineExpose({ close, open, setTestResult });
grid-column: 1 / -1; 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 { .toggle-advanced {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@@ -101,7 +101,7 @@ async function saveFieldDescription(field: any) {
v-for="field in schema?.fields || []" v-for="field in schema?.fields || []"
:key="field.fieldName" :key="field.fieldName"
:prop="field.fieldName" :prop="field.fieldName"
:label="field.fieldName" :label="field.fieldDesc || field.fieldName"
min-width="140" min-width="140"
/> />
</ElTable> </ElTable>

View File

@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import {
isSupportedExcelFileName,
sourceNameFromExcelFileName,
} from './datacenter-excel-upload';
describe('datacenter Excel upload', () => {
it('accepts only xls and xlsx extensions', () => {
expect(isSupportedExcelFileName('预算表.XLS')).toBe(true);
expect(isSupportedExcelFileName('预算表.xlsx')).toBe(true);
expect(isSupportedExcelFileName('预算表.xlsm')).toBe(false);
expect(isSupportedExcelFileName('预算表.csv')).toBe(false);
});
it('derives the connection name from the final extension', () => {
expect(sourceNameFromExcelFileName('ama 实验基线模型预算.xlsx')).toBe(
'ama 实验基线模型预算',
);
expect(sourceNameFromExcelFileName('模型预算.v2.xls')).toBe('模型预算.v2');
});
});

View File

@@ -0,0 +1,22 @@
export const EXCEL_FILE_ACCEPT = '.xls,.xlsx';
/**
* 判断文件名是否属于数据中枢支持的 Excel 格式。
*/
export function isSupportedExcelFileName(fileName: string) {
const normalizedFileName = fileName.trim().toLowerCase();
return (
normalizedFileName.endsWith('.xls') || normalizedFileName.endsWith('.xlsx')
);
}
/**
* 从 Excel 文件名生成默认连接名称。
*/
export function sourceNameFromExcelFileName(fileName: string) {
const trimmedFileName = fileName.trim();
const extensionIndex = trimmedFileName.lastIndexOf('.');
return extensionIndex > 0
? trimmedFileName.slice(0, extensionIndex)
: trimmedFileName;
}

View File

@@ -0,0 +1,4 @@
/**
* 数据中枢接口中的雪花主键会以字符串返回,避免 JavaScript Number 丢失精度。
*/
export type DatacenterId = string;

View File

@@ -1,5 +1,7 @@
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { DatacenterId } from './datacenter-id';
import { computed } from 'vue'; import { computed } from 'vue';
import { formatSourceType, formatTestStatus } from './datacenter-constants'; import { formatSourceType, formatTestStatus } from './datacenter-constants';
@@ -49,7 +51,7 @@ export function useConnectionTree(sources: Ref<any[]>) {
const id = idParts.join('-'); const id = idParts.join('-');
return { return {
type: type as 'source' | 'table', type: type as 'source' | 'table',
id: Number(id), id: id as DatacenterId,
}; };
} }

View File

@@ -1,3 +1,5 @@
import type { DatacenterId } from './datacenter-id';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { api } from '#/api/request'; import { api } from '#/api/request';
@@ -6,7 +8,7 @@ import { mergeConfigJsonBySourceType } from './datacenter-constants';
export function useDatacenterSources() { export function useDatacenterSources() {
const sources = ref<any[]>([]); const sources = ref<any[]>([]);
const selectedSourceId = ref<null | number>(null); const selectedSourceId = ref<DatacenterId | null>(null);
const loading = ref(false); const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const testing = ref(false); const testing = ref(false);
@@ -32,7 +34,7 @@ export function useDatacenterSources() {
return; return;
} }
if ( if (
!selectedSourceId.value || selectedSourceId.value === null ||
!sources.value.some((item) => item.id === selectedSourceId.value) !sources.value.some((item) => item.id === selectedSourceId.value)
) { ) {
selectedSourceId.value = sources.value[0].id; selectedSourceId.value = sources.value[0].id;
@@ -54,7 +56,7 @@ export function useDatacenterSources() {
} }
function markSourceRuntimeUnavailable(sourceId?: null | number | string) { function markSourceRuntimeUnavailable(sourceId?: null | number | string) {
if (!sourceId) return; if (sourceId === null || sourceId === undefined) return;
sources.value = sources.value.map((item) => sources.value = sources.value.map((item) =>
String(item.id) === String(sourceId) String(item.id) === String(sourceId)
? { ...item, runtimeUnavailable: true } ? { ...item, runtimeUnavailable: true }
@@ -63,7 +65,7 @@ export function useDatacenterSources() {
} }
function clearSourceRuntimeUnavailable(sourceId?: null | number | string) { function clearSourceRuntimeUnavailable(sourceId?: null | number | string) {
if (!sourceId) return; if (sourceId === null || sourceId === undefined) return;
sources.value = sources.value.map((item) => sources.value = sources.value.map((item) =>
String(item.id) === String(sourceId) String(item.id) === String(sourceId)
? { ...item, runtimeUnavailable: false } ? { ...item, runtimeUnavailable: false }
@@ -71,7 +73,7 @@ export function useDatacenterSources() {
); );
} }
function removeSourceFromState(sourceId: number) { function removeSourceFromState(sourceId: DatacenterId) {
sources.value = sources.value.filter((item) => item.id !== sourceId); sources.value = sources.value.filter((item) => item.id !== sourceId);
syncSelectedSourceAfterMutation(); syncSelectedSourceAfterMutation();
} }
@@ -126,6 +128,26 @@ export function useDatacenterSources() {
} }
} }
async function importExcelSource(file: File, sourceName: string) {
saving.value = true;
try {
const formData = new FormData();
formData.append('file', file);
formData.append('sourceName', sourceName.trim());
const res = await api.postFile(
'/api/v1/datacenterExcel/import',
formData,
{
timeout: 10 * 60 * 1000,
},
);
selectedSourceId.value = res.data?.sourceId || null;
return res.data;
} finally {
saving.value = false;
}
}
async function testConnection(target: Record<string, any>) { async function testConnection(target: Record<string, any>) {
testing.value = true; testing.value = true;
try { try {
@@ -164,7 +186,7 @@ export function useDatacenterSources() {
} }
} }
async function removeSource(sourceId: number) { async function removeSource(sourceId: DatacenterId) {
await api.post('/api/v1/datacenterSource/remove', { sourceId }); await api.post('/api/v1/datacenterSource/remove', { sourceId });
removeSourceFromState(sourceId); removeSourceFromState(sourceId);
} }
@@ -176,6 +198,7 @@ export function useDatacenterSources() {
loading, loading,
saving, saving,
testing, testing,
importExcelSource,
loadSources, loadSources,
removeSource, removeSource,
removeSourceFromState, removeSourceFromState,

View File

@@ -1,5 +1,7 @@
import type { Ref } from 'vue'; import type { Ref } from 'vue';
import type { DatacenterId } from './datacenter-id';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
@@ -7,12 +9,12 @@ import { ElMessage } from 'element-plus';
import { requestClient } from '#/api/request'; import { requestClient } from '#/api/request';
interface DatacenterTableRuntimeOptions { interface DatacenterTableRuntimeOptions {
clearSourceRuntimeUnavailable?: (sourceId?: null | number | string) => void; clearSourceRuntimeUnavailable?: (sourceId?: DatacenterId | null) => void;
markSourceRuntimeUnavailable?: (sourceId?: null | number | string) => void; markSourceRuntimeUnavailable?: (sourceId?: DatacenterId | null) => void;
} }
export function useDatacenterTables( export function useDatacenterTables(
selectedSourceId: Ref<null | number>, selectedSourceId: Ref<DatacenterId | null>,
options: DatacenterTableRuntimeOptions = {}, options: DatacenterTableRuntimeOptions = {},
) { ) {
const SOURCE_MISSING_MESSAGE = '连接不存在'; const SOURCE_MISSING_MESSAGE = '连接不存在';
@@ -24,8 +26,8 @@ export function useDatacenterTables(
const previewRows = ref<any[]>([]); const previewRows = ref<any[]>([]);
const jobs = ref<any[]>([]); const jobs = ref<any[]>([]);
const sourceUnavailable = ref(false); const sourceUnavailable = ref(false);
const selectedCatalogId = ref<null | number>(null); const selectedCatalogId = ref<DatacenterId | null>(null);
const selectedTableId = ref<null | number>(null); const selectedTableId = ref<DatacenterId | null>(null);
const previewLoading = ref(false); const previewLoading = ref(false);
const selectedCatalog = computed( const selectedCatalog = computed(
@@ -66,7 +68,7 @@ export function useDatacenterTables(
} }
async function loadCatalogs() { async function loadCatalogs() {
if (!selectedSourceId.value) { if (selectedSourceId.value === null) {
catalogs.value = []; catalogs.value = [];
selectedCatalogId.value = null; selectedCatalogId.value = null;
sourceUnavailable.value = false; sourceUnavailable.value = false;
@@ -87,7 +89,7 @@ export function useDatacenterTables(
return true; return true;
} }
if ( if (
!selectedCatalogId.value || selectedCatalogId.value === null ||
!catalogs.value.some((item) => item.id === selectedCatalogId.value) !catalogs.value.some((item) => item.id === selectedCatalogId.value)
) { ) {
selectedCatalogId.value = catalogs.value[0].id; selectedCatalogId.value = catalogs.value[0].id;
@@ -107,7 +109,7 @@ export function useDatacenterTables(
} }
async function loadSourceTables() { async function loadSourceTables() {
if (!selectedSourceId.value) { if (selectedSourceId.value === null) {
sourceTables.value = []; sourceTables.value = [];
return; return;
} }
@@ -130,7 +132,7 @@ export function useDatacenterTables(
} }
async function loadManagedTables() { async function loadManagedTables() {
if (!selectedSourceId.value) { if (selectedSourceId.value === null) {
managedTables.value = []; managedTables.value = [];
selectedTableId.value = null; selectedTableId.value = null;
return; return;
@@ -158,7 +160,7 @@ export function useDatacenterTables(
return; return;
} }
if ( if (
!selectedTableId.value || selectedTableId.value === null ||
!managedTables.value.some((item) => item.id === selectedTableId.value) !managedTables.value.some((item) => item.id === selectedTableId.value)
) { ) {
selectedTableId.value = managedTables.value[0].id; selectedTableId.value = managedTables.value[0].id;
@@ -166,7 +168,7 @@ export function useDatacenterTables(
} }
async function loadTableRuntime() { async function loadTableRuntime() {
if (!selectedTableId.value) { if (selectedTableId.value === null) {
schema.value = null; schema.value = null;
previewRows.value = []; previewRows.value = [];
jobs.value = []; jobs.value = [];
@@ -279,7 +281,10 @@ export function useDatacenterTables(
tableIds, tableIds,
}); });
ElMessage.success(`已去除 ${tableIds.length} 张表`); ElMessage.success(`已去除 ${tableIds.length} 张表`);
if (selectedTableId.value && tableIds.includes(selectedTableId.value)) { if (
selectedTableId.value !== null &&
tableIds.includes(selectedTableId.value)
) {
selectedTableId.value = null; selectedTableId.value = null;
schema.value = null; schema.value = null;
previewRows.value = []; previewRows.value = [];

View File

@@ -16,7 +16,7 @@ export function useSourceForm() {
id: undefined, id: undefined,
sourceName: '', sourceName: '',
sourceCode: '', sourceCode: '',
sourceType: 'MYSQL', sourceType: 'EXCEL',
accessMode: 'READ_ONLY', accessMode: 'READ_ONLY',
driverClassName: '', driverClassName: '',
jdbcUrl: '', jdbcUrl: '',
@@ -33,6 +33,11 @@ export function useSourceForm() {
const rules: FormRules = { const rules: FormRules = {
sourceName: [ sourceName: [
{ required: true, message: '请输入连接名称', trigger: 'blur' }, { required: true, message: '请输入连接名称', trigger: 'blur' },
{
max: 100,
message: '连接名称不能超过 100 个字符',
trigger: ['blur', 'change'],
},
], ],
sourceType: [ sourceType: [
{ required: true, message: '请选择连接类型', trigger: 'change' }, { required: true, message: '请选择连接类型', trigger: 'change' },
@@ -138,7 +143,7 @@ export function useSourceForm() {
form.id = row?.id; form.id = row?.id;
form.sourceName = row?.sourceName || ''; form.sourceName = row?.sourceName || '';
form.sourceCode = row?.sourceCode || ''; form.sourceCode = row?.sourceCode || '';
form.sourceType = row?.sourceType || 'MYSQL'; form.sourceType = row?.sourceType || 'EXCEL';
form.accessMode = row?.accessMode || 'READ_ONLY'; form.accessMode = row?.accessMode || 'READ_ONLY';
form.driverClassName = row?.driverClassName || ''; form.driverClassName = row?.driverClassName || '';
form.jdbcUrl = row?.jdbcUrl || ''; form.jdbcUrl = row?.jdbcUrl || '';

View File

@@ -15,6 +15,7 @@ interface Props {
centered?: boolean; centered?: boolean;
closable?: boolean; closable?: boolean;
closeOnClickModal?: boolean; closeOnClickModal?: boolean;
confirmDisabled?: boolean;
confirmLoading?: boolean; confirmLoading?: boolean;
confirmText?: string; confirmText?: string;
description?: string; description?: string;
@@ -40,6 +41,7 @@ const props = withDefaults(defineProps<Props>(), {
centered: false, centered: false,
closable: true, closable: true,
closeOnClickModal: false, closeOnClickModal: false,
confirmDisabled: false,
confirmLoading: false, confirmLoading: false,
confirmText: '', confirmText: '',
description: '', description: '',
@@ -124,6 +126,7 @@ function handleConfirm() {
:centered="centered" :centered="centered"
:closable="closable" :closable="closable"
:close-on-click-modal="closeOnClickModal" :close-on-click-modal="closeOnClickModal"
:confirm-disabled="confirmDisabled || submitting"
:confirm-loading="confirmLoading" :confirm-loading="confirmLoading"
content-class="p-0" content-class="p-0"
:description="description" :description="description"
@@ -164,7 +167,7 @@ function handleConfirm() {
</EasyFlowButton> </EasyFlowButton>
<EasyFlowButton <EasyFlowButton
v-if="showConfirmButton" v-if="showConfirmButton"
:disabled="submitting" :disabled="confirmDisabled || submitting"
:loading="confirmLoading || submitting" :loading="confirmLoading || submitting"
@click="handleConfirm" @click="handleConfirm"
> >