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