gongxiangp2/components/Header.tsx
2026-08-19 12:08:38 +08:00

675 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.

'use client'
import Image from 'next/image'
import Link from 'next/link'
import { signOut } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { Session } from 'next-auth'
import { useState, useRef, useEffect } from 'react'
import { initializeNetwork, setupNetworkChangeDetection, addNetworkChangeListener } from '@/lib/network'
import UpdateLogModal, { useUpdateLog, showUpdateLog } from '@/components/UpdateLogModal'
import MaintenanceBanner from '@/components/MaintenanceBanner'
import HeaderTaobaoSearch from '@/components/HeaderTaobaoSearch'
import { formatHumanSpeed, formatTransferProgressSummary } from '@/lib/human-readable-transfer'
interface HeaderProps {
session: Session | null
appearance?: 'default' | 'light'
}
const DEFAULT_HEADER_LOGO_URL = '/api/favicon'
export default function Header({ session, appearance = 'default' }: HeaderProps) {
const router = useRouter()
const role = session ? String((session.user as any)?.role || '') : ''
const isAdmin = ['ADMIN', 'SYSTEM_ADMIN', 'DEPT_ADMIN'].includes(role)
const canViewPerformance = ['ADMIN', 'SYSTEM_ADMIN'].includes(role) || Boolean((session?.user as any)?.isSuperAdmin)
const [showDropdown, setShowDropdown] = useState(false)
const [isInternalNetwork, setIsInternalNetwork] = useState(false)
const [networkChecked, setNetworkChecked] = useState(false)
// 网络切换横幅已移除:主站统一使用 https://sc.jiansun.vip,旧域名 3d.yydd.ai 仅保留跳转
const [isAccessingFromExternal, setIsAccessingFromExternal] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
// 性能监控悬停卡片状态
const [showPerfCard, setShowPerfCard] = useState(false)
const [perfLoading, setPerfLoading] = useState(false)
const [perfError, setPerfError] = useState<string | null>(null)
const [perfData, setPerfData] = useState<{
cpu?: { usage: number; loadAverage: number[] }
memory?: { used: number; total: number; percentage: number }
network?: { uploadSpeed: number; downloadSpeed: number }
uploadQueue?: { total: number; processing: any[]; waiting: any[] }
transcodeQueue?: { total: number; maxConcurrent: number; processing: any[]; waiting: any[] }
} | null>(null)
const perfHoverTimerRef = useRef<number | null>(null)
const perfHideTimerRef = useRef<number | null>(null)
const perfIntervalRef = useRef<number | null>(null)
// 使用 useUpdateLog hook 来管理更新日志弹窗
const { showModal: showUpdateLogModal, currentLog, showAllHistory, handleClose: handleUpdateLogClose } = useUpdateLog()
// 系统设置
const [headerBgColor, setHeaderBgColor] = useState('#1f2937')
const [headerLogoUrl, setHeaderLogoUrl] = useState(DEFAULT_HEADER_LOGO_URL)
const [siteName, setSiteName] = useState('由壹点3D素材')
const isLight = appearance === 'light'
// 加载系统设置
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_URL)
setSiteName(settings.siteName || '由壹点3D素材')
}
} catch (error) {
console.error('加载系统设置失败:', error)
}
}
loadSettings()
}, [])
// 更新浏览器标题
useEffect(() => {
if (siteName) {
document.title = siteName
}
}, [siteName])
// 检测网络状态
useEffect(() => {
const detectNetwork = async () => {
try {
const networkStatus = await initializeNetwork()
setIsInternalNetwork(networkStatus.isInternal)
setNetworkChecked(true)
// 检查当前是否从外网访问(仅用于显示状态,不再提供切换功能)
const currentHost = window.location.hostname
const isExternal = currentHost !== '192.168.58.20' && currentHost !== 'localhost'
setIsAccessingFromExternal(isExternal)
// 网络切换横幅已移除:主站统一使用 https://sc.jiansun.vip,旧域名 3d.yydd.ai 仅保留跳转
} catch (error) {
console.error('网络检测失败:', error)
setIsInternalNetwork(false)
setNetworkChecked(true)
}
}
detectNetwork()
// 设置网络变化监听
setupNetworkChangeDetection()
// 监听网络状态变化,自动更新UI
const unsubscribe = addNetworkChangeListener((status) => {
setIsInternalNetwork(status.isInternal)
})
return () => {
unsubscribe()
}
}, [])
// 内网切换功能已移除:主站统一使用 https://sc.jiansun.vip,旧域名 3d.yydd.ai 仅保留跳转
// 点击外部关闭下拉菜单
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)
}, [])
// 性能监控:根据 showPerfCard 控制轮询
useEffect(() => {
// 清理定时器的工具函数
const clearIntervalSafe = () => {
if (perfIntervalRef.current !== null) {
window.clearInterval(perfIntervalRef.current)
perfIntervalRef.current = null
}
}
if (!canViewPerformance || !showPerfCard) {
clearIntervalSafe()
return
}
const fetchPerf = async () => {
try {
setPerfLoading(true)
setPerfError(null)
const res = await fetch('/api/performance/quick')
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = await res.json()
setPerfData(data)
} catch (err) {
console.error('加载性能数据失败:', err)
setPerfError('加载失败')
} finally {
setPerfLoading(false)
}
}
// 立即拉一次
fetchPerf()
// 每 2 秒轮询一次
perfIntervalRef.current = window.setInterval(fetchPerf, 2000)
return () => {
clearIntervalSafe()
}
}, [canViewPerformance, showPerfCard])
// 悬停事件处理:延迟显示 / 隐藏性能卡片
const handlePerfMouseEnter = () => {
if (!canViewPerformance) return
// 取消隐藏定时器
if (perfHideTimerRef.current !== null) {
window.clearTimeout(perfHideTimerRef.current)
perfHideTimerRef.current = null
}
// 300ms 后显示,防止误触
if (perfHoverTimerRef.current === null) {
perfHoverTimerRef.current = window.setTimeout(() => {
setShowPerfCard(true)
perfHoverTimerRef.current = null
}, 300)
}
}
const handlePerfMouseLeave = () => {
if (!canViewPerformance) return
// 取消显示定时器
if (perfHoverTimerRef.current !== null) {
window.clearTimeout(perfHoverTimerRef.current)
perfHoverTimerRef.current = null
}
// 100ms 延迟隐藏,避免抖动
if (perfHideTimerRef.current !== null) {
window.clearTimeout(perfHideTimerRef.current)
perfHideTimerRef.current = null
}
perfHideTimerRef.current = window.setTimeout(() => {
setShowPerfCard(false)
perfHideTimerRef.current = null
}, 100)
}
// 获取用户名首字母
const getInitials = (name: string) => {
return name?.charAt(0).toUpperCase() || 'U'
}
const formatAgo = (ms: number): string => {
if (!Number.isFinite(ms) || ms < 0) return '--'
const sec = Math.floor(ms / 1000)
if (sec < 60) return `${sec}s`
const min = Math.floor(sec / 60)
if (min < 60) return `${min}min`
const hr = Math.floor(min / 60)
return `${hr}h${min % 60}min`
}
return (
<>
{/* 系统维护通知横幅 */}
<MaintenanceBanner />
{/* 网络切换横幅已移除:主站统一使用 https://sc.jiansun.vip,旧域名 3d.yydd.ai 仅保留跳转 */}
<header
data-appearance={appearance}
className={isLight
? 'sticky top-0 z-50 mx-3 rounded-b-[26px] bg-white text-[#101828] shadow-[0_12px_34px_rgba(74,103,140,0.09)]'
: 'sticky top-0 z-50 shadow-sm'}
style={isLight ? undefined : { backgroundColor: headerBgColor }}
>
<div className="container mx-auto max-w-7xl px-3 py-3 sm:px-4 sm:py-4">
<div className="flex items-center justify-between">
{/* Logo */}
<Link href="/assets/material-library" className="flex min-w-0 items-center space-x-2 sm:space-x-3">
<Image
src={headerLogoUrl}
alt={siteName}
width={36}
height={36}
unoptimized
className="h-9 w-9 rounded-lg sm:h-10 sm:w-10"
/>
<span className={`max-w-[11rem] truncate text-base font-semibold sm:max-w-none sm:text-2xl ${isLight ? 'text-[#101828]' : 'text-white'}`}>
{siteName}
</span>
</Link>
{/* Navigation */}
<nav className="flex items-center space-x-2 sm:space-x-4">
{session && (
<div className="hidden sm:block">
<HeaderTaobaoSearch />
</div>
)}
{/* Network Status Indicator + 性能监控悬停卡片 */}
{networkChecked && (
<div
className="relative hidden sm:block"
onMouseEnter={canViewPerformance ? handlePerfMouseEnter : undefined}
onMouseLeave={canViewPerformance ? handlePerfMouseLeave : undefined}
>
<div
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all ${
isLight
? isInternalNetwork
? 'border border-emerald-200 bg-emerald-50 text-emerald-700'
: 'border border-blue-200 bg-blue-50 text-blue-700'
: 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>
{/* 性能监控悬停卡片(优化版) */}
{canViewPerformance && showPerfCard && (
<div className="absolute right-0 mt-2 w-96 bg-gray-900 text-gray-100 rounded-lg shadow-xl border border-gray-700 p-4 z-50">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold text-gray-300">系统性能</span>
{perfLoading && (
<span className="text-xs text-gray-400">加载中...</span>
)}
{perfError && !perfLoading && (
<span className="text-xs text-red-400">{perfError}</span>
)}
</div>
{/* CPU / 内存 / 网络 */}
<div className="space-y-2 mb-3">
{/* CPU */}
<div>
<div className="flex justify-between text-sm text-gray-300 mb-1">
<span>CPU</span>
<span>{perfData?.cpu?.usage ?? '--'}%</span>
</div>
<div className="w-full h-2 rounded-full bg-gray-800 overflow-hidden">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: `${Math.min(Math.max(perfData?.cpu?.usage ?? 0, 0), 100)}%` }}
/>
</div>
<div className="flex justify-between text-xs text-gray-500 mt-0.5">
<span>1/5/15 分钟负载</span>
<span>
{(perfData?.cpu?.loadAverage || []).map(v => v.toFixed(2)).join(' / ') || '--'}
</span>
</div>
</div>
{/* 内存 */}
<div>
<div className="flex justify-between text-sm text-gray-300 mb-1">
<span>内存</span>
<span>
{perfData?.memory
? `${perfData.memory.used} / ${perfData.memory.total} MB (${perfData.memory.percentage}%)`
: '--'}
</span>
</div>
<div className="w-full h-2 rounded-full bg-gray-800 overflow-hidden">
<div
className="h-full rounded-full bg-sky-500"
style={{
width: `${Math.min(Math.max(perfData?.memory?.percentage ?? 0, 0), 100)}%`,
}}
/>
</div>
</div>
{/* 服务器整体网络流量 */}
<div>
<div className="flex justify-between text-sm text-gray-300 mb-1">
<span>服务器网络</span>
<span className="text-xs text-gray-500">全站实时</span>
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs text-gray-400">
<span>入站</span>
<span>
{perfData?.network?.uploadSpeed
? formatHumanSpeed(perfData.network.uploadSpeed)
: '--'}
</span>
</div>
<div className="flex justify-between text-xs text-gray-400">
<span>出站</span>
<span>
{perfData?.network?.downloadSpeed
? formatHumanSpeed(perfData.network.downloadSpeed)
: '--'}
</span>
</div>
<div className="text-[11px] text-gray-500">
服务器整体流量,非当前浏览器测速
</div>
</div>
</div>
</div>
{/* 上传队列 / 转码队列 详细信息 */}
<div className="border-t border-gray-800 pt-3 mt-2 space-y-3">
{/* 上传队列 */}
<div>
<div className="flex justify-between text-sm text-gray-300 mb-2">
<span>上传队列</span>
<span>
{perfData?.uploadQueue
? `处理中 ${perfData.uploadQueue.processing?.length || 0} · 等待 ${perfData.uploadQueue.waiting?.length || 0}`
: '--'}
</span>
</div>
{perfData?.uploadQueue?.processing && perfData.uploadQueue.processing.length > 0 && (
<div className="space-y-1.5 max-h-32 overflow-y-auto">
{perfData.uploadQueue.processing.map((task: any, idx: number) => {
const uploadedBytes =
task.uploadedBytes || ((task.fileSize || 0) * ((task.progress || 0) / 100))
const speedText = task.currentSpeed
? formatTransferProgressSummary({
speedBytesPerSecond: task.currentSpeed,
loadedBytes: uploadedBytes,
totalBytes: task.fileSize,
})
: '速度: --'
return (
<div key={task.uploadId || idx} className="text-xs text-gray-400 bg-gray-800/50 rounded px-2.5 py-1.5">
<div className="truncate font-medium">{task.fileName || '未知文件'}</div>
<div className="flex justify-between mt-1 text-xs">
<span>{task.username || '未知用户'}</span>
<span>{task.materialType || '未知类型'}</span>
</div>
<div className="mt-1 text-xs">
<div>进度: {task.progress ?? 0}%</div>
<div>{speedText}</div>
</div>
{task.isActive === false && (
<div className="text-xs text-amber-400 mt-1">
无活动: {formatAgo(task.inactiveMs)}
</div>
)}
</div>
)
})}
</div>
)}
{perfData?.uploadQueue?.waiting && perfData.uploadQueue.waiting.length > 0 && (
<div className="space-y-1.5 max-h-32 overflow-y-auto mt-2">
{perfData.uploadQueue.waiting.map((task: any, idx: number) => (
<div key={task.uploadId || idx} className="text-xs text-gray-500 bg-gray-800/30 rounded px-2.5 py-1.5">
<div className="truncate font-medium">{task.fileName || '未知文件'}</div>
<div className="flex justify-between mt-1 text-xs">
<span>{task.username || '未知用户'}</span>
<span>{task.materialType || '未知类型'}</span>
</div>
<div className="text-xs text-gray-500 mt-1">等待中</div>
</div>
))}
</div>
)}
</div>
{/* 转码队列 */}
<div>
<div className="flex justify-between text-sm text-gray-300 mb-2">
<span>转码队列</span>
<span>
{perfData?.transcodeQueue
? `处理中 ${perfData.transcodeQueue.processing?.length || 0} / ${perfData.transcodeQueue.maxConcurrent || 0} · 等待 ${perfData.transcodeQueue.waiting?.length || 0}`
: '--'}
</span>
</div>
{perfData?.transcodeQueue?.processing && perfData.transcodeQueue.processing.length > 0 && (
<div className="space-y-1.5 max-h-32 overflow-y-auto">
{perfData.transcodeQueue.processing.map((task: any, idx: number) => {
const stageMap: Record<string, string> = {
preview: '预览视频',
clip: '片段生成',
cover: '封面提取',
}
return (
<div key={task.materialId || idx} className="text-xs text-gray-400 bg-gray-800/50 rounded px-2.5 py-1.5">
<div className="truncate font-medium">{task.fileName || '未知文件'}</div>
<div className="flex justify-between mt-1 text-xs">
<span>{task.materialType || '未知类型'} · {task.stage ? stageMap[task.stage] || task.stage : '处理中'}</span>
<span>{task.progress ?? 0}%</span>
</div>
<div className="text-xs text-gray-500 mt-1">上传人: {task.authorName || '未知用户'}</div>
</div>
)
})}
</div>
)}
{perfData?.transcodeQueue?.waiting && perfData.transcodeQueue.waiting.length > 0 && (
<div className="space-y-1.5 max-h-32 overflow-y-auto mt-2">
{perfData.transcodeQueue.waiting.map((task: any, idx: number) => (
<div key={task.materialId || idx} className="text-xs text-gray-500 bg-gray-800/30 rounded px-2.5 py-1.5">
<div className="truncate font-medium">{task.fileName || '未知文件'}</div>
<div className="flex justify-between mt-1 text-xs">
<span>{task.materialType || '未知类型'}</span>
<span>队列 #{task.queuePosition || idx + 1}</span>
</div>
<div className="text-xs text-gray-500 mt-1">上传人: {task.authorName || '未知用户'}</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
</div>
)}
{/* User Dropdown */}
{session && (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowDropdown(!showDropdown)}
aria-label="打开用户菜单"
aria-expanded={showDropdown}
className={`flex items-center rounded-lg p-1.5 transition-all ${isLight ? 'hover:bg-[#f1f6fc]' : 'hover:bg-gray-800 dark:hover:bg-gray-900'}`}
>
{/* 圆形头像 */}
{(session.user as any)?.avatar ? (
<Image
src={(session.user as any).avatar}
alt={session.user?.name || '用户'}
width={32}
height={32}
className="w-8 h-8 rounded-full border-2 border-gray-700 dark:border-gray-600 shadow-lg hover:border-gray-500 dark:hover:border-gray-400 transition-all object-cover"
unoptimized
/>
) : (
<div className={`flex h-8 w-8 items-center justify-center rounded-full border-2 bg-white text-base font-semibold text-gray-900 transition-all ${isLight ? 'border-[#dce7f3] shadow-[0_4px_12px_rgba(67,93,126,0.08)] hover:border-[#9ec3ec]' : 'border-gray-700 shadow-lg hover:border-gray-500 dark:border-gray-600 dark:bg-gray-300 dark:text-gray-800 dark:hover:border-gray-400'}`}>
{getInitials(session.user?.name || '')}
</div>
)}
</button>
{/* 下拉菜单 */}
{showDropdown && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg py-2 z-50 border border-gray-200 dark:border-gray-700">
<div className="px-4 py-2 border-b border-gray-100 dark:border-gray-700">
<p className="text-sm font-medium text-gray-900 dark:text-white">
{session.user?.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{session.user?.email}
</p>
</div>
{/* 个人中心 */}
<Link
href="/profile"
onClick={() => setShowDropdown(false)}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all flex items-center space-x-2"
>
<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>
{/* 考核调研 */}
<Link
href="/performance-survey"
onClick={() => setShowDropdown(false)}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all flex items-center space-x-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-6m4 6V7m4 10v-3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>考核调研</span>
</Link>
{/* 作品过稿看板 */}
<Link
href="/work-review"
onClick={() => setShowDropdown(false)}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all flex items-center space-x-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5a2 2 0 012-2h12a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm4 4h8M8 13h5m-5 4h3" />
</svg>
<span>作品过稿看板</span>
</Link>
{/* 更新记录 */}
<button
onClick={() => {
setShowDropdown(false)
showUpdateLog('all') // 显示所有历史记录
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all flex items-center space-x-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span>更新记录</span>
</button>
{/* 提交反馈 */}
<a
href="https://alidocs.dingtalk.com/i/nodes/NZQYprEoWoeEabj3srv6RwDkJ1waOeDk?utm_scene=person_space"
target="_blank"
rel="noopener noreferrer"
onClick={() => setShowDropdown(false)}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all flex items-center space-x-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
<span>提交反馈</span>
</a>
{/* 系统设置(管理员) */}
{isAdmin && (
<Link
href="/admin"
onClick={() => setShowDropdown(false)}
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all flex items-center space-x-2"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>系统设置</span>
</Link>
)}
{/* 退出登录 */}
<button
onClick={async () => {
setShowDropdown(false)
try {
// 使用 redirect: false 避免 NextAuth 使用 NEXTAUTH_URL 重定向
// 然后手动使用 Next.js router 重定向,这样可以完全控制重定向逻辑
await signOut({ redirect: false })
// 短暂延迟,避免浏览器扩展脚本在处理事件时出错
await new Promise(resolve => setTimeout(resolve, 100))
// 使用 router.push 进行客户端路由,自动使用当前域名
router.push('/login')
router.refresh()
} catch (error) {
console.error('退出登录失败:', error)
// 即使出错也尝试跳转
router.push('/login')
router.refresh()
}
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-all flex items-center space-x-2"
>
<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>
{/* 更新记录弹窗 */}
<UpdateLogModal
isOpen={showUpdateLogModal}
onClose={handleUpdateLogClose}
updateLog={currentLog || undefined}
showAllHistory={showAllHistory}
/>
</>
)
}