774 lines
37 KiB
TypeScript
774 lines
37 KiB
TypeScript
'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 NetworkDiagnoseModal from '@/components/NetworkDiagnoseModal'
|
||
import MaintenanceBanner from '@/components/MaintenanceBanner'
|
||
|
||
interface HeaderProps {
|
||
session: Session | null
|
||
}
|
||
|
||
export default function Header({ session }: HeaderProps) {
|
||
const router = useRouter()
|
||
const isAdmin = session ? (session.user as any)?.role === 'ADMIN' : false
|
||
const [showDropdown, setShowDropdown] = useState(false)
|
||
const [isInternalNetwork, setIsInternalNetwork] = useState(false)
|
||
const [networkChecked, setNetworkChecked] = useState(false)
|
||
// 网络切换横幅已移除:现在统一使用 https://3d.yydd.ai,通过DNS解析自动区分内网/外网
|
||
const [isAccessingFromExternal, setIsAccessingFromExternal] = useState(false)
|
||
const [showNetworkDiagnose, setShowNetworkDiagnose] = 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('http://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png')
|
||
const [siteName, setSiteName] = useState('由壹点3D素材')
|
||
|
||
// 加载系统设置
|
||
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 || 'http://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png')
|
||
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://3d.yydd.ai,通过DNS解析自动区分内网/外网
|
||
} catch (error) {
|
||
console.error('网络检测失败:', error)
|
||
setIsInternalNetwork(false)
|
||
setNetworkChecked(true)
|
||
}
|
||
}
|
||
detectNetwork()
|
||
|
||
// 设置网络变化监听
|
||
setupNetworkChangeDetection()
|
||
|
||
// 监听网络状态变化,自动更新UI
|
||
const unsubscribe = addNetworkChangeListener((status) => {
|
||
setIsInternalNetwork(status.isInternal)
|
||
})
|
||
|
||
return () => {
|
||
unsubscribe()
|
||
}
|
||
}, [])
|
||
|
||
// 内网切换功能已移除:现在统一使用 https://3d.yydd.ai,通过DNS解析自动区分内网/外网
|
||
|
||
// 点击外部关闭下拉菜单
|
||
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 (!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()
|
||
}
|
||
}, [showPerfCard])
|
||
|
||
// 悬停事件处理:延迟显示 / 隐藏性能卡片
|
||
const handlePerfMouseEnter = () => {
|
||
// 取消隐藏定时器
|
||
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 (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 formatSpeed = (bytesPerSecond: number): string => {
|
||
// 转换为比特/秒(网络带宽标准单位)
|
||
// 1 MB/s = 8 Mbps (1字节 = 8比特)
|
||
const bitsPerSecond = bytesPerSecond * 8
|
||
|
||
if (bitsPerSecond < 1000) {
|
||
return `${bitsPerSecond.toFixed(0)} bps`
|
||
}
|
||
if (bitsPerSecond < 1000 * 1000) {
|
||
return `${(bitsPerSecond / 1000).toFixed(2)} Kbps`
|
||
}
|
||
return `${(bitsPerSecond / 1000 / 1000).toFixed(2)} Mbps`
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{/* 系统维护通知横幅 */}
|
||
<MaintenanceBanner />
|
||
|
||
{/* 网络切换横幅已移除:现在统一使用 https://3d.yydd.ai,通过DNS解析自动区分内网/外网 */}
|
||
|
||
<header
|
||
className="sticky top-0 z-50 shadow-sm"
|
||
style={{ backgroundColor: headerBgColor }}
|
||
>
|
||
<div className="container mx-auto px-4 py-4 max-w-7xl">
|
||
<div className="flex items-center justify-between">
|
||
{/* Logo */}
|
||
<Link href="/" className="flex items-center space-x-3">
|
||
<Image
|
||
src={headerLogoUrl}
|
||
alt={siteName}
|
||
width={40}
|
||
height={40}
|
||
unoptimized
|
||
className="rounded-lg"
|
||
/>
|
||
<span className="text-2xl font-semibold text-white">
|
||
{siteName}
|
||
</span>
|
||
</Link>
|
||
|
||
{/* Navigation */}
|
||
<nav className="flex items-center space-x-4">
|
||
{/* Network Status Indicator + 性能监控悬停卡片 */}
|
||
{networkChecked && (
|
||
<div className="relative" onMouseEnter={handlePerfMouseEnter} onMouseLeave={handlePerfMouseLeave}>
|
||
<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>
|
||
|
||
{/* 性能监控悬停卡片(优化版) */}
|
||
{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>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<div className="flex justify-between text-xs text-gray-400">
|
||
<span>上传</span>
|
||
<span>
|
||
{perfData?.network?.uploadSpeed
|
||
? formatSpeed(perfData.network.uploadSpeed)
|
||
: '--'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-xs text-gray-400">
|
||
<span>下载</span>
|
||
<span>
|
||
{perfData?.network?.downloadSpeed
|
||
? formatSpeed(perfData.network.downloadSpeed)
|
||
: '--'}
|
||
</span>
|
||
</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) => (
|
||
<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="flex justify-between mt-1 text-xs">
|
||
<span>进度: {task.progress ?? 0}%</span>
|
||
</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)}
|
||
className="flex items-center p-1.5 rounded-lg hover:bg-gray-800 dark:hover:bg-gray-900 transition-all"
|
||
>
|
||
{/* 圆形头像 */}
|
||
{(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="w-8 h-8 rounded-full bg-white dark:bg-gray-300 border-2 border-gray-700 dark:border-gray-600 flex items-center justify-center text-gray-900 dark:text-gray-800 font-semibold text-base shadow-lg hover:border-gray-500 dark:hover:border-gray-400 transition-all">
|
||
{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>
|
||
|
||
{/* 更新记录 */}
|
||
<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={() => {
|
||
setShowDropdown(false)
|
||
setShowNetworkDiagnose(true)
|
||
}}
|
||
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 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||
</svg>
|
||
<span>网络诊断</span>
|
||
</button>
|
||
|
||
{/* 清除缓存(仅清除素材网相关缓存) */}
|
||
<button
|
||
onClick={async () => {
|
||
setShowDropdown(false)
|
||
try {
|
||
const currentOrigin = window.location.origin
|
||
const currentHostname = window.location.hostname
|
||
|
||
// 1. 清除Cache API(仅清除当前域名的缓存)
|
||
if ('caches' in window) {
|
||
const cacheNames = await caches.keys()
|
||
// 只清除与当前域名相关的缓存
|
||
const materialCacheNames = cacheNames.filter(name => {
|
||
// Next.js缓存通常包含域名信息,或者以特定前缀命名
|
||
return name.includes(currentHostname) ||
|
||
name.includes('nextjs') ||
|
||
name.includes('_next') ||
|
||
name.startsWith('workbox-') ||
|
||
name.includes('gongxiang') ||
|
||
name.includes('material')
|
||
})
|
||
await Promise.all(materialCacheNames.map(name => caches.delete(name)))
|
||
console.log(`已清除 ${materialCacheNames.length} 个缓存:`, materialCacheNames)
|
||
}
|
||
|
||
// 2. 清除Service Worker(仅清除当前域名的)
|
||
if ('serviceWorker' in navigator) {
|
||
const registrations = await navigator.serviceWorker.getRegistrations()
|
||
// 只清除当前域名的Service Worker
|
||
const materialRegistrations = registrations.filter(reg => {
|
||
return reg.scope.startsWith(currentOrigin)
|
||
})
|
||
await Promise.all(materialRegistrations.map(reg => reg.unregister()))
|
||
console.log(`已清除 ${materialRegistrations.length} 个Service Worker`)
|
||
}
|
||
|
||
// 3. 清除localStorage(仅清除素材网相关的key)
|
||
try {
|
||
const keysToRemove: string[] = []
|
||
const materialKeyPatterns = [
|
||
'gongxiang',
|
||
'material',
|
||
'next-auth',
|
||
'nextjs',
|
||
'_next',
|
||
'youyidian',
|
||
'3d-material'
|
||
]
|
||
|
||
for (let i = 0; i < localStorage.length; i++) {
|
||
const key = localStorage.key(i)
|
||
if (key && materialKeyPatterns.some(pattern => key.toLowerCase().includes(pattern.toLowerCase()))) {
|
||
keysToRemove.push(key)
|
||
}
|
||
}
|
||
keysToRemove.forEach(key => localStorage.removeItem(key))
|
||
console.log(`已清除 ${keysToRemove.length} 个localStorage项:`, keysToRemove)
|
||
} catch (e) {
|
||
console.warn('清除localStorage失败:', e)
|
||
}
|
||
|
||
// 4. 清除sessionStorage(仅清除素材网相关的key)
|
||
try {
|
||
const keysToRemove: string[] = []
|
||
const materialKeyPatterns = [
|
||
'gongxiang',
|
||
'material',
|
||
'next-auth',
|
||
'nextjs',
|
||
'_next',
|
||
'youyidian',
|
||
'3d-material'
|
||
]
|
||
|
||
for (let i = 0; i < sessionStorage.length; i++) {
|
||
const key = sessionStorage.key(i)
|
||
if (key && materialKeyPatterns.some(pattern => key.toLowerCase().includes(pattern.toLowerCase()))) {
|
||
keysToRemove.push(key)
|
||
}
|
||
}
|
||
keysToRemove.forEach(key => sessionStorage.removeItem(key))
|
||
console.log(`已清除 ${keysToRemove.length} 个sessionStorage项:`, keysToRemove)
|
||
} catch (e) {
|
||
console.warn('清除sessionStorage失败:', e)
|
||
}
|
||
|
||
// 5. 清除IndexedDB(仅清除素材网相关的数据库)
|
||
try {
|
||
if ('indexedDB' in window) {
|
||
const databases = await indexedDB.databases()
|
||
const materialDbPatterns = [
|
||
'gongxiang',
|
||
'material',
|
||
'next-auth',
|
||
'nextjs',
|
||
'youyidian',
|
||
'3d-material'
|
||
]
|
||
|
||
const materialDatabases = databases.filter(db =>
|
||
db.name && materialDbPatterns.some(pattern =>
|
||
db.name!.toLowerCase().includes(pattern.toLowerCase())
|
||
)
|
||
)
|
||
|
||
await Promise.all(
|
||
materialDatabases.map(db => {
|
||
return new Promise<void>((resolve, reject) => {
|
||
const deleteReq = indexedDB.deleteDatabase(db.name!)
|
||
deleteReq.onsuccess = () => resolve()
|
||
deleteReq.onerror = () => reject(deleteReq.error)
|
||
deleteReq.onblocked = () => resolve() // 忽略阻塞
|
||
})
|
||
})
|
||
)
|
||
console.log(`已清除 ${materialDatabases.length} 个IndexedDB数据库:`, materialDatabases.map(db => db.name))
|
||
}
|
||
} catch (e) {
|
||
console.warn('清除IndexedDB失败:', e)
|
||
}
|
||
|
||
// 6. 清除Next.js的运行时缓存(通过修改URL参数强制刷新)
|
||
// 添加时间戳参数,确保浏览器加载最新资源
|
||
const url = new URL(window.location.href)
|
||
url.searchParams.set('_clear_cache', Date.now().toString())
|
||
|
||
// 显示提示信息
|
||
console.log('✅ 素材网缓存已清除,正在刷新页面...')
|
||
|
||
// 延迟一小段时间让用户看到提示,然后刷新
|
||
setTimeout(() => {
|
||
window.location.href = url.toString()
|
||
}, 300)
|
||
} catch (error) {
|
||
console.error('清除缓存失败:', error)
|
||
alert('清除缓存失败,请手动刷新页面(Ctrl+F5 或 Cmd+Shift+R)')
|
||
}
|
||
}}
|
||
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 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
<span>清除缓存并刷新</span>
|
||
</button>
|
||
|
||
{/* 退出登录 */}
|
||
<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}
|
||
/>
|
||
|
||
{/* 网络诊断弹窗 */}
|
||
<NetworkDiagnoseModal
|
||
isOpen={showNetworkDiagnose}
|
||
onClose={() => setShowNetworkDiagnose(false)}
|
||
/>
|
||
</>
|
||
)
|
||
}
|
||
|