import React, { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react" import { Layout, Space, Button, Tag, Spin, Avatar, Popover, Progress } from "antd" import { MenuFoldOutlined, MenuUnfoldOutlined, FullscreenOutlined, FullscreenExitOutlined, LeftOutlined, } from "@ant-design/icons" import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom" import { useTranslation } from "react-i18next" import { WindowControls } from "./WindowControls" import appLogo from "../assets/logoHD.svg" const loadHome = () => import("./Home") const loadStudentManager = () => import("./StudentManager") const loadSettings = () => import("./Settings") const loadReasonManager = () => import("./ReasonManager") const loadScoreManager = () => import("./ScoreManager") const loadAutoScoreManager = () => import("./AutoScoreManager") const loadLeaderboard = () => import("./Leaderboard") const loadSettlementHistory = () => import("./SettlementHistory") const loadRewardSettings = () => import("./RewardSettings") const loadBoardManager = () => import("./BoardManager") const loadPluginManager = () => import("./PluginManager") const Home = lazy(() => loadHome().then((m) => ({ default: m.Home }))) const StudentManager = lazy(() => loadStudentManager().then((m) => ({ default: m.StudentManager }))) const Settings = lazy(() => loadSettings().then((m) => ({ default: m.Settings }))) const ReasonManager = lazy(() => loadReasonManager().then((m) => ({ default: m.ReasonManager }))) const ScoreManager = lazy(() => loadScoreManager().then((m) => ({ default: m.ScoreManager }))) const AutoScoreManager = lazy(loadAutoScoreManager) const Leaderboard = lazy(() => loadLeaderboard().then((m) => ({ default: m.Leaderboard }))) const SettlementHistory = lazy(() => loadSettlementHistory().then((m) => ({ default: m.SettlementHistory })) ) const RewardSettings = lazy(() => loadRewardSettings().then((m) => ({ default: m.RewardSettings }))) const BoardManager = lazy(() => loadBoardManager().then((m) => ({ default: m.BoardManager }))) const PluginManager = lazy(() => loadPluginManager().then((m) => ({ default: m.PluginManager }))) const warmupRouteChunks = () => Promise.allSettled([ loadHome(), loadStudentManager(), loadSettings(), loadReasonManager(), loadScoreManager(), loadAutoScoreManager(), loadLeaderboard(), loadSettlementHistory(), loadRewardSettings(), loadBoardManager(), loadPluginManager(), ]) const { Content } = Layout interface ContentAreaProps { permission: "admin" | "points" | "view" oauthUserName?: string | null onOAuthLogout?: () => Promise | void hasAnyPassword: boolean onAuthClick: () => void onLogout: () => void showWindowControls: boolean isPortraitMode: boolean isMobileDevice: boolean sidebarCollapsed: boolean floatingExpand: boolean floatingExpanded: boolean onToggleSidebar: () => void immersiveMode: boolean isHomePage: boolean onToggleImmersiveMode: () => void showSidebarToggle?: boolean bottomInset?: number } interface HeaderStorageUsage { used_storage_formatted: string total_storage_formatted: string percentage: number file_count: number } export function ContentArea({ permission, oauthUserName, onOAuthLogout, hasAnyPassword, onAuthClick, onLogout, showWindowControls, isPortraitMode, isMobileDevice, sidebarCollapsed, floatingExpand, floatingExpanded, onToggleSidebar, immersiveMode, isHomePage, onToggleImmersiveMode, showSidebarToggle = true, bottomInset = 0, }: ContentAreaProps): React.JSX.Element { const { t } = useTranslation() const location = useLocation() const navigate = useNavigate() const isSubPage = location.pathname !== "/" && !location.pathname.startsWith("/home") const shouldAnimateSubPage = isPortraitMode && isSubPage const normalizedPath = location.pathname === "/" ? "/home" : location.pathname const isBoardPage = normalizedPath.startsWith("/boards") const isMobileHeaderMode = isPortraitMode && isMobileDevice && !immersiveMode const isPrimaryMobilePage = normalizedPath.startsWith("/home") || normalizedPath.startsWith("/settings") const showMobileBack = isMobileHeaderMode && !isPrimaryMobilePage const mobilePageTitle = (() => { if (normalizedPath.startsWith("/home")) return t("sidebar.home") if (normalizedPath.startsWith("/students")) return t("sidebar.students") if (normalizedPath.startsWith("/score")) return t("sidebar.score") if (normalizedPath.startsWith("/auto-score")) return t("sidebar.autoScore") if (normalizedPath.startsWith("/boards")) return t("sidebar.boards") if (normalizedPath.startsWith("/leaderboard")) return t("sidebar.leaderboard") if (normalizedPath.startsWith("/settlements")) return t("sidebar.settlements") if (normalizedPath.startsWith("/reasons")) return t("sidebar.reasons") if (normalizedPath.startsWith("/reward-settings")) return t("sidebar.rewardSettings") if (normalizedPath.startsWith("/plugins")) return t("sidebar.plugins") if (normalizedPath.startsWith("/settings")) return t("sidebar.settings") return "SecScore" })() const handleMobileBack = () => { if (window.history.length > 1) { navigate(-1) return } navigate("/settings") } useEffect(() => { let cancelled = false let timer: ReturnType | undefined const runWarmup = () => { if (cancelled) return warmupRouteChunks().catch(() => void 0) } if ("requestIdleCallback" in window) { ;(window as any).requestIdleCallback(runWarmup, { timeout: 1500 }) } else { timer = setTimeout(runWarmup, 300) } return () => { cancelled = true if (timer) clearTimeout(timer) } }, []) const permissionTag = ( {permission === "admin" ? t("permissions.admin") : permission === "points" ? t("permissions.points") : t("permissions.view")} ) const fallbackDisplayName = permission === "admin" ? t("permissions.admin") : permission === "points" ? t("permissions.points") : t("permissions.view") const userDisplayName = (oauthUserName || fallbackDisplayName).trim() const avatarText = userDisplayName.slice(0, 1).toUpperCase() const [profilePopoverOpen, setProfilePopoverOpen] = useState(false) const [storageUsageLoading, setStorageUsageLoading] = useState(false) const [storageUsageError, setStorageUsageError] = useState(null) const [storageUsage, setStorageUsage] = useState(null) const [oauthUserId, setOAuthUserId] = useState(null) const [lastSyncTime, setLastSyncTime] = useState(() => { try { return localStorage.getItem("ss_last_sync_time") } catch { return null } }) const [copiedUserId, setCopiedUserId] = useState(false) const [logoutLoading, setLogoutLoading] = useState(false) const copyResetTimerRef = useRef(null) const hasOAuthSession = Boolean(oauthUserName && oauthUserName.trim()) const formattedLastSyncTime = (() => { if (!lastSyncTime) return "暂无" const date = new Date(lastSyncTime) if (Number.isNaN(date.getTime())) return "暂无" return date.toLocaleString("zh-CN", { hour12: false }) })() const loadStorageUsage = useCallback(async () => { setStorageUsageLoading(true) setStorageUsageError(null) try { const api = (window as any).api if (!api?.oauthLoadLoginState) { setStorageUsage(null) setStorageUsageError("当前环境不支持云用量查询") return } const oauthStateRes = await api.oauthLoadLoginState() const oauthState = oauthStateRes?.success ? oauthStateRes.data : null if (!oauthState?.access_token || !oauthState?.user_id) { setOAuthUserId(null) setStorageUsage(null) setStorageUsageError("当前未登录云账号") return } setOAuthUserId(String(oauthState.user_id)) const platformId = import.meta.env.VITE_OAUTH_PLATFORM_ID if (!platformId) { setStorageUsage(null) setStorageUsageError("未配置平台 ID") return } const params = new URLSearchParams({ client_id: platformId, user_id: oauthState.user_id, }) const response = await fetch( `https://appwrite.sectl.top/api/cloud/storage/usage?${params.toString()}`, { headers: { Authorization: `Bearer ${oauthState.access_token}`, }, } ) const responseText = await response.text() let payload: any = {} try { payload = responseText ? JSON.parse(responseText) : {} } catch { payload = {} } if (!response.ok) { throw new Error(payload?.error_description || payload?.message || "获取云空间用量失败") } setStorageUsage({ used_storage_formatted: String(payload?.used_storage_formatted || "0 B"), total_storage_formatted: String(payload?.total_storage_formatted || "0 B"), percentage: Number.isFinite(payload?.percentage) ? Number(payload.percentage) : 0, file_count: Number.isFinite(payload?.file_count) ? Number(payload.file_count) : 0, }) } catch (error: any) { setStorageUsage(null) setStorageUsageError(error?.message || "获取云空间用量失败") } finally { setStorageUsageLoading(false) } }, []) useEffect(() => { if (!profilePopoverOpen) return void loadStorageUsage() }, [loadStorageUsage, profilePopoverOpen]) useEffect(() => { const handleDataUpdated = (event: Event) => { const customEvent = event as CustomEvent<{ source?: string }> if (customEvent?.detail?.source !== "sync") return const now = new Date().toISOString() setLastSyncTime(now) try { localStorage.setItem("ss_last_sync_time", now) } catch { void 0 } } window.addEventListener("ss:data-updated", handleDataUpdated as EventListener) return () => { window.removeEventListener("ss:data-updated", handleDataUpdated as EventListener) if (copyResetTimerRef.current) { window.clearTimeout(copyResetTimerRef.current) copyResetTimerRef.current = null } } }, []) const handleCopyUserId = async () => { if (!oauthUserId) return try { await navigator.clipboard.writeText(oauthUserId) setCopiedUserId(true) if (copyResetTimerRef.current) { window.clearTimeout(copyResetTimerRef.current) } copyResetTimerRef.current = window.setTimeout(() => { setCopiedUserId(false) copyResetTimerRef.current = null }, 1500) } catch { setCopiedUserId(false) } } const handleOAuthLogoutClick = async () => { if (logoutLoading) return setLogoutLoading(true) try { if (onOAuthLogout) { await onOAuthLogout() } else { const api = (window as any).api if (api?.oauthClearLoginState) { await api.oauthClearLoginState() window.dispatchEvent(new CustomEvent("ss:oauth-user-updated", { detail: { user: null } })) } } setProfilePopoverOpen(false) } finally { setLogoutLoading(false) } } const profilePopoverContent = (
账号 ID
{oauthUserId || "未登录"}
最近同步时间
{formattedLastSyncTime}
云空间用量
{storageUsageLoading ? (
) : storageUsage ? (
已用:{storageUsage.used_storage_formatted} / {storageUsage.total_storage_formatted}
90 ? "exception" : "active"} />
文件数量:{storageUsage.file_count}
) : (
{storageUsageError || "暂无云空间数据"}
)}
) return (
{isMobileHeaderMode ? (
{showMobileBack && (
) : ( !immersiveMode && showSidebarToggle && (
{immersiveMode && (
SecScore SecScore
)}
{(immersiveMode || (isHomePage && !isMobileDevice)) && ( {permissionTag} {hasAnyPassword && ( <> )}
{showWindowControls && }
} >
} /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } />
) }