flnxi-Wu/components/Header.tsx
WuYongJie 542c7bcc49 feat: 优化自定义上传模板系统
- 修复内置模板验证问题(builtin-video-material等)
- 添加模板验证工具 lib/template-validator.ts
- 创建模板系统优化需求文档(30个详细需求)
- 优化上传API的模板验证逻辑
- 改进错误提示信息的清晰度
- 添加字段池和分类管理相关需求
- 完善自定义字段的创建、验证、存储逻辑
- 清理过期的 .specstory 和 .trae 文件
2025-12-13 14:26:43 +08:00

755 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Header 组件
*
* 全局顶部导航栏组件
* 包含Logo、主题切换、用户下拉菜单等功能
* 使用三层架构样式系统,支持深浅色主题
*
* @created 2025-01-XX
* @component
*/
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { signOut } from 'next-auth/react'
import { Session } from 'next-auth'
import { useState, useRef, useEffect, useCallback } from 'react'
import { initializeNetwork } from '@/lib/network'
import { useThemeStyles, cn } from '@/lib/ui-styles'
import Avatar from '@/components/ui/Avatar'
interface HeaderCompactModeProps {
isScrolled: boolean
searchKeyword: string
onSearchChange: (value: string) => void
onOpenFilter: () => void
onOpenUpload: () => void
categoryItems: Array<{ key: string; label: string }>
selectedCategoryKey: string
onSelectCategory: (key: string) => void
themeColor: string
}
interface HeaderProps {
session: Session | null
compactMode?: HeaderCompactModeProps
}
const THEME_STORAGE_KEY = 'admin-theme-preference'
// 默认导航栏 LOGO:优先使用构建时环境变量,否则使用内置静态资源
const DEFAULT_HEADER_LOGO =
process.env.NEXT_PUBLIC_DEFAULT_HEADER_LOGO_URL || '/logo/logo1.png'
export default function Header({ session, compactMode }: HeaderProps) {
const styles = useThemeStyles()
const isAdmin = session ? ((session.user as any)?.role === 'ADMIN' || (session.user as any)?.isSuperAdmin) : false
const isSuperAdmin = session ? Boolean((session.user as any)?.isSuperAdmin) : false
const [showDropdown, setShowDropdown] = useState(false)
const [isInternalNetwork, setIsInternalNetwork] = useState(false)
const [networkChecked, setNetworkChecked] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const [showCategoryMenu, setShowCategoryMenu] = useState(false)
// 系统设置
const [headerBgColor, setHeaderBgColor] = useState('#1f2937')
const [headerLogoUrl, setHeaderLogoUrl] = useState(DEFAULT_HEADER_LOGO)
const [siteName, setSiteName] = useState('FLNXI素材库')
// Logo 加载状态
const [logoError, setLogoError] = useState(false)
const [logoRetryCount, setLogoRetryCount] = useState(0)
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const [themeReady, setThemeReady] = useState(false)
const [isHeaderDarkBg, setIsHeaderDarkBg] = useState(false)
const fallbackHeaderBg = theme === 'dark' ? 'rgba(15,23,42,0.9)' : 'rgba(255,255,255,0.9)'
const parseColorToRGB = useCallback((color: string): { r: number; g: number; b: number } | null => {
if (!color) return null
const trimmed = color.trim()
const hexMatch = trimmed.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)
if (hexMatch) {
let hex = hexMatch[1]
if (hex.length === 3) {
hex = hex.split('').map((ch) => ch + ch).join('')
}
const num = parseInt(hex, 16)
return {
r: (num >> 16) & 255,
g: (num >> 8) & 255,
b: num & 255,
}
}
const rgbMatch = trimmed.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i)
if (rgbMatch) {
return {
r: parseInt(rgbMatch[1], 10),
g: parseInt(rgbMatch[2], 10),
b: parseInt(rgbMatch[3], 10),
}
}
return null
}, [])
const isColorDark = useCallback((color: string) => {
const rgb = parseColorToRGB(color)
if (!rgb) {
return theme === 'dark'
}
const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255
return luminance < 0.55
}, [parseColorToRGB, theme])
const applyTheme = useCallback((nextTheme: 'light' | 'dark') => {
if (typeof document === 'undefined') return
const root = document.documentElement
if (nextTheme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
root.dataset.theme = nextTheme
try {
window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme)
} catch {
// ignore storage errors
}
}, [])
useEffect(() => {
if (typeof window === 'undefined') return
let initial: 'light' | 'dark' = 'light'
try {
const stored = window.localStorage.getItem(THEME_STORAGE_KEY)
if (stored === 'light' || stored === 'dark') {
initial = stored
} else {
initial = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
} catch {
initial = 'light'
}
setTheme(initial)
applyTheme(initial)
setThemeReady(true)
}, [applyTheme])
useEffect(() => {
if (!headerLogoUrl) return
// 对于以 / 开头的相对路径(如 /logo/logo2.png),直接认为是站内静态资源,不做 URL 解析
if (headerLogoUrl.startsWith('/')) {
setLogoError(false)
return
}
try {
const parsed = new URL(headerLogoUrl)
const host = parsed.hostname.toLowerCase()
const hostIsLocal = host === 'localhost' || host === '127.0.0.1'
if (hostIsLocal && typeof window !== 'undefined') {
const currentHost = window.location.hostname.toLowerCase()
if (currentHost !== host) {
console.warn(
`[Header] Logo 地址 ${headerLogoUrl} 指向本地服务,但当前访问域名为 ${currentHost},自动降级为占位图`
)
setLogoError(true)
}
}
} catch (error) {
console.warn('[Header] Logo 地址解析失败,自动降级为占位图:', headerLogoUrl, error)
setLogoError(true)
}
}, [headerLogoUrl])
// 加载系统设置
useEffect(() => {
const loadSettings = async () => {
try {
const response = await fetch('/api/system-settings')
if (response.ok) {
const settings = await response.json()
setHeaderBgColor(settings.headerBgColor || '#1f2937')
setHeaderLogoUrl(settings.headerLogoUrl || DEFAULT_HEADER_LOGO)
setSiteName(settings.siteName || 'FLNXI素材库')
}
} catch (error) {
console.error('加载系统设置失败:', error)
}
}
loadSettings()
}, [])
// 更新浏览器标题
useEffect(() => {
if (siteName) {
document.title = siteName
}
}, [siteName])
useEffect(() => {
const detectionColor = headerBgColor || (theme === 'dark' ? '#0f172a' : '#ffffff')
setIsHeaderDarkBg(isColorDark(detectionColor))
}, [headerBgColor, theme, isColorDark])
// 检测网络状态(仅用于右上角「内网 / 公网」状态展示,不再弹出横幅)
useEffect(() => {
const detectNetwork = async () => {
try {
const networkStatus = await initializeNetwork()
setIsInternalNetwork(networkStatus.isInternal)
setNetworkChecked(true)
} catch (error) {
console.error('网络检测失败:', error)
setIsInternalNetwork(false)
setNetworkChecked(true)
}
}
detectNetwork()
}, [])
// 点击外部关闭下拉菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowDropdown(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const headerPrimaryTextClass = isHeaderDarkBg ? 'text-white' : 'text-slate-900'
const compactActive = Boolean(compactMode?.isScrolled)
const compactCategoryItems = compactMode?.categoryItems || []
const compactSelectedCategory =
compactCategoryItems.find(
(item: HeaderCompactModeProps['categoryItems'][number]) => item.key === compactMode?.selectedCategoryKey
) || compactCategoryItems[0] || null
return (
<>
<header
className={cn(
'sticky z-50 shadow-sm',
'top-0',
'backdrop-blur-xl border-b border-white/20 dark:border-slate-700/30',
'transition-all duration-300'
)}
style={{
backgroundColor: headerBgColor
? `${headerBgColor}CC` // 叠加透明度,实现毛玻璃效果
: fallbackHeaderBg
}}
>
<div className="container mx-auto px-4 py-3 max-w-7xl">
<div className="flex items-center justify-between gap-6 h-12">
{/* Logo + 首页收缩模式下的分类与搜索卡片 */}
<div className="flex items-center gap-4 flex-1 min-w-0 h-full">
<Link href="/" className="flex items-center space-x-3 flex-shrink-0 h-full">
{headerLogoUrl && !logoError ? (
<Image
key={logoRetryCount}
src={headerLogoUrl + (logoRetryCount > 0 ? `?retry=${logoRetryCount}` : '')}
alt={siteName}
width={40}
height={40}
unoptimized
className="rounded-lg"
onError={(e) => {
try {
const img = e.target as HTMLImageElement
if (!img) return
// 检查是否是 localhost 地址但当前访问域名不是 localhost
const currentHost = typeof window !== 'undefined' ? window.location.hostname : ''
const checkLogoUrl = (url: string, host: string) => {
try {
const logoHost = new URL(url).hostname
return (logoHost === 'localhost' || logoHost === '127.0.0.1') && host !== 'localhost' && host !== '127.0.0.1'
} catch {
return true // 如果 URL 无效,视为错误
}
}
if (checkLogoUrl(headerLogoUrl, currentHost)) {
console.warn(
`[Header] Logo 地址 ${headerLogoUrl} 指向本地服务,但当前访问域名为 ${currentHost},自动降级为占位图`
)
setLogoError(true)
if (img) img.style.display = 'none'
return
}
setLogoRetryCount((prevCount) => {
const newCount = prevCount + 1
if (newCount <= 3) {
// 重试加载(最多3次)
console.warn(`Logo 加载失败,正在重试 (${newCount}/3):`, headerLogoUrl)
setTimeout(() => {
if (img && img.parentNode) {
img.src = headerLogoUrl + `?retry=${newCount}`
}
}, 1000 * newCount) // 递增延迟:1s, 2s, 3s
return newCount
} else {
// 重试失败,记录错误并显示占位符
console.error('Logo 加载失败,已重试3次:', headerLogoUrl)
setLogoError(true)
if (img) img.style.display = 'none'
return prevCount
}
})
} catch (error) {
// 捕获所有错误,防止崩溃
console.error('[Header] Logo 错误处理异常:', error)
setLogoError(true)
}
}}
/>
) : (
<div
className={cn(
'w-10 h-10 rounded-lg flex items-center justify-center',
isHeaderDarkBg ? 'bg-white/15' : 'bg-slate-900/10'
)}
>
<span
className={cn(
'text-lg font-bold',
headerPrimaryTextClass
)}
>
{siteName.charAt(0)}
</span>
</div>
)}
<span
className={cn(
'text-2xl font-semibold transition-colors',
headerPrimaryTextClass
)}
>
{siteName}
</span>
</Link>
{/* 首页收缩模式:分类 + 搜索卡片(含筛选与上传) */}
{compactActive && compactMode && (
<div className="flex items-center gap-3 flex-1 min-w-0 max-w-xl h-full animate-fadeInSlideDown">
{/* 分类 三 */}
<div
className="relative flex-shrink-0 select-none h-full flex items-center"
onMouseEnter={() => setShowCategoryMenu(true)}
onMouseLeave={() => setShowCategoryMenu(false)}
>
<button
type="button"
className={cn(
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm font-medium transition-all duration-200 h-8',
isHeaderDarkBg
? 'text-white/90'
: 'text-slate-800'
)}
>
<span>分类</span>
<span className="inline-flex items-center justify-center w-4 h-4">
<svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="none" stroke="currentColor">
<path strokeLinecap="round" strokeWidth={1.6} d="M4 6h12M4 10h12M4 14h12" />
</svg>
</span>
</button>
{showCategoryMenu && compactCategoryItems.length > 0 && (
<div
className={cn(
'absolute top-full left-0 mt-0 w-40 md:w-44 pt-1.5 pb-1.5 z-50',
'rounded-xl bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl',
'border border-white/40 dark:border-slate-700/70',
'shadow-[0_18px_45px_rgba(15,23,42,0.35)]',
'origin-top-left',
'animate-slide-in-from-top-2'
)}
onMouseEnter={() => setShowCategoryMenu(true)}
onMouseLeave={() => setShowCategoryMenu(false)}
style={{
transformOrigin: 'top left'
}}
>
{compactCategoryItems.map((item: HeaderCompactModeProps['categoryItems'][number]) => (
<button
key={item.key}
type="button"
onClick={() => {
compactMode.onSelectCategory(item.key)
setShowCategoryMenu(false)
}}
className={cn(
'w-full px-4 py-2 text-left text-sm transition-all duration-150',
'text-slate-800 dark:text-slate-100',
'hover:bg-slate-100/80 dark:hover:bg-slate-800/80',
'active:scale-[0.98]'
)}
>
{item.label}
</button>
))}
</div>
)}
</div>
{/* 搜索卡片:类别文字 + 输入框 + 筛选图标 + 上传按钮(集成在尾部) */}
<div
className={cn(
'flex items-center gap-0 flex-1 min-w-0 h-9',
'rounded-lg overflow-hidden',
'bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl',
'border border-white/40 dark:border-slate-700/70',
'shadow-[0_14px_35px_rgba(15,23,42,0.30)]'
)}
>
{/* 左侧内容区域 */}
<div className="flex items-center gap-1.5 flex-1 min-w-0 px-2.5 h-full">
{/* 当前分类标签 */}
{compactSelectedCategory && (
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-slate-100/80 dark:bg-slate-800/80 text-xs font-medium text-slate-600 dark:text-slate-200 whitespace-nowrap h-5">
<span>{compactSelectedCategory.label}</span>
</div>
)}
{/* 搜索输入 */}
<div className="relative flex-1 min-w-0 h-full flex items-center">
<input
type="text"
value={compactMode.searchKeyword}
onChange={(e) => compactMode.onSearchChange(e.target.value)}
placeholder="搜索SPU、店铺、场景类别、作者..."
className={cn(
'w-full bg-transparent border-none outline-none text-sm text-slate-900 dark:text-slate-50 placeholder:text-slate-400 dark:placeholder:text-slate-500',
'pl-5 pr-10 h-full'
)}
/>
<svg
className="absolute left-1 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
{/* 右侧筛选图标按钮(内嵌在输入框尾部区域,只显示图标) */}
<button
type="button"
onClick={compactMode.onOpenFilter}
className="absolute right-1.5 top-1/2 -translate-y-1/2 inline-flex items-center justify-center w-6 h-6 rounded border border-green-500/60 bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-green-50 dark:hover:bg-green-900/20 hover:border-green-500 transition-colors"
title="筛选"
aria-label="筛选"
>
<svg className="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 4h18l-7 8v6l-4 2v-8L3 4z"
/>
</svg>
</button>
</div>
</div>
{/* 上传素材按钮(直接集成在卡片尾部,作为卡片的一部分) */}
<button
type="button"
onClick={compactMode.onOpenUpload}
className={cn(
'flex items-center justify-center px-3 h-9 rounded-r-lg text-xs font-medium whitespace-nowrap',
'border-l border-slate-300 dark:border-slate-600',
'bg-indigo-50 dark:bg-indigo-900/20',
'text-indigo-600 dark:text-indigo-400',
'hover:bg-indigo-100 dark:hover:bg-indigo-900/30',
'active:scale-[0.98] transition-all'
)}
>
上传素材
</button>
</div>
</div>
)}
</div>
{/* Navigation */}
<nav className="flex items-center space-x-4">
{/* Network Status Indicator */}
{networkChecked && (
<div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all ${
isInternalNetwork
? 'bg-green-500/10 text-green-400 border border-green-500/20'
: 'bg-blue-500/10 text-blue-400 border border-blue-500/20'
}`}>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<circle cx="10" cy="10" r="4"/>
</svg>
<span>{isInternalNetwork ? '内网' : '公网'}</span>
</div>
)}
{/* Theme toggle */}
<div className="flex items-center">
{themeReady ? (
<button
type="button"
onClick={() => {
const next = theme === 'dark' ? 'light' : 'dark'
setTheme(next)
applyTheme(next)
}}
aria-label={`切换为${theme === 'dark' ? '浅色' : '深色'}模式`}
aria-pressed={theme === 'dark'}
className={cn(
"relative flex items-center px-2 py-1 rounded-full border shadow-[0_5px_14px_rgba(15,23,42,0.25)] backdrop-blur focus:outline-none focus-visible:outline-none focus-visible:ring-0 transition-all w-16 h-8 overflow-hidden",
isHeaderDarkBg
? 'border-white/25 bg-white/15'
: 'border-slate-300 bg-white/80'
)}
>
<div className="absolute inset-0 flex items-center justify-between px-2">
<svg
className={`w-4 h-4 transition-colors ${theme === 'light' ? 'text-amber-300' : 'text-white/35'}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.6} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg
className={`w-4 h-4 transition-colors ${theme === 'dark' ? 'text-indigo-200' : 'text-white/35'}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.6} d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
</svg>
</div>
<span
className={`absolute top-1 left-1 w-6 h-6 rounded-full bg-white shadow-lg transition-transform duration-300 ${
theme === 'dark' ? 'translate-x-7' : 'translate-x-0'
}`}
>
<span className="sr-only">{theme === 'dark' ? '深色模式' : '浅色模式'}</span>
<span className="relative block w-full h-full text-slate-700">
<svg
className={`absolute inset-0 m-auto w-3.5 h-3.5 transition-opacity ${theme === 'light' ? 'opacity-100 text-amber-500' : 'opacity-0'}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.6} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg
className={`absolute inset-0 m-auto w-3.5 h-3.5 transition-opacity ${theme === 'dark' ? 'opacity-100 text-indigo-400' : 'opacity-0'}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.6} d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
</svg>
</span>
</span>
</button>
) : (
<div className="w-16 h-8 rounded-full border border-white/20 bg-white/10 animate-pulse" />
)}
</div>
{/* User Dropdown */}
{session && (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowDropdown(!showDropdown)}
aria-label="打开用户菜单"
className={cn(
'flex items-center justify-center rounded-full transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40',
isHeaderDarkBg ? 'hover:bg-white/15 p-1' : 'hover:bg-slate-900/5 p-1'
)}
>
{/* 使用 Avatar 组件 */}
<Avatar
src={(session.user as any)?.avatar || undefined}
name={session.user?.name || ''}
size="sm"
onClick={() => setShowDropdown(!showDropdown)}
/>
</button>
{/* 下拉菜单 - 使用三层架构样式 */}
{showDropdown && (
<div className={cn(
'absolute right-0 mt-2 w-48 py-2 z-50 overflow-hidden',
styles.cardStyles.base
)}>
<div className={cn('px-4 py-2 border-b', 'border-slate-200/70 dark:border-slate-700/70')}>
<p className="text-sm font-medium text-slate-900 dark:text-white">
{session.user?.name}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
{session.user?.email}
</p>
</div>
{/* 个人中心 */}
<Link
href="/profile"
onClick={() => setShowDropdown(false)}
className={cn(
'w-full text-left px-4 py-2 text-sm transition-all flex items-center space-x-2',
'text-slate-700 dark:text-slate-200',
'hover:bg-slate-100 dark:hover:bg-slate-700'
)}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span>个人中心</span>
</Link>
{isAdmin && (
<>
<Link
href="/admin"
onClick={() => setShowDropdown(false)}
className={cn(
'w-full text-left px-4 py-2 text-sm transition-all flex items-center space-x-2',
'text-slate-700 dark:text-slate-200',
'hover:bg-slate-100 dark:hover:bg-slate-700'
)}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
/>
</svg>
<span>企业管理</span>
</Link>
{isAdmin && (
<Link
href="/admin/settings"
onClick={() => setShowDropdown(false)}
className={cn(
'w-full text-left px-4 py-2 text-sm transition-all flex items-center space-x-2',
'text-slate-700 dark:text-slate-200',
'hover:bg-slate-100 dark:hover:bg-slate-700'
)}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
/>
</svg>
<span>系统设置</span>
</Link>
)}
</>
)}
<Link
href="/admin/monitor"
onClick={() => setShowDropdown(false)}
className={cn(
'w-full text-left px-4 py-2 text-sm transition-all flex items-center space-x-2',
'text-slate-700 dark:text-slate-200',
'hover:bg-slate-100 dark:hover:bg-slate-700'
)}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 10h4l3 8 4-16 3 8h4"
/>
</svg>
<span>监控仪表板</span>
</Link>
{/* 退出登录 */}
<button
onClick={async () => {
setShowDropdown(false)
// 先退出登录,然后手动跳转到登录页
await signOut({ redirect: false })
window.location.href = '/login'
}}
className={cn(
'w-full text-left px-4 py-2 text-sm transition-all flex items-center space-x-2',
'text-slate-700 dark:text-slate-200',
'hover:bg-slate-100 dark:hover:bg-slate-700'
)}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
<span>退出登录</span>
</button>
</div>
)}
</div>
)}
</nav>
</div>
</div>
</header>
</>
)
}