- Increase skeleton card count from 6 to 10 for better visual coverage - Extend minimum skeleton display duration from 500ms to 1000ms for smoother UX - Add circle shape to all pricing tags for consistent rounded design - Apply circle styling to billing type, popularity, endpoint, and context tags This commit improves the visual consistency and user experience of the pricing card view by standardizing tag appearance and optimizing skeleton loading timing.
676 lines
25 KiB
JavaScript
676 lines
25 KiB
JavaScript
/*
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
|
|
import React, { useContext, useEffect, useState, useRef } from 'react';
|
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
|
import { UserContext } from '../../context/User/index.js';
|
|
import { useSetTheme, useTheme } from '../../context/Theme/index.js';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { API, getLogo, getSystemName, showSuccess, stringToColor } from '../../helpers/index.js';
|
|
import fireworks from 'react-fireworks';
|
|
import { CN, GB } from 'country-flag-icons/react/3x2';
|
|
import NoticeModal from './NoticeModal.js';
|
|
|
|
import {
|
|
IconClose,
|
|
IconMenu,
|
|
IconLanguage,
|
|
IconChevronDown,
|
|
IconSun,
|
|
IconMoon,
|
|
IconExit,
|
|
IconUserSetting,
|
|
IconCreditCard,
|
|
IconKey,
|
|
IconBell,
|
|
} from '@douyinfe/semi-icons';
|
|
import {
|
|
Avatar,
|
|
Button,
|
|
Dropdown,
|
|
Tag,
|
|
Typography,
|
|
Skeleton,
|
|
Badge,
|
|
} from '@douyinfe/semi-ui';
|
|
import { StatusContext } from '../../context/Status/index.js';
|
|
import { useIsMobile } from '../../hooks/common/useIsMobile.js';
|
|
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed.js';
|
|
|
|
const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
|
const { t, i18n } = useTranslation();
|
|
const [userState, userDispatch] = useContext(UserContext);
|
|
const [statusState, statusDispatch] = useContext(StatusContext);
|
|
const isMobile = useIsMobile();
|
|
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [logoLoaded, setLogoLoaded] = useState(false);
|
|
let navigate = useNavigate();
|
|
const [currentLang, setCurrentLang] = useState(i18n.language);
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
const location = useLocation();
|
|
const [noticeVisible, setNoticeVisible] = useState(false);
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const loadingStartRef = useRef(Date.now());
|
|
|
|
const systemName = getSystemName();
|
|
const logo = getLogo();
|
|
const currentDate = new Date();
|
|
const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1;
|
|
|
|
const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false;
|
|
const docsLink = statusState?.status?.docs_link || '';
|
|
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
|
|
|
|
const isConsoleRoute = location.pathname.startsWith('/console');
|
|
|
|
const theme = useTheme();
|
|
const setTheme = useSetTheme();
|
|
|
|
const announcements = statusState?.status?.announcements || [];
|
|
|
|
const getAnnouncementKey = (a) => `${a?.publishDate || ''}-${(a?.content || '').slice(0, 30)}`;
|
|
|
|
const calculateUnreadCount = () => {
|
|
if (!announcements.length) return 0;
|
|
let readKeys = [];
|
|
try {
|
|
readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
|
|
} catch (_) {
|
|
readKeys = [];
|
|
}
|
|
const readSet = new Set(readKeys);
|
|
return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).length;
|
|
};
|
|
|
|
const getUnreadKeys = () => {
|
|
if (!announcements.length) return [];
|
|
let readKeys = [];
|
|
try {
|
|
readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
|
|
} catch (_) {
|
|
readKeys = [];
|
|
}
|
|
const readSet = new Set(readKeys);
|
|
return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).map(getAnnouncementKey);
|
|
};
|
|
|
|
useEffect(() => {
|
|
setUnreadCount(calculateUnreadCount());
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [announcements]);
|
|
|
|
const mainNavLinks = [
|
|
{
|
|
text: t('首页'),
|
|
itemKey: 'home',
|
|
to: '/',
|
|
},
|
|
{
|
|
text: t('控制台'),
|
|
itemKey: 'console',
|
|
to: '/console',
|
|
},
|
|
{
|
|
text: t('定价'),
|
|
itemKey: 'pricing',
|
|
to: '/pricing',
|
|
},
|
|
...(docsLink
|
|
? [
|
|
{
|
|
text: t('文档'),
|
|
itemKey: 'docs',
|
|
isExternal: true,
|
|
externalLink: docsLink,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
text: t('关于'),
|
|
itemKey: 'about',
|
|
to: '/about',
|
|
},
|
|
];
|
|
|
|
async function logout() {
|
|
await API.get('/api/user/logout');
|
|
showSuccess(t('注销成功!'));
|
|
userDispatch({ type: 'logout' });
|
|
localStorage.removeItem('user');
|
|
navigate('/login');
|
|
setMobileMenuOpen(false);
|
|
}
|
|
|
|
const handleNewYearClick = () => {
|
|
fireworks.init('root', {});
|
|
fireworks.start();
|
|
setTimeout(() => {
|
|
fireworks.stop();
|
|
}, 3000);
|
|
};
|
|
|
|
const handleNoticeOpen = () => {
|
|
setNoticeVisible(true);
|
|
};
|
|
|
|
const handleNoticeClose = () => {
|
|
setNoticeVisible(false);
|
|
if (announcements.length) {
|
|
let readKeys = [];
|
|
try {
|
|
readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
|
|
} catch (_) {
|
|
readKeys = [];
|
|
}
|
|
const mergedKeys = Array.from(new Set([...readKeys, ...announcements.map(getAnnouncementKey)]));
|
|
localStorage.setItem('notice_read_keys', JSON.stringify(mergedKeys));
|
|
}
|
|
setUnreadCount(0);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (theme === 'dark') {
|
|
document.body.setAttribute('theme-mode', 'dark');
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.body.removeAttribute('theme-mode');
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
|
|
const iframe = document.querySelector('iframe');
|
|
if (iframe) {
|
|
iframe.contentWindow.postMessage({ themeMode: theme }, '*');
|
|
}
|
|
|
|
}, [theme, isNewYear]);
|
|
|
|
useEffect(() => {
|
|
const handleLanguageChanged = (lng) => {
|
|
setCurrentLang(lng);
|
|
const iframe = document.querySelector('iframe');
|
|
if (iframe) {
|
|
iframe.contentWindow.postMessage({ lang: lng }, '*');
|
|
}
|
|
};
|
|
|
|
i18n.on('languageChanged', handleLanguageChanged);
|
|
return () => {
|
|
i18n.off('languageChanged', handleLanguageChanged);
|
|
};
|
|
}, [i18n]);
|
|
|
|
useEffect(() => {
|
|
if (statusState?.status !== undefined) {
|
|
const elapsed = Date.now() - loadingStartRef.current;
|
|
const remaining = Math.max(0, 1000 - elapsed);
|
|
const timer = setTimeout(() => {
|
|
setIsLoading(false);
|
|
}, remaining);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [statusState?.status]);
|
|
|
|
useEffect(() => {
|
|
setLogoLoaded(false);
|
|
if (!logo) return;
|
|
const img = new Image();
|
|
img.src = logo;
|
|
img.onload = () => setLogoLoaded(true);
|
|
}, [logo]);
|
|
|
|
const handleLanguageChange = (lang) => {
|
|
i18n.changeLanguage(lang);
|
|
setMobileMenuOpen(false);
|
|
};
|
|
|
|
const handleNavLinkClick = (itemKey) => {
|
|
if (itemKey === 'home') {
|
|
// styleDispatch(styleActions.setSider(false)); // This line is removed
|
|
}
|
|
setMobileMenuOpen(false);
|
|
};
|
|
|
|
const renderNavLinks = (isMobileView = false, isLoading = false) => {
|
|
if (isLoading) {
|
|
const skeletonLinkClasses = isMobileView
|
|
? 'flex items-center gap-1 p-3 w-full rounded-md'
|
|
: 'flex items-center gap-1 p-2 rounded-md';
|
|
return Array(4)
|
|
.fill(null)
|
|
.map((_, index) => (
|
|
<div key={index} className={skeletonLinkClasses}>
|
|
<Skeleton
|
|
loading={true}
|
|
active
|
|
placeholder={
|
|
<Skeleton.Title
|
|
active
|
|
style={{ width: isMobileView ? 100 : 60, height: 16 }}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
));
|
|
}
|
|
|
|
return mainNavLinks.map((link) => {
|
|
const commonLinkClasses = isMobileView
|
|
? 'flex items-center gap-1 p-3 w-full text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors font-semibold'
|
|
: 'flex items-center gap-1 p-2 text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors rounded-md font-semibold';
|
|
|
|
const linkContent = (
|
|
<span>{link.text}</span>
|
|
);
|
|
|
|
if (link.isExternal) {
|
|
return (
|
|
<a
|
|
key={link.itemKey}
|
|
href={link.externalLink}
|
|
target='_blank'
|
|
rel='noopener noreferrer'
|
|
className={commonLinkClasses}
|
|
onClick={() => handleNavLinkClick(link.itemKey)}
|
|
>
|
|
{linkContent}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
let targetPath = link.to;
|
|
if (link.itemKey === 'console' && !userState.user) {
|
|
targetPath = '/login';
|
|
}
|
|
|
|
return (
|
|
<Link
|
|
key={link.itemKey}
|
|
to={targetPath}
|
|
className={commonLinkClasses}
|
|
onClick={() => handleNavLinkClick(link.itemKey)}
|
|
>
|
|
{linkContent}
|
|
</Link>
|
|
);
|
|
});
|
|
};
|
|
|
|
const renderUserArea = () => {
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1">
|
|
<Skeleton
|
|
loading={true}
|
|
active
|
|
placeholder={<Skeleton.Avatar active size="extra-small" className="shadow-sm" />}
|
|
/>
|
|
<div className="ml-1.5 mr-1">
|
|
<Skeleton
|
|
loading={true}
|
|
active
|
|
placeholder={
|
|
<Skeleton.Title
|
|
active
|
|
style={{ width: isMobile ? 15 : 50, height: 12 }}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (userState.user) {
|
|
return (
|
|
<Dropdown
|
|
position="bottomRight"
|
|
render={
|
|
<Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
|
|
<Dropdown.Item
|
|
onClick={() => {
|
|
navigate('/console/personal');
|
|
setMobileMenuOpen(false);
|
|
}}
|
|
className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<IconUserSetting size="small" className="text-gray-500 dark:text-gray-400" />
|
|
<span>{t('个人设置')}</span>
|
|
</div>
|
|
</Dropdown.Item>
|
|
<Dropdown.Item
|
|
onClick={() => {
|
|
navigate('/console/token');
|
|
setMobileMenuOpen(false);
|
|
}}
|
|
className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<IconKey size="small" className="text-gray-500 dark:text-gray-400" />
|
|
<span>{t('令牌管理')}</span>
|
|
</div>
|
|
</Dropdown.Item>
|
|
<Dropdown.Item
|
|
onClick={() => {
|
|
navigate('/console/topup');
|
|
setMobileMenuOpen(false);
|
|
}}
|
|
className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<IconCreditCard size="small" className="text-gray-500 dark:text-gray-400" />
|
|
<span>{t('钱包')}</span>
|
|
</div>
|
|
</Dropdown.Item>
|
|
<Dropdown.Item onClick={logout} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white">
|
|
<div className="flex items-center gap-2">
|
|
<IconExit size="small" className="text-gray-500 dark:text-gray-400" />
|
|
<span>{t('退出')}</span>
|
|
</div>
|
|
</Dropdown.Item>
|
|
</Dropdown.Menu>
|
|
}
|
|
>
|
|
<Button
|
|
theme="borderless"
|
|
type="tertiary"
|
|
className="flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
|
>
|
|
<Avatar
|
|
size="extra-small"
|
|
color={stringToColor(userState.user.username)}
|
|
className="mr-1"
|
|
>
|
|
{userState.user.username[0].toUpperCase()}
|
|
</Avatar>
|
|
<span className="hidden md:inline">
|
|
<Typography.Text className="!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1">
|
|
{userState.user.username}
|
|
</Typography.Text>
|
|
</span>
|
|
<IconChevronDown className="text-xs text-semi-color-text-2 dark:text-gray-400" />
|
|
</Button>
|
|
</Dropdown>
|
|
);
|
|
} else {
|
|
const showRegisterButton = !isSelfUseMode;
|
|
|
|
const commonSizingAndLayoutClass = "flex items-center justify-center !py-[10px] !px-1.5";
|
|
|
|
const loginButtonSpecificStyling = "!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors";
|
|
let loginButtonClasses = `${commonSizingAndLayoutClass} ${loginButtonSpecificStyling}`;
|
|
|
|
let registerButtonClasses = `${commonSizingAndLayoutClass}`;
|
|
|
|
const loginButtonTextSpanClass = "!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5";
|
|
const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5";
|
|
|
|
if (showRegisterButton) {
|
|
if (isMobile) {
|
|
loginButtonClasses += " !rounded-full";
|
|
} else {
|
|
loginButtonClasses += " !rounded-l-full !rounded-r-none";
|
|
}
|
|
registerButtonClasses += " !rounded-r-full !rounded-l-none";
|
|
} else {
|
|
loginButtonClasses += " !rounded-full";
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-center">
|
|
<Link to="/login" onClick={() => handleNavLinkClick('login')} className="flex">
|
|
<Button
|
|
theme="borderless"
|
|
type="tertiary"
|
|
className={loginButtonClasses}
|
|
>
|
|
<span className={loginButtonTextSpanClass}>
|
|
{t('登录')}
|
|
</span>
|
|
</Button>
|
|
</Link>
|
|
{showRegisterButton && (
|
|
<div className="hidden md:block">
|
|
<Link to="/register" onClick={() => handleNavLinkClick('register')} className="flex -ml-px">
|
|
<Button
|
|
theme="solid"
|
|
type="primary"
|
|
className={registerButtonClasses}
|
|
>
|
|
<span className={registerButtonTextSpanClass}>
|
|
{t('注册')}
|
|
</span>
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<header className="text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
|
|
<NoticeModal
|
|
visible={noticeVisible}
|
|
onClose={handleNoticeClose}
|
|
isMobile={isMobile}
|
|
defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
|
|
unreadKeys={getUnreadKeys()}
|
|
/>
|
|
<div className="w-full px-2">
|
|
<div className="flex items-center justify-between h-16">
|
|
<div className="flex items-center">
|
|
<div className="md:hidden">
|
|
<Button
|
|
icon={
|
|
isConsoleRoute
|
|
? ((isMobile ? drawerOpen : collapsed) ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
|
|
: (mobileMenuOpen ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
|
|
}
|
|
aria-label={
|
|
isConsoleRoute
|
|
? ((isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏'))
|
|
: (mobileMenuOpen ? t('关闭菜单') : t('打开菜单'))
|
|
}
|
|
onClick={() => {
|
|
if (isConsoleRoute) {
|
|
// 控制侧边栏的显示/隐藏,无论是否移动设备
|
|
isMobile ? onMobileMenuToggle() : toggleCollapsed();
|
|
} else {
|
|
// 控制HeaderBar自己的移动菜单
|
|
setMobileMenuOpen(!mobileMenuOpen);
|
|
}
|
|
}}
|
|
theme="borderless"
|
|
type="tertiary"
|
|
className="!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700"
|
|
/>
|
|
</div>
|
|
<Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
|
|
<div className="relative w-8 h-8 md:w-8 md:h-8">
|
|
{(isLoading || !logoLoaded) && (
|
|
<Skeleton.Image
|
|
active
|
|
className="absolute inset-0 !rounded-full"
|
|
style={{ width: '100%', height: '100%' }}
|
|
/>
|
|
)}
|
|
<img
|
|
src={logo}
|
|
alt="logo"
|
|
className={`absolute inset-0 w-full h-full transition-opacity duration-200 group-hover:scale-105 rounded-full ${(!isLoading && logoLoaded) ? 'opacity-100' : 'opacity-0'}`}
|
|
/>
|
|
</div>
|
|
<div className="hidden md:flex items-center gap-2">
|
|
<div className="flex items-center gap-2">
|
|
<Skeleton
|
|
loading={isLoading}
|
|
active
|
|
placeholder={
|
|
<Skeleton.Title
|
|
active
|
|
style={{ width: 120, height: 24 }}
|
|
/>
|
|
}
|
|
>
|
|
<Typography.Title heading={4} className="!text-lg !font-semibold !mb-0">
|
|
{systemName}
|
|
</Typography.Title>
|
|
</Skeleton>
|
|
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
|
|
<Tag
|
|
color={isSelfUseMode ? 'purple' : 'blue'}
|
|
className="text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm"
|
|
size="small"
|
|
shape='circle'
|
|
>
|
|
{isSelfUseMode ? t('自用模式') : t('演示站点')}
|
|
</Tag>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
{(isSelfUseMode || isDemoSiteMode) && !isLoading && (
|
|
<div className="md:hidden">
|
|
<Tag
|
|
color={isSelfUseMode ? 'purple' : 'blue'}
|
|
className="ml-2 text-xs px-1 py-0.5 rounded whitespace-nowrap shadow-sm"
|
|
size="small"
|
|
shape='circle'
|
|
>
|
|
{isSelfUseMode ? t('自用模式') : t('演示站点')}
|
|
</Tag>
|
|
</div>
|
|
)}
|
|
|
|
<nav className="hidden md:flex items-center gap-1 lg:gap-2 ml-6">
|
|
{renderNavLinks(false, isLoading)}
|
|
</nav>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 md:gap-3">
|
|
{isNewYear && (
|
|
<Dropdown
|
|
position="bottomRight"
|
|
render={
|
|
<Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
|
|
<Dropdown.Item onClick={handleNewYearClick} className="!text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-gray-600">
|
|
Happy New Year!!! 🎉
|
|
</Dropdown.Item>
|
|
</Dropdown.Menu>
|
|
}
|
|
>
|
|
<Button
|
|
theme="borderless"
|
|
type="tertiary"
|
|
icon={<span className="text-xl">🎉</span>}
|
|
aria-label="New Year"
|
|
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full"
|
|
/>
|
|
</Dropdown>
|
|
)}
|
|
|
|
{unreadCount > 0 ? (
|
|
<Badge count={unreadCount} type="danger" overflowCount={99}>
|
|
<Button
|
|
icon={<IconBell className="text-lg" />}
|
|
aria-label={t('系统公告')}
|
|
onClick={handleNoticeOpen}
|
|
theme="borderless"
|
|
type="tertiary"
|
|
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
|
/>
|
|
</Badge>
|
|
) : (
|
|
<Button
|
|
icon={<IconBell className="text-lg" />}
|
|
aria-label={t('系统公告')}
|
|
onClick={handleNoticeOpen}
|
|
theme="borderless"
|
|
type="tertiary"
|
|
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
|
/>
|
|
)}
|
|
|
|
<Button
|
|
icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}
|
|
aria-label={t('切换主题')}
|
|
onClick={() => setTheme(theme === 'dark' ? false : true)}
|
|
theme="borderless"
|
|
type="tertiary"
|
|
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
|
/>
|
|
|
|
<Dropdown
|
|
position="bottomRight"
|
|
render={
|
|
<Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
|
|
<Dropdown.Item
|
|
onClick={() => handleLanguageChange('zh')}
|
|
className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'zh' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
|
>
|
|
<CN title="中文" className="!w-5 !h-auto" />
|
|
<span>中文</span>
|
|
</Dropdown.Item>
|
|
<Dropdown.Item
|
|
onClick={() => handleLanguageChange('en')}
|
|
className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'en' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
|
>
|
|
<GB title="English" className="!w-5 !h-auto" />
|
|
<span>English</span>
|
|
</Dropdown.Item>
|
|
</Dropdown.Menu>
|
|
}
|
|
>
|
|
<Button
|
|
icon={<IconLanguage className="text-lg" />}
|
|
aria-label={t('切换语言')}
|
|
theme="borderless"
|
|
type="tertiary"
|
|
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
|
/>
|
|
</Dropdown>
|
|
|
|
{renderUserArea()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="md:hidden">
|
|
<div
|
|
className={`
|
|
absolute top-16 left-0 right-0 bg-semi-color-bg-0
|
|
shadow-lg p-3
|
|
transform transition-all duration-300 ease-in-out
|
|
${(!isConsoleRoute && mobileMenuOpen) ? 'translate-y-0 opacity-100 visible' : '-translate-y-4 opacity-0 invisible'}
|
|
`}
|
|
>
|
|
<nav className="flex flex-col gap-1">
|
|
{renderNavLinks(true, isLoading)}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
export default HeaderBar;
|