diff --git a/apps/desktop/src/components/transfer/DataTransferDialog.vue b/apps/desktop/src/components/transfer/DataTransferDialog.vue
index e38534cb4a..699cee4a6b 100644
--- a/apps/desktop/src/components/transfer/DataTransferDialog.vue
+++ b/apps/desktop/src/components/transfer/DataTransferDialog.vue
@@ -651,6 +651,88 @@ function resetState(cancelTaskLoad = true) {
savedConfigSnapshot.value = "";
}
+/**
+ * 交换源和目标两侧:连接、Catalog、数据库、Schema 的选择随各自一侧整体互换,
+ * 一次点击即可反转传输方向。对象树只属于源端,旧选择作废,按新源端重新加载。
+ */
+function swapSourceAndTarget() {
+ const sourceState = {
+ connectionId: sourceConnectionId.value,
+ catalog: sourceCatalog.value,
+ catalogs: sourceCatalogs.value,
+ database: sourceDatabase.value,
+ databases: sourceDatabases.value,
+ schema: sourceSchema.value,
+ schemas: sourceSchemas.value,
+ };
+ const targetState = {
+ connectionId: targetConnectionId.value,
+ catalog: targetCatalog.value,
+ catalogs: targetCatalogs.value,
+ database: targetDatabase.value,
+ databases: targetDatabases.value,
+ schema: targetSchema.value,
+ schemas: targetSchemas.value,
+ };
+
+ if (sourceState.connectionId === targetState.connectionId && sourceState.catalog === targetState.catalog && sourceState.database === targetState.database && sourceState.schema === targetState.schema) {
+ // 两侧选择完全相同(含都未选择)时交换没有意义
+ return;
+ }
+
+ // 若正在加载已保存任务,先取消,避免任务加载链覆盖交换后的状态
+ taskLoadTracker.cancel();
+ pendingSourceSchemaPrefill.value = "";
+ pendingTargetSchemaPrefill.value = "";
+ pendingSelectedTablesPrefill.value = null;
+ pendingSelectedObjectsPrefill.value = null;
+
+ // 只在值确实变化时设置一次性跳过标记:标记由对应 watcher 消费,
+ // 若值未变则 watcher 不会触发,残留标记会误吞后续一次真实变更。
+ if (sourceConnectionId.value !== targetState.connectionId) {
+ skipSourceWatch.value = true;
+ sourceConnectionId.value = targetState.connectionId;
+ }
+ if (sourceCatalog.value !== targetState.catalog) {
+ skipSourceCatalogWatch.value = true;
+ sourceCatalog.value = targetState.catalog;
+ }
+ sourceCatalogs.value = targetState.catalogs;
+ if (sourceDatabase.value !== targetState.database) {
+ skipSourceDatabaseWatch.value = true;
+ sourceDatabase.value = targetState.database;
+ }
+ sourceDatabases.value = targetState.databases;
+ if (sourceSchema.value !== targetState.schema) {
+ skipSourceSchemaWatch.value = true;
+ sourceSchema.value = targetState.schema;
+ }
+ sourceSchemas.value = targetState.schemas;
+
+ if (targetConnectionId.value !== sourceState.connectionId) {
+ skipTargetWatch.value = true;
+ targetConnectionId.value = sourceState.connectionId;
+ }
+ if (targetCatalog.value !== sourceState.catalog) {
+ skipTargetCatalogWatch.value = true;
+ targetCatalog.value = sourceState.catalog;
+ }
+ targetCatalogs.value = sourceState.catalogs;
+ if (targetDatabase.value !== sourceState.database) {
+ skipTargetDatabaseWatch.value = true;
+ targetDatabase.value = sourceState.database;
+ }
+ targetDatabases.value = sourceState.databases;
+ targetSchema.value = sourceState.schema;
+ targetSchemas.value = sourceState.schemas;
+
+ // 对象树始终跟随源端:新源端是旧目标端,旧选择已无意义,清空后重新加载
+ objectGroups.value = {};
+ selectedObjects.value = {};
+ objectSearch.value = "";
+ void loadObjects();
+}
+
async function startTransfer() {
if (!canStart.value || isSubmitting.value) return;
isSubmitting.value = true;
@@ -1037,9 +1119,11 @@ async function saveConfigTask() {
-
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 1ad132a863..e21e32e99e 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -5219,6 +5219,7 @@ export default {
searchCatalog: "Search catalog...",
sourceDatabase: "Source Database",
sourceSchema: "Source Schema",
+ swap: "Swap source and target",
targetConnection: "Target Connection",
targetDatabase: "Target Database",
targetSchema: "Target Schema",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 25e3bae76b..680e1c9145 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -5014,6 +5014,7 @@ export default withEnglishFallback({
overallProgress: "Progreso general",
dataTransfer: "Transferencia de datos",
sourceSchema: "Esquema de origen",
+ swap: "Intercambiar origen y destino",
targetSchema: "Esquema de destino",
runInBackground: "Ejecutar en segundo plano",
newTask: "Nueva transferencia",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index f428a5f696..f2a0951e9d 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -4977,6 +4977,7 @@ export default withEnglishFallback({
sourceConnection: "Connessione Sorgente",
sourceDatabase: "Database Sorgente",
sourceSchema: "Schema Sorgente",
+ swap: "Inverti sorgente e destinazione",
targetConnection: "Connessione Destinazione",
targetDatabase: "Database Destinazione",
targetSchema: "Schema Destinazione",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index ba0e0e7b98..5fe62eb138 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -5006,6 +5006,7 @@ export default withEnglishFallback({
sourceConnection: "ソース接続",
sourceDatabase: "ソースデータベース",
sourceSchema: "ソーススキーマ",
+ swap: "ソースとターゲットを入れ替え",
targetConnection: "ターゲット接続",
targetDatabase: "ターゲットデータベース",
targetSchema: "ターゲットスキーマ",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index c4b6b19f59..913af444d5 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -4644,6 +4644,7 @@ export default withEnglishFallback({
sourceConnection: "소스 연결",
sourceDatabase: "소스 데이터베이스",
sourceSchema: "소스 스키마",
+ swap: "소스와 대상 교체",
targetConnection: "대상 연결",
targetDatabase: "대상 데이터베이스",
targetSchema: "대상 스키마",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index cc1f84cc2e..be5c825b5a 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -4979,6 +4979,7 @@ export default withEnglishFallback({
sourceConnection: "Conexão de origem",
sourceDatabase: "Banco de dados de origem",
sourceSchema: "Schema de origem",
+ swap: "Trocar origem e destino",
targetConnection: "Conexão de destino",
targetDatabase: "Banco de dados de destino",
targetSchema: "Schema de destino",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index d8014a3089..7bb71fcb14 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -5204,6 +5204,7 @@ export default withEnglishFallback({
searchCatalog: "搜索 Catalog...",
sourceDatabase: "源数据库",
sourceSchema: "源 Schema",
+ swap: "交换源和目标",
targetConnection: "目标连接",
targetDatabase: "目标数据库",
targetSchema: "目标 Schema",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index b457e0d825..5fd1dec30d 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -4303,6 +4303,7 @@ export default withEnglishFallback({
sourceConnection: "來源連線",
sourceDatabase: "來源資料庫",
sourceSchema: "來源 Schema",
+ swap: "交換來源與目標",
targetConnection: "目標連線",
targetDatabase: "目標資料庫",
targetSchema: "目標 Schema",