feat(TabContainer, PinnedMinapps): enhance tab navigation and improve minapp switch indicator

- Added a new handleTabClick function to streamline tab navigation and hide the minapp popup when a tab is clicked.
- Implemented an animation for the minapp switch indicator in the TopNavbarOpenedMinappTabs component, improving visual feedback during tab switching.
- Refactored the rendering of minapp items to use Tooltip for better accessibility and user experience.
- Removed unnecessary StyledLink components to simplify the structure of the navigation items.
This commit is contained in:
kangfenmao 2025-07-30 12:00:49 +08:00
parent bee933dd72
commit 47c909dda4
2 changed files with 69 additions and 87 deletions

View File

@ -127,6 +127,11 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
navigate(lastSettingsPath) navigate(lastSettingsPath)
} }
const handleTabClick = (tab: Tab) => {
hideMinappPopup()
navigate(tab.path)
}
return ( return (
<Container> <Container>
<TabsBar $isFullscreen={isFullscreen}> <TabsBar $isFullscreen={isFullscreen}>
@ -134,15 +139,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
.filter((tab) => !specialTabs.includes(tab.id)) .filter((tab) => !specialTabs.includes(tab.id))
.map((tab) => { .map((tab) => {
return ( return (
<Tab <Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
key={tab.id}
active={tab.id === activeTabId}
onClick={() => {
hideMinappPopup()
// 我不确定这个还需不需要从Siderbar那边复制过来的
// await modelGenerating()
navigate(tab.path)
}}>
<TabHeader> <TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>} {tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
<TabTitle>{getTitleLabel(tab.id)}</TabTitle> <TabTitle>{getTitleLabel(tab.id)}</TabTitle>

View File

@ -27,6 +27,27 @@ export const TopNavbarOpenedMinappTabs: FC = () => {
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, [openedKeepAliveMinapps]) }, [openedKeepAliveMinapps])
// animation for minapp switch indicator
useEffect(() => {
const iconDefaultWidth = 30 // 22px icon + 8px gap
const iconDefaultOffset = 10 // initial offset
const container = document.querySelector('.TopNavContainer') as HTMLElement
const activeIcon = document.querySelector('.TopNavContainer .opened-active') as HTMLElement
let indicatorLeft = 0,
indicatorBottom = 0
if (minappShow && activeIcon && container) {
indicatorLeft = activeIcon.offsetLeft + activeIcon.offsetWidth / 2 - 4 // 4 is half of the indicator's width (8px)
indicatorBottom = 0
} else {
indicatorLeft =
((keepAliveMinapps.length > 0 ? keepAliveMinapps.length : 1) / 2) * iconDefaultWidth + iconDefaultOffset - 4
indicatorBottom = -50
}
container?.style.setProperty('--indicator-left', `${indicatorLeft}px`)
container?.style.setProperty('--indicator-bottom', `${indicatorBottom}px`)
}, [currentMinappId, keepAliveMinapps, minappShow])
const handleOnClick = (app: MinAppType) => { const handleOnClick = (app: MinAppType) => {
if (minappShow && currentMinappId === app.id) { if (minappShow && currentMinappId === app.id) {
hideMinappPopup() hideMinappPopup()
@ -43,6 +64,7 @@ export const TopNavbarOpenedMinappTabs: FC = () => {
return ( return (
<TopNavContainer <TopNavContainer
className="TopNavContainer"
style={{ backgroundColor: keepAliveMinapps.length > 0 ? 'var(--color-list-item)' : 'transparent' }}> style={{ backgroundColor: keepAliveMinapps.length > 0 ? 'var(--color-list-item)' : 'transparent' }}>
<TopNavMenus> <TopNavMenus>
{keepAliveMinapps.map((app) => { {keepAliveMinapps.map((app) => {
@ -62,22 +84,22 @@ export const TopNavbarOpenedMinappTabs: FC = () => {
} }
} }
] ]
const isActive = minappShow && currentMinappId === app.id const isActive = minappShow && currentMinappId === app.id
return ( return (
<StyledLink key={app.id}> <Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="bottom">
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}> <Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<TopNavItemContainer <TopNavItemContainer
className={`${isActive ? 'opened-active' : ''}`}
onClick={() => handleOnClick(app)} onClick={() => handleOnClick(app)}
theme={theme}> theme={theme}
className={`${isActive ? 'opened-active' : ''}`}>
<TopNavIcon theme={theme}> <TopNavIcon theme={theme}>
<MinAppIcon size={22} app={app} style={{ border: 'none', padding: 0 }} /> <MinAppIcon size={22} app={app} style={{ border: 'none', padding: 0 }} />
</TopNavIcon> </TopNavIcon>
<TopNavLabel>{app.name}</TopNavLabel>
</TopNavItemContainer> </TopNavItemContainer>
</Dropdown> </Dropdown>
</StyledLink> </Tooltip>
) )
})} })}
</TopNavMenus> </TopNavMenus>
@ -158,16 +180,14 @@ export const SidebarOpenedMinappTabs: FC = () => {
return ( return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right"> <Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink> <Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}> <Icon
<Icon theme={theme}
theme={theme} onClick={() => handleOnClick(app)}
onClick={() => handleOnClick(app)} className={`${isActive ? 'opened-active' : ''}`}>
className={`${isActive ? 'opened-active' : ''}`}> <MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar />
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar /> </Icon>
</Icon> </Dropdown>
</Dropdown>
</StyledLink>
</Tooltip> </Tooltip>
) )
})} })}
@ -201,16 +221,14 @@ export const SidebarPinnedApps: FC = () => {
const isActive = minappShow && currentMinappId === app.id const isActive = minappShow && currentMinappId === app.id
return ( return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right"> <Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink> <Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}> <Icon
<Icon theme={theme}
theme={theme} onClick={() => openMinappKeepAlive(app)}
onClick={() => openMinappKeepAlive(app)} className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-minapp' : ''}`}>
className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-minapp' : ''}`}> <MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar />
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} sidebar /> </Icon>
</Icon> </Dropdown>
</Dropdown>
</StyledLink>
</Tooltip> </Tooltip>
) )
}} }}
@ -239,16 +257,10 @@ const Icon = styled.div<{ theme: string }>`
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')}; background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
opacity: 0.8; opacity: 0.8;
cursor: pointer; cursor: pointer;
.icon {
color: var(--color-icon-white);
}
} }
&.active { &.active {
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')}; background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
.icon {
color: var(--color-primary);
}
} }
@keyframes borderBreath { @keyframes borderBreath {
@ -279,14 +291,6 @@ const Icon = styled.div<{ theme: string }>`
} }
` `
const StyledLink = styled.div`
text-decoration: none;
-webkit-app-region: none;
&* {
user-select: none;
}
`
const Divider = styled.div` const Divider = styled.div`
width: 50%; width: 50%;
margin: 8px 0; margin: 8px 0;
@ -330,64 +334,45 @@ const TopNavContainer = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px; padding: 2px;
gap: 6px; gap: 4px;
background-color: var(--color-list-item); background-color: var(--color-list-item);
border-radius: 20px; border-radius: 20px;
margin: 0 5px; margin: 0 5px;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
left: var(--indicator-left, 0);
bottom: var(--indicator-bottom, 0);
width: 8px;
height: 4px;
background-color: var(--color-primary);
transition:
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
bottom 0.3s ease-in-out;
border-radius: 2px;
}
` `
const TopNavMenus = styled.div` const TopNavMenus = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 0 2px;
height: 100%; height: 100%;
` `
const TopNavIcon = styled(Icon)` const TopNavIcon = styled(Icon)`
width: 22px; width: 22px;
height: 22px; height: 22px;
.icon {
width: 22px;
height: 22px;
}
&.opened-active {
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
border: 0.5px solid var(--color-border);
border-radius: 25%;
.icon {
color: var(--color-primary);
}
}
`
const TopNavLabel = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
` `
const TopNavItemContainer = styled.div` const TopNavItemContainer = styled.div`
display: flex; display: flex;
padding: 4px 2px;
transition: border 0.2s ease; transition: border 0.2s ease;
border-radius: 18px; border-radius: 18px;
/* 避免布局偏移 */ cursor: pointer;
border: 1px solid transparent; border-radius: 50%;
padding: 2px;
&:hover {
border-bottom: 2px solid var(--color-primary);
opacity: 0.8;
cursor: pointer;
}
&.opened-active {
border: 1px solid var(--color-primary);
.icon {
color: var(--color-primary);
}
}
` `