From b8a767d70465dbd24ad4196f17932c4ce086b661 Mon Sep 17 00:00:00 2001 From: hooke Date: Wed, 3 Dec 2025 07:25:46 -0700 Subject: [PATCH] feat(projects): support pinning and unpinning of tabs --- .../modules/global-tab/context-menu.vue | 25 ++++++++++- src/locales/langs/en-us.ts | 4 +- src/locales/langs/zh-cn.ts | 4 +- src/store/modules/tab/index.ts | 45 +++++++++++++++++++ src/store/modules/tab/shared.ts | 12 +++++ src/typings/app.d.ts | 2 +- 6 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/layouts/modules/global-tab/context-menu.vue b/src/layouts/modules/global-tab/context-menu.vue index 18d57f98..3997bae6 100644 --- a/src/layouts/modules/global-tab/context-menu.vue +++ b/src/layouts/modules/global-tab/context-menu.vue @@ -26,7 +26,7 @@ const props = withDefaults(defineProps(), { const visible = defineModel('visible'); -const { removeTab, clearTabs, clearLeftTabs, clearRightTabs } = useTabStore(); +const { removeTab, clearTabs, clearLeftTabs, clearRightTabs, fixTab, unfixTab, isTabRetain } = useTabStore(); const { SvgIconVNode } = useSvgIcon(); type DropdownOption = { @@ -64,6 +64,23 @@ const options = computed(() => { icon: SvgIconVNode({ icon: 'ant-design:line-outlined', fontSize: 18 }) } ]; + + if (props.tabId !== '/home') { + if (isTabRetain(props.tabId)) { + opts.push({ + key: 'unpin', + label: $t('dropdown.unpin'), + icon: SvgIconVNode({ icon: 'mdi:pin-off-outline', fontSize: 18 }) + }); + } else { + opts.push({ + key: 'pin', + label: $t('dropdown.pin'), + icon: SvgIconVNode({ icon: 'mdi:pin-outline', fontSize: 18 }) + }); + } + } + const { excludeKeys, disabledKeys } = props; const result = opts.filter(opt => !excludeKeys.includes(opt.key)); @@ -98,6 +115,12 @@ const dropdownAction: Record void> = { }, closeAll() { clearTabs(); + }, + pin() { + fixTab(props.tabId); + }, + unpin() { + unfixTab(props.tabId); } }; diff --git a/src/locales/langs/en-us.ts b/src/locales/langs/en-us.ts index e84906b2..a7b383fd 100644 --- a/src/locales/langs/en-us.ts +++ b/src/locales/langs/en-us.ts @@ -336,7 +336,9 @@ const local: App.I18n.Schema = { closeOther: 'Close Other', closeLeft: 'Close Left', closeRight: 'Close Right', - closeAll: 'Close All' + closeAll: 'Close All', + pin: 'Pin Tab', + unpin: 'Unpin Tab' }, icon: { themeConfig: 'Theme Configuration', diff --git a/src/locales/langs/zh-cn.ts b/src/locales/langs/zh-cn.ts index 11a5817c..7de2ee34 100644 --- a/src/locales/langs/zh-cn.ts +++ b/src/locales/langs/zh-cn.ts @@ -333,7 +333,9 @@ const local: App.I18n.Schema = { closeOther: '关闭其它', closeLeft: '关闭左侧', closeRight: '关闭右侧', - closeAll: '关闭所有' + closeAll: '关闭所有', + pin: '固定标签', + unpin: '取消固定' }, icon: { themeConfig: '主题配置', diff --git a/src/store/modules/tab/index.ts b/src/store/modules/tab/index.ts index 84d96902..ee47f454 100644 --- a/src/store/modules/tab/index.ts +++ b/src/store/modules/tab/index.ts @@ -18,6 +18,7 @@ import { getTabByRoute, getTabIdByRoute, isTabInTabs, + reorderFixedTabs, updateTabByI18nKey, updateTabsByI18nKey } from './shared'; @@ -248,6 +249,48 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => { await clearTabs(excludes); } + /** + * Fix tab + * + * @param tabId + */ + function fixTab(tabId: string) { + const tabIndex = tabs.value.findIndex(t => t.id === tabId); + if (tabIndex === -1) return; + + const tab = tabs.value[tabIndex]; + const fixedCount = getFixedTabIds(tabs.value).length; + tab.fixedIndex = fixedCount; + + if (tabIndex !== fixedCount) { + tabs.value.splice(tabIndex, 1); + tabs.value.splice(fixedCount, 0, tab); + } + + reorderFixedTabs(tabs.value); + } + + /** + * Unfix tab + * + * @param tabId + */ + function unfixTab(tabId: string) { + const tabIndex = tabs.value.findIndex(t => t.id === tabId); + if (tabIndex === -1) return; + + const tab = tabs.value[tabIndex]; + tab.fixedIndex = undefined; + + const fixedCount = getFixedTabIds(tabs.value).length; + if (tabIndex !== fixedCount) { + tabs.value.splice(tabIndex, 1); + tabs.value.splice(fixedCount, 0, tab); + } + + reorderFixedTabs(tabs.value); + } + /** * Set new label of tab * @@ -328,6 +371,8 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => { clearTabs, clearLeftTabs, clearRightTabs, + fixTab, + unfixTab, switchRouteByTab, setTabLabel, resetTabLabel, diff --git a/src/store/modules/tab/shared.ts b/src/store/modules/tab/shared.ts index 5a287002..48880a3a 100644 --- a/src/store/modules/tab/shared.ts +++ b/src/store/modules/tab/shared.ts @@ -198,6 +198,18 @@ export function getFixedTabIds(tabs: App.Global.Tab[]) { return fixedTabs.map(tab => tab.id); } +/** + * Reorder fixed tabs fixedIndex + * + * @param tabs + */ +export function reorderFixedTabs(tabs: App.Global.Tab[]) { + const fixedTabs = getFixedTabs(tabs); + fixedTabs.forEach((t, i) => { + t.fixedIndex = i; + }); +} + /** * Update tabs label * diff --git a/src/typings/app.d.ts b/src/typings/app.d.ts index 215d6ac0..6778fc7b 100644 --- a/src/typings/app.d.ts +++ b/src/typings/app.d.ts @@ -282,7 +282,7 @@ declare namespace App { type FormRule = import('naive-ui').FormItemRule; /** The global dropdown key */ - type DropdownKey = 'closeCurrent' | 'closeOther' | 'closeLeft' | 'closeRight' | 'closeAll'; + type DropdownKey = 'closeCurrent' | 'closeOther' | 'closeLeft' | 'closeRight' | 'closeAll' | 'pin' | 'unpin'; } /**