From 8cfac3599069aaa839c99eff927d093619ecf05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E6=A1=A5=E5=87=89=E4=BB=8B?= <113153113+HighBigSky@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:24:44 +0800 Subject: [PATCH] feat(transfer): batch-select transfer objects from a pasted name list --- .../transfer/DataTransferDialog.vue | 6 +- .../transfer/ObjectSelectionTree.vue | 97 ++++++++++- .../__tests__/ObjectSelectionTree.spec.ts | 148 +++++++++++++++- .../__tests__/transferSelections.spec.ts | 76 ++++++++- .../components/transfer/transferSelections.ts | 158 ++++++++++++++++++ .../transferBulkSelectParity.spec.ts | 47 ++++++ apps/desktop/src/i18n/locales/az.ts | 8 + apps/desktop/src/i18n/locales/en.ts | 8 + apps/desktop/src/i18n/locales/es.ts | 8 + apps/desktop/src/i18n/locales/it.ts | 8 + apps/desktop/src/i18n/locales/ja.ts | 8 + apps/desktop/src/i18n/locales/ko.ts | 8 + apps/desktop/src/i18n/locales/pt-BR.ts | 8 + apps/desktop/src/i18n/locales/tr.ts | 8 + apps/desktop/src/i18n/locales/zh-CN.ts | 8 + apps/desktop/src/i18n/locales/zh-TW.ts | 8 + 16 files changed, 605 insertions(+), 7 deletions(-) create mode 100644 apps/desktop/src/i18n/__tests__/transferBulkSelectParity.spec.ts diff --git a/apps/desktop/src/components/transfer/DataTransferDialog.vue b/apps/desktop/src/components/transfer/DataTransferDialog.vue index 2ad02ddc27..80739a6625 100644 --- a/apps/desktop/src/components/transfer/DataTransferDialog.vue +++ b/apps/desktop/src/components/transfer/DataTransferDialog.vue @@ -142,6 +142,10 @@ const showCrossFamilyViewHint = computed(() => { if (!allowed.includes("VIEW")) return false; return !isSameTransferFamily(transferDatabaseTypeForConnection(sourceConfig), transferDatabaseTypeForConnection(targetConfig)) && (selectedObjects.value.VIEW?.size ?? 0) > 0; }); +// 「批量录入」允许的 schema.table / db.table 前缀:取当前源已选的 catalog / 库 / schema, +// 便于用户从其他工具粘贴带前缀的对象名;全为空时前端退化为只按对象名匹配。 +const objectQualifiers = computed(() => [sourceCatalog.value, sourceDatabase.value, sourceSchema.value].filter((value) => value.trim().length > 0)); + const pendingSourceSchemaPrefill = ref(""); const pendingSelectedTablesPrefill = ref(null); // Pending object selection for saved-task loading (covers all object kinds, @@ -1320,7 +1324,7 @@ async function saveConfigTask() {
{{ t("transfer.selectSourceFirst") }}
- +
{{ t("transfer.crossFamilyViewHint") }}
diff --git a/apps/desktop/src/components/transfer/ObjectSelectionTree.vue b/apps/desktop/src/components/transfer/ObjectSelectionTree.vue index e8899e369b..abc9bd6c97 100644 --- a/apps/desktop/src/components/transfer/ObjectSelectionTree.vue +++ b/apps/desktop/src/components/transfer/ObjectSelectionTree.vue @@ -4,7 +4,8 @@ import { useI18n } from "vue-i18n"; import { RecycleScroller } from "vue-virtual-scroller"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; import type { TransferObjectKind } from "@/lib/backend/api"; -import { Square, CheckSquare, MinusSquare, Search, ChevronRight } from "@lucide/vue"; +import { Square, CheckSquare, MinusSquare, Search, ChevronRight, X } from "@lucide/vue"; +import { matchBulkObjectNames } from "./transferSelections"; const { t } = useI18n(); @@ -29,6 +30,11 @@ const props = defineProps<{ modelValue: Record; search?: string; loading?: boolean; + /** + * 「批量录入」可接受的名称前缀(当前源的 catalog / 库 / schema)。 + * 用于识别粘贴内容里的 schema.table 前缀,留空时放宽为只按对象名匹配。 + */ + qualifiers?: string[]; }>(); const emit = defineEmits<{ @@ -215,6 +221,54 @@ function deselectAll() { emit("update:modelValue", next); } +// 「批量录入」面板状态:showBulkInput 控制面板展开,bulkText 存放粘贴内容, +// bulkFeedback 保存最近一次匹配结果,用于反馈「已勾选多少项 / 哪些名称未匹配」。 +const showBulkInput = ref(false); +const bulkText = ref(""); +const bulkFeedback = ref<{ matchedCount: number; unmatchedNames: string[] } | null>(null); + +function toggleBulkInput() { + showBulkInput.value = !showBulkInput.value; + if (showBulkInput.value) { + // 每次重新打开都清空上一次的输入与结果,避免误用旧文本 + bulkText.value = ""; + bulkFeedback.value = null; + } +} + +/** + * 执行批量勾选。匹配始终基于完整对象清单(不受当前搜索过滤影响), + * 结果与已有选择合并(只追加勾选,不清除手工勾选或被搜索隐藏的选项)。 + */ +function applyBulkSelection() { + const result = matchBulkObjectNames(bulkText.value, props.groups, props.disabledGroups, props.qualifiers ?? []); + if (result.matchedCount > 0) { + const next: Record = { ...props.modelValue }; + for (const [kind, names] of Object.entries(result.matched)) { + if (names.length === 0) continue; + next[kind] = [...new Set([...(next[kind] ?? []), ...names])]; + } + emit("update:modelValue", next); + revealMatchedGroups(result.matched); + } + bulkFeedback.value = { matchedCount: result.matchedCount, unmatchedNames: result.unmatchedNames }; +} + +// 批量勾选后展开命中的分组:大清单分组默认折叠,展开后用户才能看到勾选结果 +function revealMatchedGroups(matched: Record) { + const next = new Set(expanded.value); + let changed = false; + for (const [kind, names] of Object.entries(matched)) { + if (names.length === 0 || next.has(kind)) continue; + next.add(kind); + changed = true; + } + if (changed) expanded.value = next; +} + +// 未匹配名称拼成一行展示,并用 title 提供完整内容(列表可能被截断) +const bulkUnmatchedNames = computed(() => bulkFeedback.value?.unmatchedNames.join(", ") ?? ""); + // The bulk button flips between “select all” and “deselect all”. It shows // “deselect all” only when every visible, enabled item is already selected // (selecting more is then a no-op); otherwise “select all” is shown so the @@ -261,6 +315,9 @@ function groupSelectionState(kind: TransferObjectKind, items: string[]): "none" class="pl-8 pr-2.5 h-8 w-full rounded-md border border-input bg-transparent text-xs outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-ring transition-colors placeholder:text-muted-foreground/70" /> + @@ -269,6 +326,42 @@ function groupSelectionState(kind: TransferObjectKind, items: string[]): "none" + +
+
+ {{ t("transfer.bulkSelectTitle") }} + +
+

{{ t("transfer.bulkSelectHint") }}

+