|
|
@@ -1,25 +1,30 @@
|
|
|
import { Fragment, memo, useMemo } from 'react';
|
|
|
|
|
|
-import { Dropdown, type MenuProps } from 'antd';
|
|
|
import { Icon } from '@iconify/react';
|
|
|
+import { Dropdown, type MenuProps } from 'antd';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
-import logoIcon from '@/assets/iconify/multi-color/logo.svg';
|
|
|
-import menuIcon from '@/assets/iconify/single-color/menu.svg';
|
|
|
-import closeIcon from '@/assets/iconify/single-color/close.svg';
|
|
|
+import logoUnion from '@/assets/iconify/multi-color/logo-union.svg';
|
|
|
import chevronDownIcon from '@/assets/iconify/single-color/chevron-down.svg';
|
|
|
-import type { NavMenuItem } from '@/utils/navUtils';
|
|
|
+import closeIcon from '@/assets/iconify/single-color/close.svg';
|
|
|
+import menuIcon from '@/assets/iconify/single-color/menu.svg';
|
|
|
+import { useAuth } from '@/hooks/useAuth';
|
|
|
+import { useLoginDialog } from '@/hooks/useLoginDialog';
|
|
|
import { useResponsive } from '@/hooks/useSize';
|
|
|
+import type { NavMenuItem } from '@/utils/navUtils';
|
|
|
+
|
|
|
import { useAction } from './useAction';
|
|
|
import { useService } from './useService';
|
|
|
|
|
|
const Topbar = memo(() => {
|
|
|
const { t } = useTranslation();
|
|
|
const { isMobile } = useResponsive();
|
|
|
- const isRtl = t('DIR') === 'rtl';
|
|
|
const { menuItems, isActive, getMenuItemLabel } = useService();
|
|
|
+ const { isLoggedIn } = useAuth();
|
|
|
+ const openLoginDialog = useLoginDialog();
|
|
|
const {
|
|
|
menuContainerRef,
|
|
|
+ loginButtonRef,
|
|
|
isMobileMenuOpen,
|
|
|
isMobileMenuClosing,
|
|
|
isOverflowMenuOpen,
|
|
|
@@ -31,7 +36,7 @@ const Topbar = memo(() => {
|
|
|
handleMenuAnimationEnd,
|
|
|
setOverflowMenuOpen,
|
|
|
setMenuItemRef,
|
|
|
- } = useAction({ menuItems, isMobile });
|
|
|
+ } = useAction({ menuItems, isMobile, isLoggedIn });
|
|
|
|
|
|
const overflowMenuProps: MenuProps = useMemo(
|
|
|
() => ({
|
|
|
@@ -47,11 +52,11 @@ const Topbar = memo(() => {
|
|
|
return (
|
|
|
<Fragment>
|
|
|
<header className="fixed top-0 start-0 end-0 z-50 bg-black/90 border-b border-white/10 backdrop-blur-sm">
|
|
|
- <div className="h-[81px] px-[30px] sm:px-6 lg:px-8 flex items-center justify-between max-w-[1276px] mx-auto">
|
|
|
+ <div className="flex items-center justify-between px-5 sm:px-6 lg:px-20 py-5 max-w-[1440px] mx-auto">
|
|
|
{/* Logo */}
|
|
|
- <div className="flex-shrink-0 flex items-center gap-2">
|
|
|
- <Icon icon={logoIcon} className="w-8 h-8" />
|
|
|
- <h1 className="text-base font-normal text-white leading-6">
|
|
|
+ <div className="flex-shrink-0 flex items-center gap-3">
|
|
|
+ <Icon icon={logoUnion} className="w-8 h-8" />
|
|
|
+ <h1 className="text-2xl font-bold italic text-white leading-none font-[REM] tracking-wide">
|
|
|
{t('components.topbar.logo')}
|
|
|
</h1>
|
|
|
</div>
|
|
|
@@ -62,21 +67,19 @@ const Topbar = memo(() => {
|
|
|
ref={menuContainerRef}
|
|
|
className="flex-1 flex items-center justify-end gap-2.5 ms-8 min-w-0"
|
|
|
>
|
|
|
- <div className="flex items-center gap-2.5 flex-1 justify-end min-w-0">
|
|
|
+ <div className="flex items-center gap-2.5 min-w-0">
|
|
|
{visibleMenuItems.map((item: NavMenuItem) => {
|
|
|
const active = isActive(item.path);
|
|
|
- const baseClasses =
|
|
|
- 'px-5 py-2.5 rounded-[20px] transition-colors font-normal text-base leading-6 border-none whitespace-nowrap';
|
|
|
- const activeClasses = 'bg-[#0FA4E9] text-white';
|
|
|
- const inactiveClasses =
|
|
|
- 'bg-transparent text-white/80 hover:text-white';
|
|
|
-
|
|
|
return (
|
|
|
<button
|
|
|
key={item.name}
|
|
|
ref={setMenuItemRef(item.name)}
|
|
|
onClick={() => handleMenuClick(item.path)}
|
|
|
- className={`${baseClasses} ${active ? activeClasses : inactiveClasses}`}
|
|
|
+ className={`px-2.5 h-10 flex items-center justify-center transition-colors text-base leading-6 border-none bg-transparent whitespace-nowrap ${
|
|
|
+ active
|
|
|
+ ? 'font-bold text-[#0FA4E9] border-b-2 border-[#0FA4E9]'
|
|
|
+ : 'font-normal text-white/80 hover:text-white'
|
|
|
+ }`}
|
|
|
>
|
|
|
{getMenuItemLabel(item)}
|
|
|
</button>
|
|
|
@@ -93,7 +96,7 @@ const Topbar = memo(() => {
|
|
|
>
|
|
|
<button
|
|
|
type="button"
|
|
|
- className="px-5 py-2.5 rounded-[20px] transition-colors font-normal text-base leading-6 border-none bg-transparent text-white/80 hover:text-white flex items-center gap-1 whitespace-nowrap"
|
|
|
+ className="px-2.5 h-10 flex items-center justify-center transition-colors text-base leading-6 border-none bg-transparent text-white/80 hover:text-white gap-1 whitespace-nowrap"
|
|
|
>
|
|
|
<span>...</span>
|
|
|
<Icon
|
|
|
@@ -106,6 +109,17 @@ const Topbar = memo(() => {
|
|
|
</Dropdown>
|
|
|
)}
|
|
|
</div>
|
|
|
+
|
|
|
+ {!isLoggedIn && (
|
|
|
+ <button
|
|
|
+ ref={loginButtonRef}
|
|
|
+ type="button"
|
|
|
+ onClick={() => openLoginDialog()}
|
|
|
+ className="ms-2 px-4 h-10 rounded-full bg-[#0FA4E9] text-white text-base font-normal border-none hover:bg-[#0d93d1] transition-colors whitespace-nowrap"
|
|
|
+ >
|
|
|
+ {t('components.topbar.login')}
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
</nav>
|
|
|
)}
|
|
|
|
|
|
@@ -114,7 +128,7 @@ const Topbar = memo(() => {
|
|
|
<button
|
|
|
type="button"
|
|
|
onClick={toggleMobileMenu}
|
|
|
- className="p-2 rounded-lg bg-transparent border-none text-white outline-none focus:outline-none focus:bg-transparent active:bg-transparent hover:text-[#0EA5E9]/80 active:text-[#0EA5E9]/60 [-webkit-tap-highlight-color:transparent] transition-colors"
|
|
|
+ className="p-2 rounded-xl bg-white/20 border-none text-white outline-none focus:outline-none [-webkit-tap-highlight-color:transparent] transition-colors"
|
|
|
aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
|
|
|
>
|
|
|
<Icon
|
|
|
@@ -126,44 +140,50 @@ const Topbar = memo(() => {
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
- {/* Mobile Expanded Menu (Sidebar) - 移到 header 外部 */}
|
|
|
+ {/* Mobile Expanded Menu */}
|
|
|
{isMobile && (isMobileMenuOpen || isMobileMenuClosing) && (
|
|
|
<>
|
|
|
- {/* 遮罩层(毛玻璃) */}
|
|
|
<div
|
|
|
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40 top-[81px]"
|
|
|
onClick={closeMobileMenu}
|
|
|
/>
|
|
|
- {/* 侧边栏菜单:LTR 自右向左进入/向右退出,RTL 自左向右进入/向左退出 */}
|
|
|
<nav
|
|
|
- className={`fixed end-0 top-[81px] bottom-0 w-[250px] bg-black/80 backdrop-blur-[4px] z-50 ${
|
|
|
+ className={`fixed end-5 top-[81px] z-50 border border-white/[0.35] bg-black/85 rounded-lg origin-top ${
|
|
|
isMobileMenuClosing
|
|
|
- ? isRtl
|
|
|
- ? 'animate-slide-out-to-start'
|
|
|
- : 'animate-slide-out-to-end'
|
|
|
- : isRtl
|
|
|
- ? 'animate-slide-in-from-start'
|
|
|
- : 'animate-slide-in-from-end'
|
|
|
+ ? 'animate-collapse-up'
|
|
|
+ : 'animate-expand-down'
|
|
|
}`}
|
|
|
onAnimationEnd={handleMenuAnimationEnd}
|
|
|
>
|
|
|
- <div className="h-full px-[30px] pt-[30px] flex flex-col gap-[14px]">
|
|
|
+ <div className="flex flex-col items-end gap-4 p-4">
|
|
|
{menuItems.map((item: NavMenuItem) => {
|
|
|
const active = isActive(item.path);
|
|
|
return (
|
|
|
<button
|
|
|
key={item.name}
|
|
|
onClick={() => handleMenuClick(item.path)}
|
|
|
- className={`w-full text-start text-xl font-medium leading-[1.4em] transition-colors border-none bg-transparent ${
|
|
|
+ className={`text-base leading-[1.5] transition-colors border-none bg-transparent whitespace-nowrap ${
|
|
|
active
|
|
|
- ? 'text-[#0EA5E9]'
|
|
|
- : 'text-white hover:text-white/80'
|
|
|
+ ? 'font-bold text-white'
|
|
|
+ : 'font-normal text-[#999] hover:text-white'
|
|
|
}`}
|
|
|
>
|
|
|
{getMenuItemLabel(item)}
|
|
|
</button>
|
|
|
);
|
|
|
})}
|
|
|
+ {!isLoggedIn && (
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={() => {
|
|
|
+ closeMobileMenu();
|
|
|
+ openLoginDialog();
|
|
|
+ }}
|
|
|
+ className="text-base leading-[1.5] font-normal text-[#999] hover:text-white transition-colors border-none bg-transparent whitespace-nowrap"
|
|
|
+ >
|
|
|
+ {t('components.topbar.login')}
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
</div>
|
|
|
</nav>
|
|
|
</>
|