From 3f96c9c6502243b06efd4bb34a189e72e3a5557c Mon Sep 17 00:00:00 2001
From: yearning <10538594+wangweifeng1999@user.noreply.gitee.com>
Date: 星期一, 28 九月 2026 19:24:55 +0800
Subject: [PATCH] 复制项目
---
frontend/src/layout/MainLayout.tsx | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 106 insertions(+), 0 deletions(-)
diff --git a/frontend/src/layout/MainLayout.tsx b/frontend/src/layout/MainLayout.tsx
new file mode 100644
index 0000000..4e1fe92
--- /dev/null
+++ b/frontend/src/layout/MainLayout.tsx
@@ -0,0 +1,106 @@
+import { Layout, Menu, Button, Grid, Drawer } from "antd";
+import { Outlet, useNavigate, useLocation } from "react-router-dom";
+import { useMemo, useState } from "react";
+import { MenuOutlined } from "@ant-design/icons";
+import type { UserMe } from "../App";
+import { clearToken } from "../api";
+import SystemBrand from "../components/SystemBrand";
+import ChangePasswordModal from "../components/ChangePasswordModal";
+import AppBreadcrumb from "./AppBreadcrumb";
+import "./MainLayout.css";
+
+const { Header, Sider, Content } = Layout;
+const { useBreakpoint } = Grid;
+
+export default function MainLayout({
+ user,
+ onLogout,
+}: {
+ user: UserMe;
+ onLogout: () => void;
+}) {
+ const nav = useNavigate();
+ const loc = useLocation();
+ const screens = useBreakpoint();
+ const [drawerOpen, setDrawerOpen] = useState(false);
+ const [pwdOpen, setPwdOpen] = useState(false);
+
+ const items = useMemo(() => {
+ const m = [{ key: "/scores", label: "鎴愮哗鏌ヨ" }];
+ if (user.role === "admin") {
+ m.push({ key: "/admin/import", label: "鏁版嵁瀵煎叆" });
+ m.push({ key: "/admin/graders", label: "鑰冭瘎鍛樼鐞�" });
+ m.push({ key: "/admin/settings", label: "绯荤粺璁剧疆" });
+ }
+ return m;
+ }, [user.role]);
+
+ const menu = (
+ <Menu
+ className="app-sider-menu"
+ mode="inline"
+ selectedKeys={[loc.pathname.startsWith("/attempts") ? "/scores" : loc.pathname]}
+ items={items}
+ onClick={({ key }) => {
+ nav(key);
+ setDrawerOpen(false);
+ }}
+ />
+ );
+
+ const isMobile = !screens.md;
+
+ return (
+ <Layout style={{ minHeight: "100vh", display: "flex", flexDirection: "column" }}>
+ <Header className="app-header">
+ {isMobile && (
+ <Button type="text" icon={<MenuOutlined />} onClick={() => setDrawerOpen(true)} style={{ marginRight: 8 }} />
+ )}
+ <div style={{ flex: 1, minWidth: 0 }}>
+ <SystemBrand size="sm" />
+ </div>
+ <span style={{ marginRight: 12 }}>{user.display_name || user.username}</span>
+ <Button type="link" onClick={() => setPwdOpen(true)}>淇敼瀵嗙爜</Button>
+ <Button
+ type="link"
+ onClick={() => {
+ clearToken();
+ onLogout();
+ nav("/login");
+ }}
+ >
+ 閫�鍑�
+ </Button>
+ </Header>
+ <ChangePasswordModal open={pwdOpen} onClose={() => setPwdOpen(false)} />
+ <Layout className="app-shell-body" style={{ flex: 1, minHeight: 0 }}>
+ {!isMobile && (
+ <Sider width={200} theme="light" className="app-sider">
+ {menu}
+ </Sider>
+ )}
+ <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} placement="left" width={220}>
+ {menu}
+ </Drawer>
+ <Content
+ className={[
+ "main-layout__content",
+ loc.pathname.startsWith("/attempts/") ? "main-layout__content--attempt" : "",
+ loc.pathname === "/scores" ? "main-layout__content--scores" : "",
+ ]
+ .filter(Boolean)
+ .join(" ")}
+ style={{
+ padding: 16,
+ background: loc.pathname.startsWith("/attempts/") ? "#fff" : "#f5f5f5",
+ }}
+ >
+ <AppBreadcrumb />
+ <div className="main-layout__content-inner">
+ <Outlet />
+ </div>
+ </Content>
+ </Layout>
+ </Layout>
+ );
+}
--
Gitblit v1.8.0