gongxiangp2/components/Header.tsx
chenpei 6c6c282203 feat: 全站视觉统一白皮书语言 + 个人档案改造 + 顶栏平台化
视觉统一(Zen Minimal × Future Organic 白皮书语言,全站浅色单主题):
- 登录页:品牌→由壹点·品牌中心,白纸底+发丝线+柔光光晕,黑按钮
- 作品过稿:深灰头→白皮书,返回链接→返回创意资产库
- 绩效考核:桌面+移动双头部换白皮书,标题改绩效考核,返回改链接
- 组织架构:BoardShell+页面 76 处调色板全量替换(#eef1f4→#fafafa 等)
- admin 后台:MaterialsHeader(面包屑治理与设置),Tab 栏墨色高亮
- 深色主题废弃:globals.css 移除媒体查询,tailwind darkMode:'class' 不挂 .dark
- 设计文档同步更新:AGENTS.md UI 基线/DESIGN.md/PRODUCT.md/README.md/design-qa.md

个人档案(原个人中心):
- API 合并权威人员档案(getEmployeeByUserId → 花名/工号/部门/职位/M-P等级/主管/状态)
- 页面白皮书化:权威档案卡(只读 HR 字段)+ 账号区(头像上传)+ 素材/收藏

顶栏平台化:
- MaterialsHeader 双模式:首页(五板块导航+全套工具+菜单)/板块(面包屑)
- 菜单重排:我的档案/绩效考核/作品过稿看板‖更新记录/提交反馈‖治理与设置‖退出
- 菜单头身份行:花名+职位·部门(权威档案)
- 首页排序:01 创意资产库(全员)→ 02 组织管理(管理员)→ 03-05 规划中
- 素材库改名创意资产库,全站同步(索引/导航/卡片/按钮/页脚)
- 排行榜换白皮书(白底发丝线+琥珀前三名)

构建修复:
- tsconfig 排除 output/(本地审计临时文件不再卡类型检查)
- 存储边界门禁豁免 output/(与 .dockerignore 对齐)
2026-08-15 14:16:20 +08:00

672 lines
31 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, type MouseEvent as ReactMouseEvent } 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 BrandLogo from '@/components/BrandLogo'
import { formatHumanSpeed, formatTransferProgressSummary } from '@/lib/human-readable-transfer'
import { DEFAULT_HEADER_LOGO_URL } from '@/lib/brand-assets'
interface HeaderProps {
session: Session | null
}
export default function Header({ session }: 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素材')
// 加载系统设置
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`
}
const handleLogoHomeClick = (event: ReactMouseEvent<HTMLAnchorElement>) => {
event.preventDefault()
window.location.assign('/')
}
return (
<>
{/* 系统维护通知横幅 */}
<MaintenanceBanner />
{/* 网络切换横幅已移除:主站统一使用 https://sc.jiansun.vip,旧域名 3d.yydd.ai 仅保留跳转 */}
<header
className="sticky top-0 z-50 shadow-sm"
style={{ 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="/" onClick={handleLogoHomeClick} className="flex min-w-0 items-center space-x-2 sm:space-x-3">
<BrandLogo
src={headerLogoUrl}
fallbackSrc={DEFAULT_HEADER_LOGO_URL}
alt=""
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 text-white sm:max-w-none sm:text-2xl">
{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 ${
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 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>
{/* 绩效考核 */}
<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}
/>
</>
)
}