mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 00:56:37 +08:00
Refactor: Remove summary cards from various pages and update UI components
- Removed summary cards from Managed Domains, Nodes, Proxy Routes, TLS Certificates, Users pages to streamline UI. - Updated Nodes and Proxy Routes pages to enhance action buttons and descriptions. - Added new runtime configuration options for rate limiting in Settings page, allowing for dynamic updates. - Updated documentation to reflect new runtime configurations and their effects. - Enhanced unit tests for DashboardOverview to include mock data for comprehensive coverage.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
@@ -100,36 +101,40 @@ function SidebarNavItem({
|
||||
item,
|
||||
currentPath,
|
||||
isSidebarCollapsed,
|
||||
forceExpanded,
|
||||
onNavigate,
|
||||
depth = 0,
|
||||
}: {
|
||||
item: NavigationItem;
|
||||
currentPath: string;
|
||||
isSidebarCollapsed: boolean;
|
||||
forceExpanded?: boolean;
|
||||
onNavigate?: () => void;
|
||||
depth?: number;
|
||||
}) {
|
||||
const active = isNavigationItemActive(currentPath, item);
|
||||
const hasChildren = Boolean(item.children?.length);
|
||||
const showLabel = forceExpanded || !isSidebarCollapsed;
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-2xl border px-3 py-3 transition-colors',
|
||||
depth > 0 && 'ml-3 rounded-xl py-2.5',
|
||||
'flex min-h-[50px] items-center gap-3 rounded-2xl border px-3 py-2.5 transition-colors',
|
||||
depth > 0 && 'ml-3 rounded-xl',
|
||||
active
|
||||
? 'border-[var(--border-strong)] bg-[var(--accent-soft)] text-[var(--foreground-primary)]'
|
||||
: 'border-transparent text-[var(--foreground-secondary)] hover:border-[var(--border-default)] hover:bg-[var(--surface-muted)] hover:text-[var(--foreground-primary)]',
|
||||
)}
|
||||
>
|
||||
<span className='flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[var(--control-background)] text-[var(--foreground-primary)]'>
|
||||
<span className='flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-[var(--control-background)] text-[var(--foreground-primary)]'>
|
||||
<SidebarIcon icon={item.icon} />
|
||||
</span>
|
||||
{!isSidebarCollapsed ? (
|
||||
<span className='min-w-0 flex-1 text-sm font-medium'>{item.label}</span>
|
||||
) : null}
|
||||
{showLabel ? <span className='min-w-0 flex-1 text-sm font-medium'>{item.label}</span> : null}
|
||||
</Link>
|
||||
{!isSidebarCollapsed && hasChildren ? (
|
||||
{showLabel && hasChildren ? (
|
||||
<div className='space-y-2'>
|
||||
{item.children?.map((child) => (
|
||||
<SidebarNavItem
|
||||
@@ -137,6 +142,8 @@ function SidebarNavItem({
|
||||
item={child}
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
forceExpanded={forceExpanded}
|
||||
onNavigate={onNavigate}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))}
|
||||
@@ -146,50 +153,101 @@ function SidebarNavItem({
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardSidebar() {
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const isSidebarCollapsed = useAppShellStore((state) => state.isSidebarCollapsed);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'sticky top-0 hidden h-screen shrink-0 overflow-hidden border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-6 backdrop-blur lg:block',
|
||||
isSidebarCollapsed ? 'w-24' : 'w-72',
|
||||
)}
|
||||
>
|
||||
<div className='flex h-full flex-col gap-6'>
|
||||
<div className='flex items-center gap-3 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-muted)] px-3 py-3'>
|
||||
<div className='flex h-11 w-11 items-center justify-center rounded-2xl bg-[var(--brand-primary-soft)] text-lg font-semibold text-[var(--brand-primary)]'>
|
||||
AF
|
||||
</div>
|
||||
{!isSidebarCollapsed ? (
|
||||
<div>
|
||||
<p className='text-sm font-semibold text-[var(--foreground-primary)]'>ATSFlare</p>
|
||||
<p className='text-xs text-[var(--foreground-secondary)]'>控制面新版工程</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<nav className='flex-1 space-y-2'>
|
||||
<div className='flex max-h-full min-h-0 flex-col gap-2 overflow-y-auto pr-1'>
|
||||
{dashboardNavigation.map((item) => (
|
||||
<SidebarNavItem
|
||||
key={item.href}
|
||||
item={item}
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
function SidebarContent({
|
||||
currentPath,
|
||||
isSidebarCollapsed,
|
||||
forceExpanded = false,
|
||||
onNavigate,
|
||||
}: {
|
||||
currentPath: string;
|
||||
isSidebarCollapsed: boolean;
|
||||
forceExpanded?: boolean;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const showLabel = forceExpanded || !isSidebarCollapsed;
|
||||
|
||||
{!isSidebarCollapsed ? (
|
||||
<div className='rounded-2xl border border-[var(--status-success-border)] bg-[var(--status-success-soft)] px-4 py-4 text-xs leading-6 text-[var(--status-success-foreground)]'>
|
||||
新版控制台已切换为生产导向的信息架构,核心操作统一收敛到列表与弹窗。
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-5'>
|
||||
<div className='flex items-center gap-3 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-muted)] px-3 py-3'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-2xl bg-[var(--brand-primary-soft)] text-sm font-semibold text-[var(--brand-primary)]'>
|
||||
AF
|
||||
</div>
|
||||
{showLabel ? (
|
||||
<div>
|
||||
<p className='text-sm font-semibold text-[var(--foreground-primary)]'>ATSFlare</p>
|
||||
<p className='text-xs text-[var(--foreground-secondary)]'>控制面</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<nav className='flex-1 space-y-2'>
|
||||
<div className='flex max-h-full min-h-0 flex-col gap-2 overflow-y-auto pr-1'>
|
||||
{dashboardNavigation.map((item) => (
|
||||
<SidebarNavItem
|
||||
key={item.href}
|
||||
item={item}
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
forceExpanded={forceExpanded}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{showLabel ? (
|
||||
<div className='rounded-2xl border border-[var(--status-success-border)] bg-[var(--status-success-soft)] px-3 py-3 text-xs leading-6 text-[var(--status-success-foreground)]'>
|
||||
总览页现在承接各模块摘要,侧栏只保留稳定导航。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardSidebar() {
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const isSidebarCollapsed = useAppShellStore((state) => state.isSidebarCollapsed);
|
||||
const isMobileSidebarOpen = useAppShellStore((state) => state.isMobileSidebarOpen);
|
||||
const setMobileSidebarOpen = useAppShellStore((state) => state.setMobileSidebarOpen);
|
||||
|
||||
useEffect(() => {
|
||||
setMobileSidebarOpen(false);
|
||||
}, [currentPath, setMobileSidebarOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-30 bg-black/35 transition-opacity duration-200 min-[1000px]:hidden',
|
||||
isMobileSidebarOpen ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
onClick={() => setMobileSidebarOpen(false)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
|
||||
<aside
|
||||
className={cn(
|
||||
'fixed left-0 top-0 z-40 h-screen w-[200px] overflow-hidden border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-5 backdrop-blur transition-transform duration-200 min-[1000px]:hidden',
|
||||
isMobileSidebarOpen ? 'translate-x-0' : '-translate-x-full',
|
||||
)}
|
||||
>
|
||||
<SidebarContent
|
||||
currentPath={currentPath}
|
||||
isSidebarCollapsed={false}
|
||||
forceExpanded
|
||||
onNavigate={() => setMobileSidebarOpen(false)}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<aside
|
||||
className={cn(
|
||||
'sticky top-0 z-10 hidden h-screen shrink-0 overflow-hidden border-r border-[var(--border-default)] bg-[var(--surface-panel)]/95 px-3 py-5 backdrop-blur min-[1000px]:block',
|
||||
isSidebarCollapsed ? 'w-[76px]' : 'w-[200px]',
|
||||
)}
|
||||
>
|
||||
<SidebarContent currentPath={currentPath} isSidebarCollapsed={isSidebarCollapsed} />
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,73 +1,119 @@
|
||||
'use client';
|
||||
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useAuth } from '@/components/providers/auth-provider';
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
import { getCurrentNavigationItem } from '@/lib/utils/navigation';
|
||||
import { publicEnv } from '@/lib/env/public-env';
|
||||
import { useAppShellStore } from '@/store/app-shell';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function DashboardTopbar() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const currentPath = pathname ?? '/';
|
||||
const { logout, user } = useAuth();
|
||||
const toggleSidebar = useAppShellStore((state) => state.toggleSidebar);
|
||||
const currentItem = getCurrentNavigationItem(currentPath);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoggingOut(true);
|
||||
await logout();
|
||||
router.replace('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<header className='sticky top-0 z-10 border-b border-[var(--border-default)] bg-[var(--surface-panel)]/75 px-4 py-4 backdrop-blur md:px-8'>
|
||||
<div className='flex flex-col gap-4 md:flex-row md:items-center md:justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={toggleSidebar}
|
||||
className='inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] text-lg text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)]'
|
||||
aria-label='切换侧边栏'
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<div>
|
||||
<p className='text-xs uppercase tracking-[0.24em] text-[var(--foreground-secondary)]'>当前模块</p>
|
||||
<h2 className='text-lg font-semibold text-[var(--foreground-primary)]'>
|
||||
{currentItem?.label ?? 'ATSFlare 控制台'}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-3 text-sm text-[var(--foreground-secondary)]'>
|
||||
{user ? (
|
||||
<span className='hidden rounded-full border border-[var(--border-default)] px-3 py-1.5 md:inline-flex'>
|
||||
{user.display_name || user.username}
|
||||
</span>
|
||||
) : null}
|
||||
<ThemeToggle />
|
||||
<span className='rounded-full border border-[var(--border-default)] px-3 py-1.5'>
|
||||
静态导出模式
|
||||
</span>
|
||||
<span className='rounded-full border border-[var(--border-default)] px-3 py-1.5'>
|
||||
版本 {publicEnv.appVersion}
|
||||
</span>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => void handleLogout()}
|
||||
disabled={isLoggingOut}
|
||||
className='inline-flex rounded-full border border-[var(--border-default)] bg-[var(--control-background)] px-3 py-1.5 text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)] disabled:cursor-not-allowed disabled:opacity-60'
|
||||
>
|
||||
{isLoggingOut ? '退出中...' : '退出登录'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
import { publicEnv } from '@/lib/env/public-env';
|
||||
import { useAppShellStore } from '@/store/app-shell';
|
||||
|
||||
export function DashboardTopbar() {
|
||||
const router = useRouter();
|
||||
const { logout, user } = useAuth();
|
||||
const toggleSidebar = useAppShellStore((state) => state.toggleSidebar);
|
||||
const isMobileSidebarOpen = useAppShellStore((state) => state.isMobileSidebarOpen);
|
||||
const setMobileSidebarOpen = useAppShellStore((state) => state.setMobileSidebarOpen);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUserMenuOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
if (!menuRef.current?.contains(event.target as Node)) {
|
||||
setIsUserMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsUserMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', handlePointerDown);
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handlePointerDown);
|
||||
window.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [isUserMenuOpen]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoggingOut(true);
|
||||
setIsUserMenuOpen(false);
|
||||
await logout();
|
||||
router.replace('/login');
|
||||
};
|
||||
|
||||
const handleSidebarToggle = () => {
|
||||
if (window.innerWidth < 1000) {
|
||||
setMobileSidebarOpen(!isMobileSidebarOpen);
|
||||
return;
|
||||
}
|
||||
|
||||
toggleSidebar();
|
||||
};
|
||||
|
||||
return (
|
||||
<header className='sticky top-0 z-20 border-b border-[var(--border-default)] bg-[var(--surface-panel)]/88 px-4 py-4 backdrop-blur md:px-8'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSidebarToggle}
|
||||
className='inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] text-lg text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)]'
|
||||
aria-label='切换侧边栏'
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
|
||||
<div className='flex items-center gap-3 text-sm text-[var(--foreground-secondary)]'>
|
||||
<span className='hidden rounded-full border border-[var(--border-default)] px-3 py-1.5 sm:inline-flex'>
|
||||
版本 {publicEnv.appVersion}
|
||||
</span>
|
||||
<ThemeToggle />
|
||||
<div className='relative' ref={menuRef}>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setIsUserMenuOpen((value) => !value)}
|
||||
className='inline-flex h-11 items-center gap-2 rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] px-3 text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)]'
|
||||
aria-expanded={isUserMenuOpen}
|
||||
aria-haspopup='menu'
|
||||
>
|
||||
<span className='inline-flex h-7 w-7 items-center justify-center rounded-full bg-[var(--accent-soft)] text-xs font-semibold'>
|
||||
{(user?.display_name || user?.username || 'U').slice(0, 1).toUpperCase()}
|
||||
</span>
|
||||
<span className='hidden sm:inline'>{user?.display_name || user?.username || '用户'}</span>
|
||||
</button>
|
||||
|
||||
{isUserMenuOpen ? (
|
||||
<div className='absolute right-0 top-[calc(100%+0.5rem)] w-52 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-panel)] p-2 shadow-[var(--shadow-lg)]'>
|
||||
<div className='rounded-xl px-3 py-2'>
|
||||
<p className='text-sm font-semibold text-[var(--foreground-primary)]'>
|
||||
{user?.display_name || user?.username || '用户'}
|
||||
</p>
|
||||
{user?.username ? (
|
||||
<p className='mt-1 text-xs text-[var(--foreground-secondary)]'>@{user.username}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => void handleLogout()}
|
||||
disabled={isLoggingOut}
|
||||
className='flex w-full items-center rounded-xl px-3 py-2 text-left text-sm text-[var(--status-danger-foreground)] transition hover:bg-[var(--status-danger-soft)] disabled:cursor-not-allowed disabled:opacity-60'
|
||||
>
|
||||
{isLoggingOut ? '退出中...' : '退出登录'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,55 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import type { ThemeMode } from '@/lib/theme/theme';
|
||||
import { useTheme } from '@/components/providers/theme-provider';
|
||||
|
||||
const themeOptions: Array<{ value: ThemeMode; label: string }> = [
|
||||
{ value: 'light', label: '浅色' },
|
||||
{ value: 'system', label: '跟随系统' },
|
||||
{ value: 'dark', label: '深色' },
|
||||
];
|
||||
|
||||
interface ThemeToggleProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
const { themeMode, resolvedTheme, setThemeMode } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded-full border border-[var(--border-default)] bg-[var(--control-background)] p-1 text-xs text-[var(--foreground-secondary)]',
|
||||
className,
|
||||
)}
|
||||
aria-label='主题切换'
|
||||
>
|
||||
{themeOptions.map((option) => {
|
||||
const active = themeMode === option.value;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type='button'
|
||||
onClick={() => setThemeMode(option.value)}
|
||||
className={cn(
|
||||
'rounded-full px-3 py-1.5 transition-colors',
|
||||
active
|
||||
? 'bg-[var(--accent-soft)] text-[var(--foreground-primary)]'
|
||||
: 'hover:bg-[var(--control-background-hover)] hover:text-[var(--foreground-primary)]',
|
||||
)}
|
||||
aria-pressed={active}
|
||||
title={
|
||||
option.value === 'system'
|
||||
? `当前跟随系统(${resolvedTheme === 'dark' ? '深色' : '浅色'})`
|
||||
: `切换到${option.label}`
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { themeModes, type ThemeMode } from '@/lib/theme/theme';
|
||||
import { useTheme } from '@/components/providers/theme-provider';
|
||||
|
||||
interface ThemeToggleProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
const { themeMode, resolvedTheme, setThemeMode } = useTheme();
|
||||
const currentIndex = themeModes.indexOf(themeMode);
|
||||
const nextTheme = themeModes[(currentIndex + 1) % themeModes.length] as ThemeMode;
|
||||
|
||||
const icon =
|
||||
themeMode === 'light' ? (
|
||||
<svg
|
||||
className='h-5 w-5'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.8'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<circle cx='12' cy='12' r='4' />
|
||||
<path d='M12 2.5v2.2M12 19.3v2.2M21.5 12h-2.2M4.7 12H2.5M18.7 5.3l-1.6 1.6M6.9 17.1l-1.6 1.6M18.7 18.7l-1.6-1.6M6.9 6.9 5.3 5.3' />
|
||||
</svg>
|
||||
) : themeMode === 'dark' ? (
|
||||
<svg
|
||||
className='h-5 w-5'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.8'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<path d='M20 15.2A7.8 7.8 0 1 1 8.8 4 6.4 6.4 0 0 0 20 15.2Z' />
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className='h-5 w-5'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.8'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<rect x='3.5' y='4' width='17' height='12' rx='2.5' />
|
||||
<path d='M8 20h8M12 16v4' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const label =
|
||||
themeMode === 'light' ? '浅色模式' : themeMode === 'dark' ? '深色模式' : '跟随系统';
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setThemeMode(nextTheme)}
|
||||
className={cn(
|
||||
'inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] text-[var(--foreground-primary)] transition hover:bg-[var(--control-background-hover)]',
|
||||
className,
|
||||
)}
|
||||
aria-label={`当前${label},点击切换到${nextTheme === 'light' ? '浅色模式' : nextTheme === 'dark' ? '深色模式' : '跟随系统'}`}
|
||||
title={`${label}(当前生效:${resolvedTheme === 'dark' ? '深色' : '浅色'}),点击切换到${nextTheme === 'light' ? '浅色模式' : nextTheme === 'dark' ? '深色模式' : '跟随系统'}`}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user