gongxiangp2/components/Header.tsx

774 lines
37 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 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)}
/>
</>
)
}