675 lines
32 KiB
TypeScript
675 lines
32 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 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}
|
||
/>
|
||
</>
|
||
)
|
||
}
|