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/pages/AdminGradersPage.tsx | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 201 insertions(+), 0 deletions(-)
diff --git a/frontend/src/pages/AdminGradersPage.tsx b/frontend/src/pages/AdminGradersPage.tsx
new file mode 100644
index 0000000..d19acea
--- /dev/null
+++ b/frontend/src/pages/AdminGradersPage.tsx
@@ -0,0 +1,201 @@
+import { useEffect, useState } from "react";
+import { Button, Card, Form, Input, Modal, Select, Space, Switch, Table, Tag, message } from "antd";
+import { api } from "../api";
+
+type Grader = {
+ id: number;
+ username: string;
+ display_name: string;
+ is_active: boolean;
+ created_at: string;
+ subjects: string[];
+};
+
+type FormValues = {
+ username: string;
+ password?: string;
+ display_name?: string;
+ is_active?: boolean;
+ subjects: string[];
+};
+
+export default function AdminGradersPage() {
+ const [items, setItems] = useState<Grader[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [editing, setEditing] = useState<Grader | null>(null);
+ const [form] = Form.useForm<FormValues>();
+ const [subjectOptions, setSubjectOptions] = useState<string[]>([]);
+
+ const loadSubjects = async () => {
+ const { data } = await api.get<{ items: string[] }>("/api/subjects");
+ setSubjectOptions(data.items);
+ };
+
+ const load = async () => {
+ setLoading(true);
+ try {
+ const { data } = await api.get<{ items: Grader[] }>("/api/admin/graders");
+ setItems(data.items);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ load();
+ loadSubjects();
+ }, []);
+
+ const openCreate = () => {
+ setEditing(null);
+ form.resetFields();
+ form.setFieldsValue({ is_active: true });
+ setModalOpen(true);
+ };
+
+ const openEdit = (row: Grader) => {
+ setEditing(row);
+ form.setFieldsValue({
+ username: row.username,
+ display_name: row.display_name,
+ is_active: row.is_active,
+ subjects: row.subjects ?? [],
+ password: "",
+ });
+ setModalOpen(true);
+ };
+
+ const submit = async () => {
+ const v = await form.validateFields();
+ try {
+ if (editing) {
+ const body: Record<string, unknown> = {
+ username: v.username,
+ display_name: v.display_name ?? "",
+ is_active: v.is_active,
+ subjects: v.subjects,
+ };
+ if (v.password) body.password = v.password;
+ await api.put(`/api/admin/graders/${editing.id}`, body);
+ message.success("宸叉洿鏂�");
+ } else {
+ await api.post("/api/admin/graders", {
+ username: v.username,
+ password: v.password,
+ display_name: v.display_name ?? "",
+ subjects: v.subjects,
+ });
+ message.success("宸插垱寤�");
+ }
+ setModalOpen(false);
+ load();
+ } catch (e: unknown) {
+ const msg =
+ (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || "鎿嶄綔澶辫触";
+ message.error(typeof msg === "string" ? msg : "鎿嶄綔澶辫触");
+ }
+ };
+
+ return (
+ <Card
+ title="鑰冭瘎鍛樼鐞�"
+ extra={
+ <Button type="primary" onClick={openCreate}>
+ 鏂板鑰冭瘎鍛�
+ </Button>
+ }
+ >
+ <Table
+ rowKey="id"
+ loading={loading}
+ dataSource={items}
+ scroll={{ x: 720 }}
+ columns={[
+ { title: "璐﹀彿", dataIndex: "username" },
+ { title: "濮撳悕", dataIndex: "display_name" },
+ {
+ title: "鍏宠仈绉戠洰",
+ dataIndex: "subjects",
+ render: (subs: string[]) =>
+ subs?.length
+ ? subs.map((s) => <Tag key={s}>{s}</Tag>)
+ : <span style={{ color: "#999" }}>鏈厤缃�</span>,
+ },
+ {
+ title: "鐘舵��",
+ dataIndex: "is_active",
+ render: (v: boolean) => (v ? "鍚敤" : "鍋滅敤"),
+ },
+ { title: "鍒涘缓鏃堕棿", dataIndex: "created_at" },
+ {
+ title: "鎿嶄綔",
+ render: (_, row) => (
+ <Space>
+ <Button type="link" onClick={() => openEdit(row)}>缂栬緫</Button>
+ <Button
+ type="link"
+ danger
+ onClick={() => {
+ Modal.confirm({
+ title: "鍒犻櫎璇ヨ�冭瘎鍛橈紵",
+ content: `璐﹀彿锛�${row.username}`,
+ onOk: async () => {
+ await api.delete(`/api/admin/graders/${row.id}`);
+ message.success("宸插垹闄�");
+ load();
+ },
+ });
+ }}
+ >
+ 鍒犻櫎
+ </Button>
+ </Space>
+ ),
+ },
+ ]}
+ />
+
+ <Modal
+ title={editing ? "缂栬緫鑰冭瘎鍛�" : "鏂板鑰冭瘎鍛�"}
+ open={modalOpen}
+ onCancel={() => setModalOpen(false)}
+ onOk={submit}
+ destroyOnClose
+ >
+ <Form form={form} layout="vertical">
+ <Form.Item name="username" label="璐﹀彿" rules={[{ required: true, min: 2 }]}>
+ <Input autoComplete="off" />
+ </Form.Item>
+ <Form.Item
+ name="password"
+ label={editing ? "鏂板瘑鐮侊紙鐣欑┖涓嶄慨鏀癸級" : "瀵嗙爜"}
+ rules={editing ? [] : [{ required: true, min: 6 }]}
+ >
+ <Input.Password autoComplete="new-password" />
+ </Form.Item>
+ <Form.Item name="display_name" label="濮撳悕">
+ <Input />
+ </Form.Item>
+ <Form.Item
+ name="subjects"
+ label="鍏宠仈绉戠洰"
+ rules={[{ required: true, message: "璇疯嚦灏戦�夋嫨涓�涓鐩�" }]}
+ >
+ <Select
+ mode="multiple"
+ placeholder="閫夋嫨鍙煡鐪嬫垚缁╃殑绉戠洰"
+ options={subjectOptions.map((s) => ({ label: s, value: s }))}
+ optionFilterProp="label"
+ />
+ </Form.Item>
+ {editing && (
+ <Form.Item name="is_active" label="鍚敤" valuePropName="checked">
+ <Switch />
+ </Form.Item>
+ )}
+ </Form>
+ </Modal>
+ </Card>
+ );
+}
--
Gitblit v1.8.0