From c9203de1e7b39d5dfc1638e59f9bec93dbe73299 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 11 Jun 2026 01:37:58 +0800 Subject: [PATCH] feat(tabs): add duplicate tab feature in context menu --- .../src/components/layout/AppTabBar.vue | 8 ++- apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.ts | 1 + apps/desktop/src/i18n/locales/it.ts | 1 + apps/desktop/src/i18n/locales/pt-BR.ts | 1 + apps/desktop/src/i18n/locales/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + apps/desktop/src/stores/queryStore.ts | 64 +++++++++++++++++++ 8 files changed, 77 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue index 40ae9e51a9..e39d273da4 100644 --- a/apps/desktop/src/components/layout/AppTabBar.vue +++ b/apps/desktop/src/components/layout/AppTabBar.vue @@ -2,7 +2,7 @@ import { computed, ref, watch, nextTick } from "vue"; import type { CSSProperties } from "vue"; import { useI18n } from "vue-i18n"; -import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Check, Lock } from "@lucide/vue"; +import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Check, Lock, Copy } from "@lucide/vue"; import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import LightDropdown from "@/components/ui/LightDropdown.vue"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; @@ -83,6 +83,12 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] { icon: Pencil, visible: canRenameTab(tab), }, + { + label: t("contextMenu.duplicateTab"), + action: () => queryStore.duplicateTab(tab.id), + icon: Copy, + visible: canRenameTab(tab), + }, { label: "", separator: true }, { label: tab.pinned ? t("contextMenu.unpin") : t("contextMenu.pin"), diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index cd6c93c26b..319edc354f 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1010,6 +1010,7 @@ unpin: "Unpin", refreshTab: "Refresh Data", renameTab: "Rename Tab", + duplicateTab: "Duplicate Tab", compactTabTitle: "Shorten Tab Titles", closeTab: "Close Tab", closeOtherTabs: "Close Other Tabs", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 32349523d0..ff2d85d4c6 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -873,6 +873,7 @@ unpin: "Desfijar", refreshTab: "Actualizar datos", renameTab: "Renombrar pestaña", + duplicateTab: "Duplicar pestaña", closeTab: "Cerrar pestaña", closeOtherTabs: "Cerrar otras pestañas", closeAllTabs: "Cerrar todas las pestañas", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index f901f40e4e..7088a8194a 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -974,6 +974,7 @@ unpin: "Sblocca", refreshTab: "Aggiorna Dati", renameTab: "Rinomina Scheda", + duplicateTab: "Duplica Scheda", compactTabTitle: "Accorcia Titoli Schede", closeTab: "Chiudi Scheda", closeOtherTabs: "Chiudi Altre Schede", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 29d7c705be..20387fcb05 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -974,6 +974,7 @@ unpin: "Desafixar", refreshTab: "Atualizar Dados", renameTab: "Renomear Aba", + duplicateTab: "Duplicar Aba", compactTabTitle: "Encurtar Títulos das Abas", closeTab: "Fechar Aba", closeOtherTabs: "Fechar Outras Abas", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index ac995bbed0..a18cddd983 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1009,6 +1009,7 @@ unpin: "取消置顶", refreshTab: "刷新数据", renameTab: "重命名标签页", + duplicateTab: "复制标签页", compactTabTitle: "缩短标签标题", closeTab: "关闭标签页", closeOtherTabs: "关闭其他标签页", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 2250048b9b..99c3b64d98 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -953,6 +953,7 @@ unpin: "取消置頂", refreshTab: "重新整理資料", renameTab: "重新命名分頁", + duplicateTab: "複製分頁", compactTabTitle: "縮短標籤標題", closeTab: "關閉分頁", closeOtherTabs: "關閉其他分頁", diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 1e5ad97e4d..60bf9b887c 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -397,6 +397,69 @@ export const useQueryStore = defineStore("query", () => { activeTabId.value = next.activeTabId; } + function duplicateTab(id: string) { + const idx = tabs.value.findIndex((t) => t.id === id); + if (idx < 0) return; + const original = tabs.value[idx]; + const newId = uuid(); + const newTab: QueryTab = { + id: newId, + title: original.title, + customTitle: original.customTitle, + connectionId: original.connectionId, + database: original.database, + schema: original.schema, + sql: original.sql, + savedSqlId: original.savedSqlId, + lastExecutedSql: undefined, + resultBaseSql: original.resultBaseSql, + resultSortedSql: undefined, + resultSortColumn: undefined, + resultSortColumnIndex: undefined, + resultSortDirection: undefined, + orderByInput: undefined, + resultPageSql: undefined, + resultPageLimit: undefined, + resultPageOffset: undefined, + resultCountSql: undefined, + resultTotalRowCount: undefined, + resultTotalRowCountLoading: undefined, + resultSessionId: undefined, + resultAccessedAt: undefined, + resultCacheKey: undefined, + resultCacheState: undefined, + pinned: false, + result: undefined, + results: undefined, + activeResultIndex: undefined, + explainPlan: undefined, + explainError: undefined, + explainSql: undefined, + lastExplainedSql: undefined, + isExecuting: false, + isCancelling: false, + queryExecutionStartedAt: undefined, + editorViewport: undefined, + editorSelection: undefined, + executionId: undefined, + isExplaining: false, + explainExecutionId: undefined, + mode: original.mode, + structureTableName: original.structureTableName, + objectBrowser: original.objectBrowser ? { ...original.objectBrowser } : undefined, + objectSource: original.objectSource ? { ...original.objectSource } : undefined, + tableMeta: original.tableMeta ? { ...original.tableMeta, columns: [...original.tableMeta.columns], primaryKeys: [...original.tableMeta.primaryKeys] } : undefined, + queryAnalysis: original.queryAnalysis ? { ...original.queryAnalysis, columns: original.queryAnalysis.columns.map((c) => ({ ...c })) } : undefined, + querySourceColumns: original.querySourceColumns ? [...original.querySourceColumns] : undefined, + queryEditabilityReason: original.queryEditabilityReason, + resultEvicted: undefined, + whereInput: original.whereInput, + previewSql: original.previewSql, + }; + tabs.value.splice(idx + 1, 0, newTab); + activeTabId.value = newId; + } + function closeTabsWhere(predicate: (tab: QueryTab) => boolean) { const closingIds = new Set(tabs.value.filter((tab) => predicate(tab)).map((tab) => tab.id)); if (closingIds.size === 0) return; @@ -1535,6 +1598,7 @@ export const useQueryStore = defineStore("query", () => { closeTab, closeOtherTabs, closeAllTabs, + duplicateTab, closeConnectionTabs, closeDatabaseTabs, releaseConnectionTabs,