import { Layout, Modal, Input, message, ConfigProvider, theme as antTheme } from "antd" import { HomeOutlined, SettingOutlined } from "@ant-design/icons" import { useEffect, useMemo, useRef, useState } from "react" import { HashRouter, useLocation, useNavigate, Routes, Route } from "react-router-dom" import { useTranslation } from "react-i18next" import { Sidebar } from "./components/Sidebar" import { ContentArea } from "./components/ContentArea" import { OOBE } from "./components/OOBE/OOBE" import { ThemeProvider, useTheme } from "./contexts/ThemeContext" function MainContent(): React.JSX.Element { const { t } = useTranslation() const navigate = useNavigate() const location = useLocation() const { currentTheme } = useTheme() const [messageApi, contextHolder] = message.useMessage() const { isIosDevice, isAndroidDevice, defaultPortraitMode } = useMemo(getMobileDeviceInfo, []) const [immersiveMode, setImmersiveMode] = useState(false) useEffect(() => { const api = (window as any).api if (!api) return let disposed = false let unlisten: (() => void) | null = null api .onNavigate((route: string) => { const currentPath = location.pathname === "/" ? "/home" : location.pathname const targetPath = route === "/" ? "/home" : route if (immersiveMode && !targetPath.startsWith("/home")) { navigate("/", { replace: true }) return } if (currentPath !== targetPath) { navigate(route) } }) .then((fn: () => void) => { if (disposed) { fn() return } unlisten = fn }) .catch(() => void 0) return () => { disposed = true if (unlisten) unlisten() } }, [navigate, location.pathname, immersiveMode]) const [wizardVisible, setWizardVisible] = useState(false) const [permission, setPermission] = useState<"admin" | "points" | "view">("view") const [hasAnyPassword, setHasAnyPassword] = useState(false) const [authVisible, setAuthVisible] = useState(false) const [authPassword, setAuthPassword] = useState("") const [authLoading, setAuthLoading] = useState(false) const [isPortraitMode] = useState(defaultPortraitMode) const [sidebarCollapsed, setSidebarCollapsed] = useState(defaultPortraitMode) const [floatingSidebarExpanded, setFloatingSidebarExpanded] = useState(false) const [syncConflictVisible, setSyncConflictVisible] = useState(false) const [syncConflicts, setSyncConflicts] = useState< Array<{ table: string; key: string; local_summary: string; remote_summary: string }> >([]) const [syncApplyLoading, setSyncApplyLoading] = useState(false) const syncCheckingRef = useRef(false) const syncApplyLoadingRef = useRef(false) const lastLocalMutationAtRef = useRef(0) const activeMenu = useMemo(() => { const p = location.pathname if (p === "/" || p.startsWith("/home")) return "home" if (p.startsWith("/students")) return "students" if (p.startsWith("/score")) return "score" if (p.startsWith("/boards")) return "boards" if (p.startsWith("/leaderboard")) return "leaderboard" if (p.startsWith("/settlements")) return "settlements" if (p.startsWith("/reasons")) return "reasons" if (p.startsWith("/auto-score")) return "auto-score" if (p.startsWith("/reward-settings")) return "reward-settings" if (p.startsWith("/settings")) return "settings" return "home" }, [location.pathname]) useEffect(() => { const checkWizard = async () => { if (!(window as any).api) return const res = await (window as any).api.getAllSettings() if (res.success && res.data && !res.data.is_wizard_completed) { setWizardVisible(true) } } checkWizard() }, []) useEffect(() => { const loadAuthAndSettings = async () => { if (!(window as any).api) return const authRes = await (window as any).api.authGetStatus() if (authRes?.success && authRes.data) { setPermission(authRes.data.permission) const anyPwd = Boolean(authRes.data.hasAdminPassword || authRes.data.hasPointsPassword) setHasAnyPassword(anyPwd) if (anyPwd && authRes.data.permission === "view") setAuthVisible(true) } } loadAuthAndSettings() }, []) useEffect(() => { const api = (window as any).api if (!api || !isIosDevice) return const fitIosWindow = async () => { try { await api.windowMaximize() } catch { void 0 } try { await api.windowSetResizable(false) } catch { void 0 } try { // 传入远大于设备尺寸的物理像素,交由系统裁剪为可用全屏区域 await api.windowResize(10000, 10000) } catch { void 0 } } fitIosWindow().catch(() => void 0) const timer = window.setTimeout(() => { fitIosWindow().catch(() => void 0) }, 300) return () => { window.clearTimeout(timer) } }, [isIosDevice]) useEffect(() => { const api = (window as any).api if (!api || typeof api.onDataUpdated !== "function") return let disposed = false let unlisten: (() => void) | null = null api .onDataUpdated((payload: { category?: string; source?: string }) => { const detail = { category: payload?.category || "all", source: payload?.source || "tauri", } window.dispatchEvent(new CustomEvent("ss:data-updated", { detail })) }) .then((fn: () => void) => { if (disposed) { fn() return } unlisten = fn }) .catch(() => void 0) return () => { disposed = true if (unlisten) unlisten() } }, []) const applySyncStrategy = async (strategy: "keep_local" | "keep_remote") => { const api = (window as any).api if (!api) return setSyncApplyLoading(true) syncApplyLoadingRef.current = true try { const res = await api.dbSyncApply(strategy) if (res?.success && res?.data?.success) { messageApi.success( res.data.message || `同步完成(同步 ${res.data.synced_records} 条,解决冲突 ${res.data.resolved_conflicts} 条)` ) window.dispatchEvent( new CustomEvent("ss:data-updated", { detail: { category: "all", source: "sync" } }) ) } else { messageApi.error(res?.data?.message || res?.message || "同步失败") } } catch (error: any) { messageApi.error(error?.message || "同步失败") } finally { setSyncApplyLoading(false) syncApplyLoadingRef.current = false setSyncConflictVisible(false) setSyncConflicts([]) } } useEffect(() => { const api = (window as any).api if (!api) return let disposed = false const checkAndSync = async () => { if (disposed || syncCheckingRef.current || syncApplyLoadingRef.current) return if (permission !== "admin") return try { syncCheckingRef.current = true const statusRes = await api.dbGetStatus() if ( !statusRes?.success || statusRes?.data?.type !== "postgresql" || !statusRes?.data?.connected ) { return } const previewRes = await api.dbSyncPreview() if (!previewRes?.success || !previewRes?.data?.can_sync || !previewRes?.data?.need_sync) { return } const conflicts = previewRes.data.conflicts || [] if (conflicts.length > 0) { const recentLocalMutation = Date.now() - lastLocalMutationAtRef.current < 15000 if (recentLocalMutation) { const autoApplyRes = await api.dbSyncApply("keep_remote") if ( autoApplyRes?.success && autoApplyRes?.data?.success && autoApplyRes?.data?.synced_records > 0 ) { window.dispatchEvent( new CustomEvent("ss:data-updated", { detail: { category: "all", source: "sync" } }) ) } return } setSyncConflicts(conflicts) setSyncConflictVisible(true) return } const applyRes = await api.dbSyncApply("keep_remote") if (applyRes?.success && applyRes?.data?.success && applyRes?.data?.synced_records > 0) { window.dispatchEvent( new CustomEvent("ss:data-updated", { detail: { category: "all", source: "sync" } }) ) } } catch (error) { console.error("Auto sync failed:", error) } finally { syncCheckingRef.current = false } } checkAndSync() const timer = window.setInterval(checkAndSync, 30000) const onDataUpdated = (e: Event) => { const customEvent = e as CustomEvent<{ source?: string }> if (customEvent?.detail?.source !== "sync") { lastLocalMutationAtRef.current = Date.now() } window.setTimeout(() => { checkAndSync().catch(() => void 0) }, 1200) } window.addEventListener("ss:data-updated", onDataUpdated) return () => { disposed = true window.clearInterval(timer) window.removeEventListener("ss:data-updated", onDataUpdated) } }, [permission]) const login = async () => { if (!(window as any).api) return setAuthLoading(true) const res = await (window as any).api.authLogin(authPassword) setAuthLoading(false) if (res.success && res.data) { setPermission(res.data.permission) setAuthVisible(false) setAuthPassword("") messageApi.success(t("auth.unlocked")) } else { messageApi.error(res.message || t("common.error")) } } const logout = async () => { if (!(window as any).api) return const res = await (window as any).api.authLogout() if (res?.success && res.data) { setPermission(res.data.permission) messageApi.success(t("auth.logout")) } } const onMenuChange = (v: string) => { const key = String(v) if (immersiveMode && key !== "home") return if (key === "home") navigate("/") if (key === "students") navigate("/students") if (key === "score") navigate("/score") if (key === "boards") navigate("/boards") if (key === "leaderboard") navigate("/leaderboard") if (key === "settlements") navigate("/settlements") if (key === "reasons") navigate("/reasons") if (key === "auto-score") navigate("/auto-score") if (key === "reward-settings") navigate("/reward-settings") if (key === "settings") navigate("/settings") } const toggleSidebar = () => { if (immersiveMode) return if (isPortraitMode && sidebarCollapsed) { setFloatingSidebarExpanded((prev) => !prev) return } setSidebarCollapsed((prev) => !prev) } useEffect(() => { if (!immersiveMode) return if (location.pathname !== "/" && !location.pathname.startsWith("/home")) { navigate("/", { replace: true }) } }, [immersiveMode, location.pathname, navigate]) const toggleImmersiveMode = () => { setImmersiveMode((prev) => { const next = !prev if (next) { setFloatingSidebarExpanded(false) if (location.pathname !== "/" && !location.pathname.startsWith("/home")) { navigate("/", { replace: true }) } } return next }) } const isDark = currentTheme?.mode === "dark" const brandColor = currentTheme?.config?.tdesign?.brandColor || "#0052D9" const isMobileDevice = isIosDevice || isAndroidDevice const showMobileBottomNav = isPortraitMode && !immersiveMode return ( {contextHolder} {!isPortraitMode && (
)} setAuthVisible(true)} onLogout={logout} showWindowControls={!isIosDevice && !isAndroidDevice} isPortraitMode={isPortraitMode} isMobileDevice={isMobileDevice} sidebarCollapsed={sidebarCollapsed} floatingExpand={isPortraitMode} floatingExpanded={floatingSidebarExpanded} onToggleSidebar={toggleSidebar} immersiveMode={immersiveMode} isHomePage={activeMenu === "home"} onToggleImmersiveMode={toggleImmersiveMode} showSidebarToggle={!isPortraitMode} bottomInset={showMobileBottomNav ? 84 : 0} /> {showMobileBottomNav && (
)} setWizardVisible(false)} /> setAuthVisible(false)} onOk={login} confirmLoading={authLoading} okText={t("auth.unlockButton")} cancelText={t("common.cancel")} >
{t("auth.unlockHint")}
setAuthPassword(e.target.value)} placeholder={t("auth.passwordPlaceholder")} maxLength={6} />
{ if (syncApplyLoading) return setSyncConflictVisible(false) }} footer={null} closable={!syncApplyLoading} maskClosable={false} destroyOnClose >
自动同步发现冲突,请选择冲突时优先保留哪一侧的数据。
{syncConflicts.slice(0, 30).map((item) => (
{item.table} / {item.key}
本地: {item.local_summary}
远程: {item.remote_summary}
))} {syncConflicts.length > 30 &&
仅显示前 30 条冲突...
}
{import.meta.env.DEV ? (

开发中画面,不代表最终品质

SecScore Dev ({getPlatform()}-{getArchitecture()})

) : null}
) } function getArchitecture(): string { const userAgent = navigator.userAgent.toLowerCase() if (userAgent.includes("arm64") || userAgent.includes("aarch64")) { return "ARM64" } else if ( userAgent.includes("x64") || userAgent.includes("amd64") || userAgent.includes("x86_64") || userAgent.includes("intel") ) { return "x64" } else if (userAgent.includes("i386") || userAgent.includes("i686")) { return "x86" } return "Unknown" } function getPlatform(): string { const userAgent = navigator.userAgent.toLowerCase() if (userAgent.includes("iphone") || userAgent.includes("ipad") || userAgent.includes("ipod")) { return "iOS" } else if (userAgent.includes("android")) { return "Android" } if (userAgent.includes("windows")) { return "Windows" } else if (userAgent.includes("mac")) { return "Mac" } else if (userAgent.includes("linux")) { return "Linux" } return "Unknown" } function getIosDeviceInfo(): { isIosDevice: boolean; isIosPhone: boolean } { const userAgent = navigator.userAgent.toLowerCase() const isIosDevice = /iphone|ipad|ipod/.test(userAgent) const isIosTablet = userAgent.includes("ipad") return { isIosDevice, isIosPhone: isIosDevice && !isIosTablet, } } function getMobileDeviceInfo(): { isIosDevice: boolean isAndroidDevice: boolean defaultPortraitMode: boolean } { const { isIosDevice, isIosPhone } = getIosDeviceInfo() const isAndroidDevice = navigator.userAgent.toLowerCase().includes("android") return { isIosDevice, isAndroidDevice, defaultPortraitMode: isIosPhone || isAndroidDevice, } } function App(): React.JSX.Element { return ( } /> ) } export default App