360 lines
16 KiB
TypeScript
360 lines
16 KiB
TypeScript
'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 } from 'react'
|
||
import { initializeNetwork } from '@/lib/network'
|
||
import UpdateLogModal, { useUpdateLog, showUpdateLog } from '@/components/UpdateLogModal'
|
||
import NetworkDiagnoseModal from '@/components/NetworkDiagnoseModal'
|
||
|
||
interface HeaderProps {
|
||
session: Session | null
|
||
}
|
||
|
||
export default function Header({ session }: HeaderProps) {
|
||
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)
|
||
const [showNetworkBanner, setShowNetworkBanner] = useState(false)
|
||
const [isAccessingFromExternal, setIsAccessingFromExternal] = useState(false)
|
||
const [showNetworkDiagnose, setShowNetworkDiagnose] = useState(false)
|
||
const dropdownRef = useRef<HTMLDivElement>(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)
|
||
|
||
// 如果内网可达但当前从外网访问,显示切换提示(除非用户已关闭)
|
||
const bannerDismissed = sessionStorage.getItem('networkBannerDismissed')
|
||
if (networkStatus.isInternal && isExternal && !bannerDismissed) {
|
||
setShowNetworkBanner(true)
|
||
}
|
||
} catch (error) {
|
||
console.error('网络检测失败:', error)
|
||
setIsInternalNetwork(false)
|
||
setNetworkChecked(true)
|
||
}
|
||
}
|
||
detectNetwork()
|
||
}, [])
|
||
|
||
// 切换到内网地址
|
||
const switchToInternal = () => {
|
||
const currentPath = window.location.pathname + window.location.search
|
||
window.location.href = `http://192.168.58.20:4320${currentPath}`
|
||
}
|
||
|
||
// 关闭横幅
|
||
const dismissBanner = () => {
|
||
setShowNetworkBanner(false)
|
||
sessionStorage.setItem('networkBannerDismissed', 'true')
|
||
}
|
||
|
||
// 点击外部关闭下拉菜单
|
||
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 getInitials = (name: string) => {
|
||
return name?.charAt(0).toUpperCase() || 'U'
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{/* 网络切换提示横幅 */}
|
||
{showNetworkBanner && (
|
||
<div className="fixed top-0 left-0 right-0 z-[60] bg-gradient-to-r from-green-500 to-emerald-500 text-white shadow-lg">
|
||
<div className="container mx-auto px-4 py-3 max-w-7xl">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<svg className="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||
</svg>
|
||
<div>
|
||
<p className="font-semibold">检测到您在内网环境</p>
|
||
<p className="text-sm text-white/90">切换到内网地址可获得更快的上传下载速度,并避免登录问题</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={switchToInternal}
|
||
className="px-4 py-2 bg-white text-green-600 rounded-lg font-medium hover:bg-green-50 transition-all shadow-sm"
|
||
>
|
||
立即切换
|
||
</button>
|
||
<button
|
||
onClick={dismissBanner}
|
||
className="px-3 py-2 text-white/80 hover:text-white transition-all"
|
||
aria-label="关闭"
|
||
>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<header
|
||
className={`sticky z-50 shadow-sm ${showNetworkBanner ? 'top-[72px]' : 'top-0'}`}
|
||
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={`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>
|
||
)}
|
||
|
||
{/* 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={() => {
|
||
setShowDropdown(false)
|
||
// 使用相对路径,避免被 NEXTAUTH_URL 等配置强制回落到 localhost
|
||
signOut({ callbackUrl: '/login', redirect: true })
|
||
}}
|
||
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)}
|
||
/>
|
||
</>
|
||
)
|
||
}
|
||
|