feat: 新增独立管理窗口

This commit is contained in:
JSR
2026-07-10 22:26:07 +08:00
parent 795bae1f03
commit 7971c6adbc
13 changed files with 343 additions and 163 deletions
+50 -28
View File
@@ -44,6 +44,7 @@ const DEFAULT_MOBILE_BOTTOM_PRIMARY_KEYS: MobileNavKey[] = DEFAULT_MOBILE_BOTTOM
0,
4
)
const MANAGEMENT_DEFAULT_ROUTE = "/students"
const applyGlobalFontFamily = (fontValue?: string) => {
const fontFamily = resolveStoredFontFamily(fontValue)
document.documentElement.style.setProperty("--ss-font-family", fontFamily)
@@ -61,7 +62,11 @@ function MainContent(): React.JSX.Element {
typeof navigator !== "undefined" &&
/mac/i.test(navigator.userAgent) &&
!/iphone|ipad|ipod|android/i.test(navigator.userAgent)
const [immersiveMode, setImmersiveMode] = useState(false)
const isManagementWindow = useMemo(() => {
const params = new URLSearchParams(window.location.search)
return params.get("window") === "management"
}, [])
const [immersiveMode] = useState(!isManagementWindow)
const normalizeStoredBottomKeys = (raw: unknown): MobileNavKey[] => {
if (Array.isArray(raw)) {
@@ -82,11 +87,16 @@ function MainContent(): React.JSX.Element {
const currentPath = location.pathname === "/" ? "/home" : location.pathname
const targetPath = route === "/" ? "/home" : route
if (immersiveMode && !targetPath.startsWith("/home")) {
if (!isManagementWindow && !targetPath.startsWith("/home")) {
navigate("/", { replace: true })
return
}
if (isManagementWindow && targetPath.startsWith("/home")) {
navigate(MANAGEMENT_DEFAULT_ROUTE, { replace: true })
return
}
if (currentPath !== targetPath) {
navigate(route)
}
@@ -104,7 +114,7 @@ function MainContent(): React.JSX.Element {
disposed = true
if (unlisten) unlisten()
}
}, [navigate, location.pathname, immersiveMode])
}, [navigate, location.pathname, isManagementWindow])
const [wizardVisible, setWizardVisible] = useState(false)
const [permission, setPermission] = useState<"admin" | "points" | "view">("view")
@@ -154,7 +164,7 @@ function MainContent(): React.JSX.Element {
const activeMenu = useMemo(() => {
const p = location.pathname
if (p === "/" || p.startsWith("/home")) return "home"
if (p === "/" || p.startsWith("/home")) return isManagementWindow ? "students" : "home"
if (p.startsWith("/students")) return "students"
if (p.startsWith("/score")) return "score"
if (p.startsWith("/boards")) return "boards"
@@ -165,8 +175,8 @@ function MainContent(): React.JSX.Element {
if (p.startsWith("/reward-settings")) return "reward-settings"
if (p.startsWith("/plugins")) return "plugins"
if (p.startsWith("/settings")) return "settings"
return "home"
}, [location.pathname])
return isManagementWindow ? "students" : "home"
}, [location.pathname, isManagementWindow])
useEffect(() => {
const runtime = pluginRuntimeRef.current
@@ -602,7 +612,8 @@ function MainContent(): React.JSX.Element {
const key = String(v)
setMoreNavVisible(false)
setEditingNav(false)
if (immersiveMode && key !== "home") return
if (!isManagementWindow && key !== "home") return
if (isManagementWindow && key === "home") return
if (key === "home") navigate("/")
if (key === "students") navigate("/students")
if (key === "score") navigate("/score")
@@ -626,29 +637,32 @@ function MainContent(): React.JSX.Element {
}
useEffect(() => {
if (!immersiveMode) return
if (location.pathname !== "/" && !location.pathname.startsWith("/home")) {
if (
!isManagementWindow &&
location.pathname !== "/" &&
!location.pathname.startsWith("/home")
) {
navigate("/", { replace: true })
return
}
}, [immersiveMode, location.pathname, navigate])
if (
isManagementWindow &&
(location.pathname === "/" || location.pathname.startsWith("/home"))
) {
navigate(MANAGEMENT_DEFAULT_ROUTE, { replace: true })
}
}, [isManagementWindow, 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 openManagementWindow = useCallback(async () => {
const api = (window as any).api
if (!api?.openManagementWindow) return
await api.openManagementWindow()
}, [])
const isDark = currentTheme?.mode === "dark"
const brandColor = currentTheme?.config?.tdesign?.brandColor || "#0052D9"
const isMobileDevice = isIosDevice || isAndroidDevice
const showMobileBottomNav = isPortraitMode && !immersiveMode
const showMobileBottomNav = isPortraitMode && isManagementWindow
const mobileBottomNavIconMap: Record<MobileNavKey, React.ReactNode> = {
home: <HomeOutlined style={{ fontSize: "18px" }} />,
students: <UserOutlined style={{ fontSize: "18px" }} />,
@@ -664,8 +678,13 @@ function MainContent(): React.JSX.Element {
}
const mobileNavAvailableItems = useMemo(
() => MOBILE_NAV_ITEMS.filter((item) => !(item.adminOnly && permission !== "admin")),
[permission]
() =>
MOBILE_NAV_ITEMS.filter(
(item) =>
(isManagementWindow ? item.key !== "home" : item.key === "home") &&
!(item.adminOnly && permission !== "admin")
),
[isManagementWindow, permission]
)
const mobileBottomSelectedKeys = useMemo(() => {
const availableKeySet = new Set(mobileNavAvailableItems.map((item) => item.key))
@@ -768,7 +787,7 @@ function MainContent(): React.JSX.Element {
<Layout
style={{ height: "100%", flexDirection: "row", overflow: "hidden", position: "relative" }}
>
{!isPortraitMode && (
{!isPortraitMode && isManagementWindow && (
<div
className={`ss-immersive-sidebar ${immersiveMode ? "is-hidden" : "is-visible"}`}
style={
@@ -785,6 +804,7 @@ function MainContent(): React.JSX.Element {
floatingExpand={isPortraitMode}
floatingExpanded={floatingSidebarExpanded}
onFloatingExpandedChange={setFloatingSidebarExpanded}
showHome={false}
/>
</div>
)}
@@ -804,8 +824,10 @@ function MainContent(): React.JSX.Element {
onToggleSidebar={toggleSidebar}
immersiveMode={immersiveMode}
isHomePage={activeMenu === "home"}
onToggleImmersiveMode={toggleImmersiveMode}
showSidebarToggle={!isPortraitMode}
onOpenManagementWindow={openManagementWindow}
showSidebarToggle={!isPortraitMode && isManagementWindow}
showHomeRoute={!isManagementWindow}
fallbackRoute={isManagementWindow ? MANAGEMENT_DEFAULT_ROUTE : "/"}
bottomInset={showMobileBottomNav ? 84 : 0}
/>
{showMobileBottomNav && (
+24 -20
View File
@@ -3,9 +3,8 @@ import { Layout, Space, Button, Tag, Spin, Avatar, Popover, Progress } from "ant
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
LeftOutlined,
SettingOutlined,
} from "@ant-design/icons"
import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
@@ -71,8 +70,10 @@ interface ContentAreaProps {
onToggleSidebar: () => void
immersiveMode: boolean
isHomePage: boolean
onToggleImmersiveMode: () => void
onOpenManagementWindow?: () => Promise<void> | void
showSidebarToggle?: boolean
showHomeRoute?: boolean
fallbackRoute?: string
bottomInset?: number
}
@@ -99,8 +100,10 @@ export function ContentArea({
onToggleSidebar,
immersiveMode,
isHomePage,
onToggleImmersiveMode,
onOpenManagementWindow,
showSidebarToggle = true,
showHomeRoute = true,
fallbackRoute = "/",
bottomInset = 0,
}: ContentAreaProps): React.JSX.Element {
const { t } = useTranslation()
@@ -118,7 +121,7 @@ export function ContentArea({
const isBoardPage = normalizedPath.startsWith("/boards")
const isMobileHeaderMode = isPortraitMode && isMobileDevice && !immersiveMode
const isPrimaryMobilePage =
normalizedPath.startsWith("/home") || normalizedPath.startsWith("/settings")
(showHomeRoute && normalizedPath.startsWith("/home")) || normalizedPath.startsWith("/settings")
const showMobileBack = isMobileHeaderMode && !isPrimaryMobilePage
const mobilePageTitle = (() => {
if (normalizedPath.startsWith("/home")) return t("sidebar.home")
@@ -549,10 +552,9 @@ export function ContentArea({
{(immersiveMode || (isHomePage && !isMobileDevice)) && (
<Button
size="small"
type={immersiveMode ? "primary" : "default"}
icon={immersiveMode ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onClick={onToggleImmersiveMode}
title={immersiveMode ? "退出沉浸模式" : "进入沉浸模式"}
icon={<SettingOutlined />}
onClick={onOpenManagementWindow}
title="管理"
/>
)}
<Popover
@@ -651,16 +653,18 @@ export function ContentArea({
className={`ss-route-page${shouldAnimateSubPage ? " is-subpage-enter" : ""}${isBoardPage ? " is-board-page" : ""}`}
>
<Routes>
<Route
path="/"
element={
<Home
canEdit={permission === "admin" || permission === "points"}
isPortraitMode={isPortraitMode}
immersiveMode={immersiveMode}
/>
}
/>
{showHomeRoute && (
<Route
path="/"
element={
<Home
canEdit={permission === "admin" || permission === "points"}
isPortraitMode={isPortraitMode}
immersiveMode={immersiveMode}
/>
}
/>
)}
<Route
path="/students"
element={<StudentManager canEdit={permission === "admin"} />}
@@ -685,7 +689,7 @@ export function ContentArea({
/>
<Route path="/plugins" element={<PluginManager canEdit={permission === "admin"} />} />
<Route path="/settings" element={<Settings permission={permission} />} />
<Route path="*" element={<Navigate to="/" replace />} />
<Route path="*" element={<Navigate to={fallbackRoute} replace />} />
</Routes>
</div>
</Suspense>
+3 -1
View File
@@ -27,6 +27,7 @@ interface SidebarProps {
floatingExpand: boolean
floatingExpanded: boolean
onFloatingExpandedChange: (expanded: boolean) => void
showHome?: boolean
}
interface DbStatus {
@@ -43,6 +44,7 @@ export function Sidebar({
floatingExpand,
floatingExpanded,
onFloatingExpandedChange,
showHome = true,
}: SidebarProps): React.JSX.Element {
const { t } = useTranslation()
const [dbStatus, setDbStatus] = useState<DbStatus>({ type: "sqlite", connected: true })
@@ -209,7 +211,7 @@ export function Sidebar({
label: t("sidebar.settings"),
disabled: permission !== "admin",
},
]
].filter((item) => showHome || item.key !== "home")
const showFloatingPanel = floatingExpand && collapsed && floatingExpanded
+1
View File
@@ -638,6 +638,7 @@ const api = {
windowMaximize: (): Promise<boolean> => invoke("window_maximize"),
windowClose: (): Promise<void> => invoke("window_close"),
windowIsMaximized: (): Promise<boolean> => invoke("window_is_maximized"),
openManagementWindow: (): Promise<void> => invoke("window_open_management"),
startDraggingWindow: (): Promise<void> => invoke("window_start_dragging"),
toggleDevTools: (): Promise<void> => invoke("toggle_devtools"),
windowResize: (width: number, height: number): Promise<void> =>