diff --git a/src/components/TabBar/ContextMenu.tsx b/src/components/TabBar/ContextMenu.tsx new file mode 100644 index 0000000..0b656c4 --- /dev/null +++ b/src/components/TabBar/ContextMenu.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Dropdown, Menu } from '@arco-design/web-react'; +import { + IconRefresh, + IconClose, + IconLeft, + IconRight, + IconCopy, +} from '@arco-design/web-react/icon'; +import { ContextMenuType } from '@/types/tabs'; + +interface ContextMenuProps { + /** 右键点击的标签 key */ + targetKey: string; + /** 是否可以关闭左侧 */ + canCloseLeft: boolean; + /** 是否可以关闭右侧 */ + canCloseRight: boolean; + /** 是否可以关闭其他 */ + canCloseOthers: boolean; + /** 菜单操作回调 */ + onMenuClick: (type: ContextMenuType, key: string) => void; + /** 子元素 */ + children: React.ReactNode; +} + +const ContextMenu: React.FC = ({ + targetKey, + canCloseLeft, + canCloseRight, + canCloseOthers, + onMenuClick, + children, +}) => { + const handleMenuClick = (key: string) => { + onMenuClick(key as ContextMenuType, targetKey); + }; + + const dropList = ( + + + + 重新加载 + + + + 关闭当前 + + + + 关闭左侧 + + + + 关闭右侧 + + + + 关闭其他 + + + ); + + return ( + + {children} + + ); +}; + +export default ContextMenu; diff --git a/src/components/TabBar/index.module.less b/src/components/TabBar/index.module.less new file mode 100644 index 0000000..b0cc586 --- /dev/null +++ b/src/components/TabBar/index.module.less @@ -0,0 +1,105 @@ +.tab-bar { + display: flex; + align-items: center; + height: 48px; + background-color: var(--color-bg-2); + border-bottom: 1px solid var(--color-border); + padding: 0 16px; + overflow-x: auto; + overflow-y: hidden; + + &::-webkit-scrollbar { + height: 4px; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--color-border-3); + border-radius: 2px; + + &:hover { + background-color: var(--color-border-4); + } + } + + .tab-list { + display: flex; + align-items: center; + gap: 8px; + height: 100%; + } + + .tab-item { + display: inline-flex; + align-items: center; + height: 32px; + padding: 0 16px; + background-color: var(--color-fill-2); + border-radius: 16px; + cursor: pointer; + user-select: none; + transition: all 0.3s cubic-bezier(0.34, 0.69, 0.1, 1); + white-space: nowrap; + font-size: 14px; + color: var(--color-text-2); + + &:hover { + background-color: var(--color-fill-3); + transform: translateY(-1px); + } + + &.tab-item-active { + background: linear-gradient( + 135deg, + rgb(var(--primary-6)) 0%, + rgb(var(--primary-5)) 100% + ); + color: #fff; + font-weight: 500; + box-shadow: 0 2px 8px rgba(var(--primary-6), 0.3); + + &:hover { + background: linear-gradient( + 135deg, + rgb(var(--primary-5)) 0%, + rgb(var(--primary-4)) 100% + ); + box-shadow: 0 4px 12px rgba(var(--primary-6), 0.4); + } + + .tab-close { + color: rgba(255, 255, 255, 80%); + + &:hover { + color: #fff; + background-color: rgba(255, 255, 255, 20%); + } + } + } + + .tab-title { + display: inline-block; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .tab-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + margin-left: 8px; + border-radius: 50%; + color: var(--color-text-3); + transition: all 0.2s; + font-size: 12px; + + &:hover { + color: var(--color-text-1); + background-color: var(--color-fill-3); + } + } + } +} diff --git a/src/components/TabBar/index.tsx b/src/components/TabBar/index.tsx new file mode 100644 index 0000000..b89a4b3 --- /dev/null +++ b/src/components/TabBar/index.tsx @@ -0,0 +1,178 @@ +import React from 'react'; +import { IconClose } from '@arco-design/web-react/icon'; +import { useHistory } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { GlobalState } from '@/store'; +import { ContextMenuType } from '@/types/tabs'; +import useLocale from '@/utils/useLocale'; +import ContextMenu from './ContextMenu'; +import styles from './index.module.less'; + +const TabBar: React.FC = () => { + const history = useHistory(); + const dispatch = useDispatch(); + const locale = useLocale(); + + const { tabs, activeKey } = useSelector((state: GlobalState) => state.tabs); + + // 点击标签页切换路由 + const handleTabClick = (key: string) => { + const targetTab = tabs.find((tab) => tab.key === key); + if (targetTab) { + dispatch({ + type: 'tabs/setActiveTab', + payload: key, + }); + history.push(targetTab.path); + } + }; + + // 关闭标签页 + const handleTabClose = (e: React.MouseEvent, key: string) => { + e.stopPropagation(); + const targetTab = tabs.find((tab) => tab.key === key); + if (!targetTab || !targetTab.closable) { + return; + } + + const targetIndex = tabs.findIndex((tab) => tab.key === key); + + dispatch({ + type: 'tabs/removeTab', + payload: key, + }); + + // 如果关闭的是当前激活的标签,需要切换到相邻标签 + if (activeKey === key && tabs.length > 1) { + let nextTab; + if (targetIndex < tabs.length - 1) { + nextTab = tabs[targetIndex + 1]; + } else { + nextTab = tabs[targetIndex - 1]; + } + history.push(nextTab.path); + } + }; + + // 右键菜单操作 + const handleContextMenu = (type: ContextMenuType, targetKey: string) => { + const targetTab = tabs.find((tab) => tab.key === targetKey); + + if (!targetTab) { + return; + } + + switch (type) { + case ContextMenuType.RELOAD: + // 重新加载页面 + window.location.reload(); + break; + + case ContextMenuType.CLOSE_CURRENT: + // 关闭当前标签 + if (targetTab.closable) { + const e = new MouseEvent('click') as any; + handleTabClose(e, targetKey); + } + break; + + case ContextMenuType.CLOSE_LEFT: + // 关闭左侧标签 + dispatch({ + type: 'tabs/closeLeftTabs', + payload: targetKey, + }); + break; + + case ContextMenuType.CLOSE_RIGHT: + // 关闭右侧标签 + dispatch({ + type: 'tabs/closeRightTabs', + payload: targetKey, + }); + break; + + case ContextMenuType.CLOSE_OTHERS: + // 关闭其他标签 + dispatch({ + type: 'tabs/closeOtherTabs', + payload: targetKey, + }); + history.push(targetTab.path); + break; + + default: + break; + } + }; + + // 计算右键菜单的可用状态 + const getContextMenuProps = (targetKey: string) => { + const targetIndex = tabs.findIndex((tab) => tab.key === targetKey); + + // 检查左侧是否有可关闭的标签 + const canCloseLeft = tabs.slice(0, targetIndex).some((tab) => tab.closable); + + // 检查右侧是否有可关闭的标签 + const canCloseRight = tabs + .slice(targetIndex + 1) + .some((tab) => tab.closable); + + // 检查是否有其他可关闭的标签 + const canCloseOthers = tabs.some( + (tab) => tab.key !== targetKey && tab.closable + ); + + return { + targetKey, + canCloseLeft, + canCloseRight, + canCloseOthers, + }; + }; + + // 如果没有标签页,不渲染 + if (!tabs || tabs.length === 0) { + return null; + } + + return ( +
+
+ {tabs.map((tab) => { + const contextMenuProps = getContextMenuProps(tab.key); + const isActive = activeKey === tab.key; + + return ( + +
handleTabClick(tab.key)} + > + + {locale[tab.name] || tab.name} + + {tab.closable && ( + handleTabClose(e, tab.key)} + > + + + )} +
+
+ ); + })} +
+
+ ); +}; + +export default TabBar;