增加顶部标签栏左右滚动控制

This commit is contained in:
qq143838
2026-08-28 05:59:23 +08:00
parent c42765034e
commit 70892d57b8
2 changed files with 169 additions and 29 deletions
+36
View File
@@ -33,6 +33,15 @@
height: 100%; 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 { .workspace-tabs__track {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
@@ -41,6 +50,8 @@
height: 100%; height: 100%;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
overscroll-behavior-x: contain;
scroll-behavior: smooth;
scrollbar-width: none; scrollbar-width: none;
} }
@@ -85,6 +96,7 @@
.workspace-tab__main, .workspace-tab__main,
.workspace-tab__close, .workspace-tab__close,
.workspace-tabs__scroll,
.workspace-tabs__more, .workspace-tabs__more,
.workspace-tabs__theme, .workspace-tabs__theme,
.workspace-tabs__logout, .workspace-tabs__logout,
@@ -131,6 +143,7 @@
.workspace-tab__main:focus-visible, .workspace-tab__main:focus-visible,
.workspace-tab__close:focus-visible, .workspace-tab__close:focus-visible,
.workspace-tabs__scroll:focus-visible,
.workspace-tabs__more:focus-visible, .workspace-tabs__more:focus-visible,
.workspace-tabs__theme:focus-visible, .workspace-tabs__theme:focus-visible,
.workspace-tabs__logout:focus-visible, .workspace-tabs__logout:focus-visible,
@@ -139,6 +152,29 @@
outline-offset: -2px; 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__more,
.workspace-tabs__theme, .workspace-tabs__theme,
.workspace-tabs__logout { .workspace-tabs__logout {
+133 -29
View File
@@ -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 type { ComponentType } from 'react';
import { Dropdown, Tooltip } from 'antd'; import { Dropdown, Tooltip } from 'antd';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
@@ -11,10 +11,12 @@ import {
DownOutlined, DownOutlined,
IdcardOutlined, IdcardOutlined,
KeyOutlined, KeyOutlined,
LeftOutlined,
LogoutOutlined, LogoutOutlined,
MoreOutlined, MoreOutlined,
MoonFilled, MoonFilled,
MoonOutlined, MoonOutlined,
RightOutlined,
ShoppingCartOutlined, ShoppingCartOutlined,
SunOutlined, SunOutlined,
UserOutlined, UserOutlined,
@@ -90,6 +92,13 @@ export default function WorkspaceTabs() {
const { pathname } = useLocation(); const { pathname } = useLocation();
const activePath = normalizePath(pathname); const activePath = normalizePath(pathname);
const [openedPaths, setOpenedPaths] = useState(() => readOpenedPaths(activePath)); const [openedPaths, setOpenedPaths] = useState(() => readOpenedPaths(activePath));
const tabsRailRef = useRef<HTMLDivElement>(null);
const tabsTrackRef = useRef<HTMLDivElement>(null);
const [tabScrollState, setTabScrollState] = useState({
hasOverflow: false,
canScrollLeft: false,
canScrollRight: false,
});
const openedTabs = useMemo( const openedTabs = useMemo(
() => () =>
@@ -105,10 +114,69 @@ export default function WorkspaceTabs() {
setOpenedPaths(paths); 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(() => { useEffect(() => {
saveOpenedPaths(openedPaths); saveOpenedPaths(openedPaths);
}, [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<HTMLElement>('.workspace-tab.is-active');
activeTabElement?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
updateTabScrollState();
}, [activePath, updateTabScrollState]);
const closeTab = useCallback( const closeTab = useCallback(
(path: string) => { (path: string) => {
const target = definitionByPath.get(path); const target = definitionByPath.get(path);
@@ -247,38 +315,74 @@ export default function WorkspaceTabs() {
<h1 className="workspace-tabs__page-title">{activeTab.title}</h1> <h1 className="workspace-tabs__page-title">{activeTab.title}</h1>
<div className="workspace-tabs__desktop"> <div className="workspace-tabs__desktop">
<div className="workspace-tabs__track" role="tablist" aria-label="已打开页面"> <div ref={tabsRailRef} className="workspace-tabs__rail">
{openedTabs.map((tab) => { {tabScrollState.hasOverflow && (
const Icon = tab.icon; <Tooltip title="向左滚动">
const active = tab.path === activePath; <button
return ( type="button"
<div key={tab.path} className={`workspace-tab${active ? ' is-active' : ''}`}> className="workspace-tabs__scroll"
<button aria-label="向左滚动已打开页面"
type="button" disabled={!tabScrollState.canScrollLeft}
className="workspace-tab__main" onClick={() => scrollTabs('left')}
role="tab" >
aria-label={tab.title} <LeftOutlined />
aria-selected={active} </button>
title={tab.title} </Tooltip>
onClick={() => navigate(tab.path)} )}
>
<Icon /> <div
<span className="workspace-tab__label">{tab.title}</span> ref={tabsTrackRef}
</button> className="workspace-tabs__track"
{!tab.pinned && ( role="tablist"
aria-label="已打开页面"
onScroll={updateTabScrollState}
>
{openedTabs.map((tab) => {
const Icon = tab.icon;
const active = tab.path === activePath;
return (
<div key={tab.path} className={`workspace-tab${active ? ' is-active' : ''}`}>
<button <button
type="button" type="button"
className="workspace-tab__close" className="workspace-tab__main"
aria-label={`关闭 ${tab.title}`} role="tab"
title={`关闭 ${tab.title}`} aria-label={tab.title}
onClick={() => closeTab(tab.path)} aria-selected={active}
title={tab.title}
onClick={() => navigate(tab.path)}
> >
<CloseOutlined /> <Icon />
<span className="workspace-tab__label">{tab.title}</span>
</button> </button>
)} {!tab.pinned && (
</div> <button
); type="button"
})} className="workspace-tab__close"
aria-label={`关闭 ${tab.title}`}
title={`关闭 ${tab.title}`}
onClick={() => closeTab(tab.path)}
>
<CloseOutlined />
</button>
)}
</div>
);
})}
</div>
{tabScrollState.hasOverflow && (
<Tooltip title="向右滚动">
<button
type="button"
className="workspace-tabs__scroll"
aria-label="向右滚动已打开页面"
disabled={!tabScrollState.canScrollRight}
onClick={() => scrollTabs('right')}
>
<RightOutlined />
</button>
</Tooltip>
)}
</div> </div>
<Dropdown menu={{ items: actionItems, onClick: handleAction }} trigger={['click']}> <Dropdown menu={{ items: actionItems, onClick: handleAction }} trigger={['click']}>