mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 22:10:21 +08:00
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:
parent
bee933dd72
commit
47c909dda4
@ -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>
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user