From 2c681a414270679cbc1d16262fe17cb3aef8c027 Mon Sep 17 00:00:00 2001 From: thzxx Date: Wed, 27 May 2026 20:56:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=87=E7=AD=BE=E6=A0=8F=E5=8F=B3?= =?UTF-8?q?=E9=94=AE=E8=8F=9C=E5=8D=95=20=E2=80=94=20=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E5=85=B6=E4=BB=96/=E5=8F=B3=E4=BE=A7/=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 右键点击标签弹出上下文菜单 - 关闭:关闭当前标签(带未保存确认) - 关闭其他标签:保留当前,关闭其余(带未保存确认) - 关闭右侧标签:关闭当前标签右边的所有标签 - 关闭全部标签:关闭所有标签(带未保存确认) - 点击空白处自动关闭菜单 技术实现: - tabStore 新增 closeOtherTabs/closeAllTabs/closeTabsToRight 方法 - TabBar 组件新增 ContextMenu 状态管理和右键事件处理 - CSS 新增 .tab-context-menu 样式(暗色主题兼容) --- src/renderer/components/TabBar/TabBar.tsx | 108 +++++++++++++++++++++- src/renderer/stores/tabStore.ts | 35 +++++++ src/renderer/styles/global.css | 33 +++++++ 3 files changed, 175 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/TabBar/TabBar.tsx b/src/renderer/components/TabBar/TabBar.tsx index c747f18..75b3765 100644 --- a/src/renderer/components/TabBar/TabBar.tsx +++ b/src/renderer/components/TabBar/TabBar.tsx @@ -1,14 +1,28 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useState, useEffect, useRef } from 'react' import { useTabStore } from '../../stores/tabStore' import { getFileName } from '../../lib/fileUtils' +interface ContextMenuState { + visible: boolean + x: number + y: number + tabId: string +} + export function TabBar() { const tabs = useTabStore(s => s.tabs) const activeTabId = useTabStore(s => s.activeTabId) const switchToTab = useTabStore(s => s.switchToTab) const closeTab = useTabStore(s => s.closeTab) const createTab = useTabStore(s => s.createTab) + const closeOtherTabs = useTabStore(s => s.closeOtherTabs) + const closeAllTabs = useTabStore(s => s.closeAllTabs) + const closeTabsToRight = useTabStore(s => s.closeTabsToRight) + const [menu, setMenu] = useState({ visible: false, x: 0, y: 0, tabId: '' }) + const menuRef = useRef(null) + + // 关闭标签(带未保存确认) const handleClose = useCallback((e: React.MouseEvent, tabId: string) => { e.stopPropagation() const tab = tabs.find(t => t.id === tabId) @@ -19,6 +33,71 @@ export function TabBar() { closeTab(tabId) }, [tabs, closeTab]) + // 右键菜单 + const handleContextMenu = useCallback((e: React.MouseEvent, tabId: string) => { + e.preventDefault() + e.stopPropagation() + setMenu({ visible: true, x: e.clientX, y: e.clientY, tabId }) + }, []) + + // 点击空白处关闭菜单 + useEffect(() => { + if (!menu.visible) return + const handleClick = () => setMenu(prev => ({ ...prev, visible: false })) + document.addEventListener('click', handleClick) + return () => document.removeEventListener('click', handleClick) + }, [menu.visible]) + + // 菜单操作 + const handleMenuClose = useCallback(() => { + const tab = tabs.find(t => t.id === menu.tabId) + if (tab?.isModified) { + const name = tab.filePath ? getFileName(tab.filePath) : '未命名' + if (!confirm(`"${name}" 尚未保存,确定要关闭吗?`)) return + } + closeTab(menu.tabId) + setMenu(prev => ({ ...prev, visible: false })) + }, [tabs, menu.tabId, closeTab]) + + const handleMenuCloseOthers = useCallback(() => { + // 检查其他标签是否有未保存 + const otherModified = tabs.filter(t => t.id !== menu.tabId && t.isModified) + if (otherModified.length > 0) { + const names = otherModified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、') + if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return + } + closeOtherTabs(menu.tabId) + setMenu(prev => ({ ...prev, visible: false })) + }, [tabs, menu.tabId, closeOtherTabs]) + + const handleMenuCloseAll = useCallback(() => { + const modified = tabs.filter(t => t.isModified) + if (modified.length > 0) { + const names = modified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、') + if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return + } + closeAllTabs() + setMenu(prev => ({ ...prev, visible: false })) + }, [tabs, closeAllTabs]) + + const handleMenuCloseRight = useCallback(() => { + const index = tabs.findIndex(t => t.id === menu.tabId) + const rightTabs = tabs.slice(index + 1) + const modified = rightTabs.filter(t => t.isModified) + if (modified.length > 0) { + const names = modified.map(t => t.filePath ? getFileName(t.filePath) : '未命名').join('、') + if (!confirm(`以下文件尚未保存:${names},确定要关闭吗?`)) return + } + closeTabsToRight(menu.tabId) + setMenu(prev => ({ ...prev, visible: false })) + }, [tabs, menu.tabId, closeTabsToRight]) + + // 判断右侧是否还有标签 + const hasRightTabs = menu.visible && (() => { + const index = tabs.findIndex(t => t.id === menu.tabId) + return index < tabs.length - 1 + })() + if (tabs.length === 0) return null return ( @@ -29,6 +108,7 @@ export function TabBar() { key={tab.id} className={`tab-item ${tab.id === activeTabId ? 'active' : ''} ${tab.isModified ? 'modified' : ''}`} onClick={() => switchToTab(tab.id)} + onContextMenu={(e) => handleContextMenu(e, tab.id)} > {tab.filePath ? getFileName(tab.filePath) : '未命名'} @@ -55,6 +135,32 @@ export function TabBar() { + + {/* 右键菜单 */} + {menu.visible && ( +
e.stopPropagation()} + > +
+ 关闭 +
+
+ 关闭其他标签 +
+ {hasRightTabs && ( +
+ 关闭右侧标签 +
+ )} +
+
+ 关闭全部标签 +
+
+ )}
) } diff --git a/src/renderer/stores/tabStore.ts b/src/renderer/stores/tabStore.ts index 473eb84..8db67d3 100644 --- a/src/renderer/stores/tabStore.ts +++ b/src/renderer/stores/tabStore.ts @@ -9,6 +9,9 @@ interface TabState { createTab: (filePath?: string | null, content?: string) => Tab closeTab: (tabId: string) => void + closeOtherTabs: (tabId: string) => void + closeAllTabs: () => void + closeTabsToRight: (tabId: string) => void switchToTab: (tabId: string) => void updateTabContent: (tabId: string, content: string) => void setModified: (tabId: string, modified: boolean) => void @@ -79,6 +82,38 @@ export const useTabStore = create((set, get) => ({ }) }, + closeOtherTabs: (tabId) => { + set(state => { + const target = state.tabs.find(t => t.id === tabId) + if (!target) return state + return { + tabs: [target], + activeTabId: tabId, + mruStack: [] + } + }) + }, + + closeAllTabs: () => { + set({ tabs: [], activeTabId: null, mruStack: [] }) + }, + + closeTabsToRight: (tabId) => { + set(state => { + const index = state.tabs.findIndex(t => t.id === tabId) + if (index === -1) return state + const newTabs = state.tabs.slice(0, index + 1) + const newActiveId = state.activeTabId && newTabs.find(t => t.id === state.activeTabId) + ? state.activeTabId + : tabId + return { + tabs: newTabs, + activeTabId: newActiveId, + mruStack: state.mruStack.filter(id => newTabs.some(t => t.id === id)) + } + }) + }, + switchToTab: (tabId) => { set(state => { if (state.activeTabId === tabId) return state diff --git a/src/renderer/styles/global.css b/src/renderer/styles/global.css index 56dca68..0e23a6c 100644 --- a/src/renderer/styles/global.css +++ b/src/renderer/styles/global.css @@ -300,6 +300,39 @@ html, body { color: var(--text); } +/* Tab Context Menu */ +.tab-context-menu { + position: fixed; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + padding: 4px 0; + min-width: 160px; + z-index: 9999; + font-size: 13px; + font-family: var(--font-ui); +} + +.tab-context-item { + padding: 6px 16px; + cursor: pointer; + color: var(--text); + white-space: nowrap; + transition: background 0.1s ease; +} + +.tab-context-item:hover { + background: var(--primary-light); + color: var(--primary); +} + +.tab-context-divider { + height: 1px; + background: var(--border); + margin: 4px 0; +} + /* Modified Banner */ #modified-banner { display: flex;