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

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 {
+106 -2
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,7 +315,28 @@ 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">
{tabScrollState.hasOverflow && (
<Tooltip title="向左滚动">
<button
type="button"
className="workspace-tabs__scroll"
aria-label="向左滚动已打开页面"
disabled={!tabScrollState.canScrollLeft}
onClick={() => scrollTabs('left')}
>
<LeftOutlined />
</button>
</Tooltip>
)}
<div
ref={tabsTrackRef}
className="workspace-tabs__track"
role="tablist"
aria-label="已打开页面"
onScroll={updateTabScrollState}
>
{openedTabs.map((tab) => { {openedTabs.map((tab) => {
const Icon = tab.icon; const Icon = tab.icon;
const active = tab.path === activePath; const active = tab.path === activePath;
@@ -281,6 +370,21 @@ export default function WorkspaceTabs() {
})} })}
</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>
<Dropdown menu={{ items: actionItems, onClick: handleAction }} trigger={['click']}> <Dropdown menu={{ items: actionItems, onClick: handleAction }} trigger={['click']}>
<Tooltip title="标签操作"> <Tooltip title="标签操作">
<button type="button" className="workspace-tabs__more" aria-label="标签操作"> <button type="button" className="workspace-tabs__more" aria-label="标签操作">