From 70892d57b896250c7c979888aa0da089c5e9ece7 Mon Sep 17 00:00:00 2001 From: qq143838 Date: Fri, 28 Aug 2026 05:59:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=A1=B6=E9=83=A8=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E6=A0=8F=E5=B7=A6=E5=8F=B3=E6=BB=9A=E5=8A=A8=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/WorkspaceTabs.css | 36 ++++++++ src/layouts/WorkspaceTabs.tsx | 162 ++++++++++++++++++++++++++++------ 2 files changed, 169 insertions(+), 29 deletions(-) diff --git a/src/layouts/WorkspaceTabs.css b/src/layouts/WorkspaceTabs.css index f413238..91221e8 100644 --- a/src/layouts/WorkspaceTabs.css +++ b/src/layouts/WorkspaceTabs.css @@ -33,6 +33,15 @@ height: 100%; } +.workspace-tabs__rail { + display: flex; + flex: 1 1 auto; + align-items: flex-end; + min-width: 0; + height: 100%; + overflow: hidden; +} + .workspace-tabs__track { display: flex; flex: 1 1 auto; @@ -41,6 +50,8 @@ height: 100%; overflow-x: auto; overflow-y: hidden; + overscroll-behavior-x: contain; + scroll-behavior: smooth; scrollbar-width: none; } @@ -85,6 +96,7 @@ .workspace-tab__main, .workspace-tab__close, +.workspace-tabs__scroll, .workspace-tabs__more, .workspace-tabs__theme, .workspace-tabs__logout, @@ -131,6 +143,7 @@ .workspace-tab__main:focus-visible, .workspace-tab__close:focus-visible, +.workspace-tabs__scroll:focus-visible, .workspace-tabs__more:focus-visible, .workspace-tabs__theme:focus-visible, .workspace-tabs__logout:focus-visible, @@ -139,6 +152,29 @@ outline-offset: -2px; } +.workspace-tabs__scroll { + display: inline-flex; + flex: 0 0 28px; + align-items: center; + justify-content: center; + width: 28px; + height: 32px; + margin-bottom: 4px; + border-radius: 4px; + color: var(--ant-color-text-secondary); + font-size: 12px; +} + +.workspace-tabs__scroll:not(:disabled):hover { + color: var(--ant-color-primary); + background: var(--ant-color-fill-tertiary); +} + +.workspace-tabs__scroll:disabled { + cursor: default; + opacity: 0.28; +} + .workspace-tabs__more, .workspace-tabs__theme, .workspace-tabs__logout { diff --git a/src/layouts/WorkspaceTabs.tsx b/src/layouts/WorkspaceTabs.tsx index a3b448d..fcdd318 100644 --- a/src/layouts/WorkspaceTabs.tsx +++ b/src/layouts/WorkspaceTabs.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { ComponentType } from 'react'; import { Dropdown, Tooltip } from 'antd'; import type { MenuProps } from 'antd'; @@ -11,10 +11,12 @@ import { DownOutlined, IdcardOutlined, KeyOutlined, + LeftOutlined, LogoutOutlined, MoreOutlined, MoonFilled, MoonOutlined, + RightOutlined, ShoppingCartOutlined, SunOutlined, UserOutlined, @@ -90,6 +92,13 @@ export default function WorkspaceTabs() { const { pathname } = useLocation(); const activePath = normalizePath(pathname); const [openedPaths, setOpenedPaths] = useState(() => readOpenedPaths(activePath)); + const tabsRailRef = useRef(null); + const tabsTrackRef = useRef(null); + const [tabScrollState, setTabScrollState] = useState({ + hasOverflow: false, + canScrollLeft: false, + canScrollRight: false, + }); const openedTabs = useMemo( () => @@ -105,10 +114,69 @@ export default function WorkspaceTabs() { setOpenedPaths(paths); }, []); + const updateTabScrollState = useCallback(() => { + const rail = tabsRailRef.current; + const track = tabsTrackRef.current; + if (!rail || !track) return; + + const maxScrollLeft = Math.max(0, track.scrollWidth - track.clientWidth); + const hasOverflow = track.scrollWidth > rail.clientWidth + 1; + const nextState = { + hasOverflow, + canScrollLeft: hasOverflow && track.scrollLeft > 1, + canScrollRight: hasOverflow && track.scrollLeft < maxScrollLeft - 1, + }; + + setTabScrollState((currentState) => + currentState.hasOverflow === nextState.hasOverflow && + currentState.canScrollLeft === nextState.canScrollLeft && + currentState.canScrollRight === nextState.canScrollRight + ? currentState + : nextState, + ); + }, []); + + const scrollTabs = useCallback((direction: 'left' | 'right') => { + const track = tabsTrackRef.current; + if (!track) return; + + const distance = Math.max(180, Math.round(track.clientWidth * 0.7)); + track.scrollBy({ + left: direction === 'left' ? -distance : distance, + behavior: 'smooth', + }); + }, []); + useEffect(() => { saveOpenedPaths(openedPaths); }, [openedPaths]); + useEffect(() => { + const rail = tabsRailRef.current; + const track = tabsTrackRef.current; + if (!rail || !track) return; + + updateTabScrollState(); + + const resizeObserver = + typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(updateTabScrollState); + resizeObserver?.observe(rail); + resizeObserver?.observe(track); + window.addEventListener('resize', updateTabScrollState); + + return () => { + resizeObserver?.disconnect(); + window.removeEventListener('resize', updateTabScrollState); + }; + }, [openedTabs, updateTabScrollState]); + + useEffect(() => { + const track = tabsTrackRef.current; + const activeTabElement = track?.querySelector('.workspace-tab.is-active'); + activeTabElement?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); + updateTabScrollState(); + }, [activePath, updateTabScrollState]); + const closeTab = useCallback( (path: string) => { const target = definitionByPath.get(path); @@ -247,38 +315,74 @@ export default function WorkspaceTabs() {

{activeTab.title}

-
- {openedTabs.map((tab) => { - const Icon = tab.icon; - const active = tab.path === activePath; - return ( -
- - {!tab.pinned && ( +
+ {tabScrollState.hasOverflow && ( + + + + )} + +
+ {openedTabs.map((tab) => { + const Icon = tab.icon; + const active = tab.path === activePath; + return ( +
- )} -
- ); - })} + {!tab.pinned && ( + + )} +
+ ); + })} +
+ + {tabScrollState.hasOverflow && ( + + + + )}