'use client'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/components/providers/auth-provider'; import { ThemeToggle } from '@/components/ui/theme-toggle'; import { getPublicStatus } from '@/features/auth/api/public'; import { confirmManualServerUpgrade, getLatestRelease, upgradeServer, uploadServerBinary, } from '@/features/update/api/update'; import { VersionUpgradeModal } from '@/features/update/components/version-upgrade-modal'; import type { ReleaseChannel, UploadedServerBinaryInfo, } from '@/features/update/types'; import { publicEnv } from '@/lib/env/public-env'; import { useAppShellStore } from '@/store/app-shell'; export function DashboardTopbar() { const router = useRouter(); const { logout, user } = useAuth(); const toggleSidebar = useAppShellStore((state) => state.toggleSidebar); const isMobileSidebarOpen = useAppShellStore( (state) => state.isMobileSidebarOpen, ); const setMobileSidebarOpen = useAppShellStore( (state) => state.setMobileSidebarOpen, ); const [isLoggingOut, setIsLoggingOut] = useState(false); const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const [isVersionModalOpen, setIsVersionModalOpen] = useState(false); const [selectedReleaseChannel, setSelectedReleaseChannel] = useState('stable'); const [versionFeedback, setVersionFeedback] = useState(null); const [manualUpgradeStatus, setManualUpgradeStatus] = useState( null, ); const [manualUpgradeError, setManualUpgradeError] = useState( null, ); const [uploadedBinary, setUploadedBinary] = useState(null); const menuRef = useRef(null); const isRoot = (user?.role ?? 0) >= 100; const publicStatusQuery = useQuery({ queryKey: ['public-status'], queryFn: getPublicStatus, }); const stableReleaseQuery = useQuery({ queryKey: ['update', 'latest-release', 'stable'], queryFn: () => getLatestRelease('stable'), enabled: isRoot, refetchInterval: 60 * 60 * 1000, }); const previewReleaseQuery = useQuery({ queryKey: ['update', 'latest-release', 'preview'], queryFn: () => getLatestRelease('preview'), enabled: false, }); const upgradeMutation = useMutation({ mutationFn: (channel: ReleaseChannel) => upgradeServer(channel), onSuccess: (release) => { setUploadedBinary(null); setManualUpgradeStatus(null); setManualUpgradeError(null); setVersionFeedback( `服务升级任务已启动,目标版本 ${release.tag_name}(${release.channel === 'preview' ? '预览版' : '正式版'})。页面可能短暂不可用。`, ); void stableReleaseQuery.refetch(); if (release.channel === 'preview') { void previewReleaseQuery.refetch(); } }, onError: (error) => { setVersionFeedback( error instanceof Error ? error.message : '升级失败,请稍后重试。', ); }, }); const uploadBinaryMutation = useMutation({ mutationFn: uploadServerBinary, onSuccess: (candidate) => { setVersionFeedback(null); setManualUpgradeError(null); setUploadedBinary(candidate); setManualUpgradeStatus(candidate.comparison_message); }, onError: (error) => { setUploadedBinary(null); setManualUpgradeStatus(null); setManualUpgradeError( error instanceof Error ? error.message : '上传升级包失败,请稍后重试。', ); }, }); const confirmManualUpgradeMutation = useMutation({ mutationFn: confirmManualServerUpgrade, onSuccess: (candidate) => { setVersionFeedback(null); setManualUpgradeError(null); setUploadedBinary(candidate); setManualUpgradeStatus( `手动升级任务已启动,目标版本 ${candidate.detected_version}。页面可能短暂不可用。`, ); void stableReleaseQuery.refetch(); void previewReleaseQuery.refetch(); }, onError: (error) => { setManualUpgradeStatus(null); setManualUpgradeError( error instanceof Error ? error.message : '确认手动升级失败,请稍后重试。', ); }, }); useEffect(() => { if (!isUserMenuOpen) { return; } const handlePointerDown = (event: MouseEvent) => { if (!menuRef.current?.contains(event.target as Node)) { setIsUserMenuOpen(false); } }; const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { setIsUserMenuOpen(false); } }; window.addEventListener('mousedown', handlePointerDown); window.addEventListener('keydown', handleEscape); return () => { window.removeEventListener('mousedown', handlePointerDown); window.removeEventListener('keydown', handleEscape); }; }, [isUserMenuOpen]); const handleLogout = async () => { setIsLoggingOut(true); setIsUserMenuOpen(false); await logout(); router.replace('/login'); }; const handleSidebarToggle = () => { if (window.innerWidth < 1000) { setMobileSidebarOpen(!isMobileSidebarOpen); return; } toggleSidebar(); }; const handleOpenVersionModal = () => { setSelectedReleaseChannel('stable'); setVersionFeedback(null); setManualUpgradeStatus(null); setManualUpgradeError(null); setIsVersionModalOpen(true); if (isRoot) { void stableReleaseQuery.refetch(); } }; const handleUpgrade = () => { setVersionFeedback(null); setManualUpgradeStatus(null); setManualUpgradeError(null); upgradeMutation.mutate(selectedReleaseChannel); }; const handleCheckRelease = () => { setVersionFeedback(null); if (isRoot) { if (selectedReleaseChannel === 'preview') { void previewReleaseQuery.refetch(); } else { void stableReleaseQuery.refetch(); } } }; const handleReleaseChannelChange = (channel: ReleaseChannel) => { setSelectedReleaseChannel(channel); setVersionFeedback(null); }; const handleUploadBinary = (binary: File) => { setManualUpgradeStatus(null); setManualUpgradeError(null); uploadBinaryMutation.mutate(binary); }; const handleConfirmManualUpgrade = () => { if (!uploadedBinary?.upload_token) { setManualUpgradeStatus(null); setManualUpgradeError('请先上传并检查升级包。'); return; } setVersionFeedback(null); setManualUpgradeStatus(null); setManualUpgradeError(null); confirmManualUpgradeMutation.mutate(uploadedBinary.upload_token); }; const selectedRelease = selectedReleaseChannel === 'preview' ? previewReleaseQuery.data : stableReleaseQuery.data; const selectedReleaseError = selectedReleaseChannel === 'preview' ? previewReleaseQuery.error : stableReleaseQuery.error; const isSelectedReleaseError = selectedReleaseChannel === 'preview' ? previewReleaseQuery.isError : stableReleaseQuery.isError; const hasUpdate = Boolean(isRoot && stableReleaseQuery.data?.has_update); const currentVersion = publicStatusQuery.data?.version || 'unknown'; const versionLabel = hasUpdate ? `版本 ${publicEnv.appVersion} · 可升级` : `版本 ${publicEnv.appVersion}`; const versionButtonClassName = hasUpdate ? 'border-[var(--status-warning-border)] bg-[var(--status-warning-soft)] text-[var(--status-warning-foreground)]' : 'border-[var(--border-default)]'; const versionErrorMessage = versionFeedback || (isSelectedReleaseError ? selectedReleaseError instanceof Error ? selectedReleaseError.message : '版本检查失败,请稍后重试。' : undefined); const manualUpgradeErrorMessage = manualUpgradeError ?? undefined; return ( <>
{isUserMenuOpen ? (

{user?.display_name || user?.username || '用户'}

{user?.username ? (

@{user.username}

) : null}
) : null}
setIsVersionModalOpen(false)} currentVersion={currentVersion} frontendVersion={publicEnv.appVersion} release={selectedRelease} selectedChannel={selectedReleaseChannel} uploadedBinary={uploadedBinary} isLoading={ (selectedReleaseChannel === 'preview' ? previewReleaseQuery.isLoading && !previewReleaseQuery.data : stableReleaseQuery.isLoading && !stableReleaseQuery.data) && isRoot } releaseErrorMessage={versionErrorMessage} manualStatusMessage={manualUpgradeStatus ?? undefined} manualErrorMessage={manualUpgradeErrorMessage} canUpgrade={isRoot} isChecking={ selectedReleaseChannel === 'preview' ? previewReleaseQuery.isFetching : stableReleaseQuery.isFetching } isUpgrading={upgradeMutation.isPending} isUploadingBinary={uploadBinaryMutation.isPending} isConfirmingManualUpgrade={confirmManualUpgradeMutation.isPending} onChannelChange={handleReleaseChannelChange} onCheck={handleCheckRelease} onUpgrade={handleUpgrade} onUploadBinary={handleUploadBinary} onConfirmManualUpgrade={handleConfirmManualUpgrade} /> ); }