diff --git a/src/api/problem.ts b/src/api/problem.ts new file mode 100644 index 0000000..11cf54a --- /dev/null +++ b/src/api/problem.ts @@ -0,0 +1,40 @@ +import type { Problem } from "@/types/problem"; + +// 获取所有题目 +export async function getProblems(): Promise { + const res = await fetch("/api/problem"); + if (!res.ok) throw new Error("获取题目失败"); + return res.json(); +} + +// 新建题目 +export async function createProblem(data: Partial): Promise { + const res = await fetch("/api/problem", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("新建题目失败"); + return res.json(); +} + +// 编辑题目 +export async function updateProblem(data: Partial): Promise { + const res = await fetch("/api/problem", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error("更新题目失败"); + return res.json(); +} + +// 删除题目 +export async function deleteProblem(id: string): Promise { + const res = await fetch("/api/problem", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + if (!res.ok) throw new Error("删除题目失败"); +} \ No newline at end of file diff --git a/src/api/user.ts b/src/api/user.ts index 6638a98..f08cb61 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -2,39 +2,51 @@ import type { UserBase } from "@/types/user"; // 获取所有用户 export async function getUsers(userType: string): Promise { - const res = await fetch(`/api/${userType}`); - if (!res.ok) throw new Error("获取用户失败"); + const res = await fetch(`/api/user`); + if (!res.ok) { + const error = await res.json(); + throw new Error(error.error || "获取用户失败"); + } return res.json(); } // 新建用户 export async function createUser(userType: string, data: Partial): Promise { - const res = await fetch(`/api/${userType}`, { + const res = await fetch(`/api/user`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); - if (!res.ok) throw new Error("新建用户失败"); + if (!res.ok) { + const error = await res.json(); + throw new Error(error.error || "新建用户失败"); + } return res.json(); } // 更新用户 export async function updateUser(userType: string, data: Partial): Promise { - const res = await fetch(`/api/${userType}`, { + const res = await fetch(`/api/user`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); - if (!res.ok) throw new Error("更新用户失败"); + if (!res.ok) { + const error = await res.json(); + throw new Error(error.error || "更新用户失败"); + } return res.json(); } // 删除用户 export async function deleteUser(userType: string, id: string): Promise { - const res = await fetch(`/api/${userType}`, { + const res = await fetch(`/api/user`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }), }); - if (!res.ok) throw new Error("删除用户失败"); + if (!res.ok) { + const error = await res.json(); + throw new Error(error.error || "删除用户失败"); + } } \ No newline at end of file diff --git a/src/app/(app)/usermanagement/_components/ProtectedLayout.tsx b/src/app/(app)/usermanagement/_components/ProtectedLayout.tsx new file mode 100644 index 0000000..ed7830a --- /dev/null +++ b/src/app/(app)/usermanagement/_components/ProtectedLayout.tsx @@ -0,0 +1,28 @@ +import { auth } from "@/lib/auth"; +import prisma from "@/lib/prisma"; +import { redirect } from "next/navigation"; + +interface ProtectedLayoutProps { + children: React.ReactNode; + allowedRoles: string[]; +} + +export default async function ProtectedLayout({ children, allowedRoles }: ProtectedLayoutProps) { + const session = await auth(); + const userId = session?.user?.id; + + if (!userId) { + redirect("/sign-in"); + } + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { role: true } + }); + + if (!user || !allowedRoles.includes(user.role)) { + redirect("/sign-in"); + } + + return
{children}
; +} \ No newline at end of file diff --git a/src/app/(app)/usermanagement/admin/layout.tsx b/src/app/(app)/usermanagement/admin/layout.tsx new file mode 100644 index 0000000..4a0b1d1 --- /dev/null +++ b/src/app/(app)/usermanagement/admin/layout.tsx @@ -0,0 +1,5 @@ +import ProtectedLayout from "../_components/ProtectedLayout"; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + return {children}; +} \ No newline at end of file diff --git a/src/app/(app)/usermanagement/admin/page.tsx b/src/app/(app)/usermanagement/admin/page.tsx new file mode 100644 index 0000000..fc631b5 --- /dev/null +++ b/src/app/(app)/usermanagement/admin/page.tsx @@ -0,0 +1,5 @@ +import { UserManagement } from "@/features/user-management" + +export default function Page() { + return +} \ No newline at end of file diff --git a/src/app/(app)/usermanagement/problem/layout.tsx b/src/app/(app)/usermanagement/problem/layout.tsx new file mode 100644 index 0000000..8ba9c17 --- /dev/null +++ b/src/app/(app)/usermanagement/problem/layout.tsx @@ -0,0 +1,5 @@ +import ProtectedLayout from "../_components/ProtectedLayout"; + +export default function ProblemLayout({ children }: { children: React.ReactNode }) { + return {children}; +} \ No newline at end of file diff --git a/src/app/(app)/usermanagement/problem/page.tsx b/src/app/(app)/usermanagement/problem/page.tsx new file mode 100644 index 0000000..40af293 --- /dev/null +++ b/src/app/(app)/usermanagement/problem/page.tsx @@ -0,0 +1,5 @@ +import { UserManagement } from "@/features/user-management" + +export default function Page() { + return +} \ No newline at end of file diff --git a/src/app/(app)/usermanagement/student/layout.tsx b/src/app/(app)/usermanagement/student/layout.tsx new file mode 100644 index 0000000..f5a6a03 --- /dev/null +++ b/src/app/(app)/usermanagement/student/layout.tsx @@ -0,0 +1,5 @@ +import ProtectedLayout from "../_components/ProtectedLayout"; + +export default function StudentLayout({ children }: { children: React.ReactNode }) { + return {children}; +} \ No newline at end of file diff --git a/src/app/(app)/usermanagement/student/page.tsx b/src/app/(app)/usermanagement/student/page.tsx new file mode 100644 index 0000000..7b750d9 --- /dev/null +++ b/src/app/(app)/usermanagement/student/page.tsx @@ -0,0 +1,5 @@ +import { UserManagement } from "@/features/user-management" + +export default function Page() { + return +} \ No newline at end of file diff --git a/src/app/(app)/usermanagement/teacher/layout.tsx b/src/app/(app)/usermanagement/teacher/layout.tsx new file mode 100644 index 0000000..fa544d2 --- /dev/null +++ b/src/app/(app)/usermanagement/teacher/layout.tsx @@ -0,0 +1,5 @@ +import ProtectedLayout from "../_components/ProtectedLayout"; + +export default function TeacherLayout({ children }: { children: React.ReactNode }) { + return {children}; +} \ No newline at end of file diff --git a/src/app/(app)/usermanagement/teacher/page.tsx b/src/app/(app)/usermanagement/teacher/page.tsx new file mode 100644 index 0000000..3983daf --- /dev/null +++ b/src/app/(app)/usermanagement/teacher/page.tsx @@ -0,0 +1,5 @@ +import { UserManagement } from "@/features/user-management" + +export default function Page() { + return +} \ No newline at end of file diff --git a/src/app/admin/data.json b/src/app/admin/data.json deleted file mode 100644 index b62bde6..0000000 --- a/src/app/admin/data.json +++ /dev/null @@ -1,122 +0,0 @@ -[ - { - "id": "1", - "name": "张三", - "email": "zhangsan@example.com", - "createdAt": "2023-10-01T08:00:00Z" - }, - { - "id": "2", - "name": "李四", - "email": "lisi@example.com", - "createdAt": "2023-10-02T09:30:00Z" - }, - { - "id": "3", - "name": "王五", - "email": "wangwu@example.com", - "createdAt": "2023-10-03T11:45:00Z" - }, - { - "id": "4", - "name": "赵六", - "email": "zhaoliu@example.org", - "createdAt": "2023-10-04T14:00:00Z" - }, - { - "id": "5", - "name": "孙七", - "email": "sunqi@example.net", - "createdAt": "2023-10-05T16:15:00Z" - }, - { - "id": "6", - "name": "周八", - "email": "zhouba@example.org", - "createdAt": "2023-10-06T18:30:00Z" - }, - { - "id": "7", - "name": "吴九", - "email": "wujiu@example.net", - "createdAt": "2023-10-07T20:45:00Z" - }, - { - "id": "8", - "name": "郑十", - "email": "zhengshi@example.org", - "createdAt": "2023-10-08T23:00:00Z" - }, - { - "id": "9", - "name": "钱十一", - "email": "qian11@example.net", - "createdAt": "2023-10-09T01:15:00Z" - }, - { - "id": "10", - "name": "孙十二", - "email": "sun12@example.org", - "createdAt": "2023-10-10T03:30:00Z" - }, - { - "id": "11", - "name": "周十三", - "email": "zhou13@example.net", - "createdAt": "2023-10-11T05:45:00Z" - }, - { - "id": "12", - "name": "吴十四", - "email": "wushi14@example.org", - "createdAt": "2023-10-12T08:00:00Z" - }, - { - "id": "13", - "name": "郑十五", - "email": "zheng15@example.net", - "createdAt": "2023-10-13T10:15:00Z" - }, - { - "id": "14", - "name": "钱十六", - "email": "qian16@example.org", - "createdAt": "2023-10-14T12:30:00Z" - }, - { - "id": "15", - "name": "孙十七", - "email": "sun17@example.net", - "createdAt": "2023-10-15T14:45:00Z" - }, - { - "id": "16", - "name": "周十八", - "email": "zhou18@example.org", - "createdAt": "2023-10-16T17:00:00Z" - }, - { - "id": "17", - "name": "吴十九", - "email": "wujiu19@example.net", - "createdAt": "2023-10-17T19:15:00Z" - }, - { - "id": "18", - "name": "郑二十", - "email": "zheng20@example.org", - "createdAt": "2023-10-18T21:30:00Z" - }, - { - "id": "19", - "name": "周十九", - "email": "zhou19@example.org", - "createdAt": "2023-10-19T15:30:00Z" - }, - { - "id": "20", - "name": "郑二十", - "email": "zheng20@example.net", - "createdAt": "2023-10-20T17:45:00Z" - } -] diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx deleted file mode 100644 index 2c1c7a6..0000000 --- a/src/app/admin/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { DataTable } from "@/components/data-table" -import { SiteHeader } from "@/components/site-header" - -export default function Page() { - return ( -
- -
-
- -
-
-
- ) -} diff --git a/src/app/api/admin/route.ts b/src/app/api/admin/route.ts deleted file mode 100644 index b945dbf..0000000 --- a/src/app/api/admin/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import prisma from "@/lib/prisma"; -import { NextRequest, NextResponse } from "next/server"; -import bcrypt from "bcryptjs"; - -// 获取所有管理员 -export async function GET() { - const users = await prisma.user.findMany({ where: { role: "ADMIN" } }); - return NextResponse.json(users); -} - -// 新建管理员 -export async function POST(req: NextRequest) { - const data = await req.json(); - if (data.password) { - data.password = await bcrypt.hash(data.password, 10); - } - data.role = "ADMIN"; - const user = await prisma.user.create({ data }); - const { password, ...userWithoutPassword } = user; - return NextResponse.json(userWithoutPassword); -} - -// 编辑管理员 -export async function PUT(req: NextRequest) { - const data = await req.json(); - if (data.password) { - data.password = await bcrypt.hash(data.password, 10); - } - data.role = "ADMIN"; - const user = await prisma.user.update({ - where: { id: data.id }, - data, - }); - const { password, ...userWithoutPassword } = user; - return NextResponse.json(userWithoutPassword); -} - -// 删除管理员 -export async function DELETE(req: NextRequest) { - const { id } = await req.json(); - await prisma.user.delete({ where: { id } }); - return NextResponse.json({ success: true }); -} \ No newline at end of file diff --git a/src/app/api/problem/route.ts b/src/app/api/problem/route.ts new file mode 100644 index 0000000..75656c5 --- /dev/null +++ b/src/app/api/problem/route.ts @@ -0,0 +1,96 @@ +import prisma from "@/lib/prisma"; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; + +// 获取所有题目 +export async function GET() { + try { + // 权限校验(可选:只允许管理员/教师) + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未授权" }, { status: 401 }); + } + // 可根据需要校验角色 + // const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { role: true } }); + // if (user?.role !== "ADMIN" && user?.role !== "TEACHER") { + // return NextResponse.json({ error: "权限不足" }, { status: 403 }); + // } + const problems = await prisma.problem.findMany({ + select: { + id: true, + displayId: true, + difficulty: true, + } + }); + return NextResponse.json(problems); + } catch (error) { + console.error("获取题目列表失败:", error); + return NextResponse.json({ error: "服务器错误" }, { status: 500 }); + } +} + +// 新建题目 +export async function POST(req: NextRequest) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未授权" }, { status: 401 }); + } + // 只允许管理员/教师添加 + // const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { role: true } }); + // if (user?.role !== "ADMIN" && user?.role !== "TEACHER") { + // return NextResponse.json({ error: "权限不足" }, { status: 403 }); + // } + const data = await req.json(); + const newProblem = await prisma.problem.create({ data }); + return NextResponse.json(newProblem); + } catch (error) { + console.error("创建题目失败:", error); + return NextResponse.json({ error: "服务器错误" }, { status: 500 }); + } +} + +// 编辑题目 +export async function PUT(req: NextRequest) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未授权" }, { status: 401 }); + } + // 只允许管理员/教师编辑 + // const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { role: true } }); + // if (user?.role !== "ADMIN" && user?.role !== "TEACHER") { + // return NextResponse.json({ error: "权限不足" }, { status: 403 }); + // } + const data = await req.json(); + const updatedProblem = await prisma.problem.update({ + where: { id: data.id }, + data, + }); + return NextResponse.json(updatedProblem); + } catch (error) { + console.error("更新题目失败:", error); + return NextResponse.json({ error: "服务器错误" }, { status: 500 }); + } +} + +// 删除题目 +export async function DELETE(req: NextRequest) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未授权" }, { status: 401 }); + } + // 只允许管理员/教师删除 + // const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { role: true } }); + // if (user?.role !== "ADMIN" && user?.role !== "TEACHER") { + // return NextResponse.json({ error: "权限不足" }, { status: 403 }); + // } + const { id } = await req.json(); + await prisma.problem.delete({ where: { id } }); + return NextResponse.json({ success: true }); + } catch (error) { + console.error("删除题目失败:", error); + return NextResponse.json({ error: "服务器错误" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 720dc83..1dd5df6 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -1,41 +1,189 @@ import prisma from "@/lib/prisma"; import { NextRequest, NextResponse } from "next/server"; import bcrypt from "bcryptjs"; +import { auth } from "@/lib/auth"; -// 获取所有管理员 +// 获取所有用户 export async function GET() { - const users = await prisma.user.findMany(); - return NextResponse.json(users); + try { + // 验证管理员权限 + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未授权" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { role: true } + }); + + if (user?.role !== "ADMIN") { + return NextResponse.json({ error: "权限不足" }, { status: 403 }); + } + + const users = await prisma.user.findMany({ + select: { + id: true, + name: true, + email: true, + role: true, + createdAt: true, + updatedAt: true, + password: true, // 包含密码字段用于处理 + } + }); + + // 在服务器端处理密码显示逻辑 + const processedUsers = users.map(user => ({ + ...user, + password: user.password ? "******" : "(无)", // 服务器端处理密码显示 + createdAt: user.createdAt instanceof Date ? user.createdAt.toLocaleString() : user.createdAt, // 服务器端处理日期格式 + })); + + return NextResponse.json(processedUsers); + } catch (error) { + console.error("获取用户列表失败:", error); + return NextResponse.json({ error: "服务器错误" }, { status: 500 }); + } } -// 新建管理员 +// 新建用户 export async function POST(req: NextRequest) { - const data = await req.json(); - if (data.password) { - data.password = await bcrypt.hash(data.password, 10); + try { + // 验证管理员权限 + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未授权" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { role: true } + }); + + if (user?.role !== "ADMIN") { + return NextResponse.json({ error: "权限不足" }, { status: 403 }); + } + + const data = await req.json(); + + // 如果提供了密码,进行加密 + if (data.password) { + data.password = await bcrypt.hash(data.password, 10); + } + + const newUser = await prisma.user.create({ + data, + select: { + id: true, + name: true, + email: true, + role: true, + createdAt: true, + updatedAt: true, + password: true, + } + }); + + // 处理返回数据 + const processedUser = { + ...newUser, + password: newUser.password ? "******" : "(无)", + createdAt: newUser.createdAt instanceof Date ? newUser.createdAt.toLocaleString() : newUser.createdAt, + }; + + return NextResponse.json(processedUser); + } catch (error) { + console.error("创建用户失败:", error); + return NextResponse.json({ error: "服务器错误" }, { status: 500 }); } - const user = await prisma.user.create({ data }); - const { password, ...userWithoutPassword } = user; - return NextResponse.json(userWithoutPassword); } -// 编辑管理员 +// 编辑用户 export async function PUT(req: NextRequest) { - const data = await req.json(); - if (data.password) { - data.password = await bcrypt.hash(data.password, 10); + try { + // 验证管理员权限 + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未授权" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { role: true } + }); + + if (user?.role !== "ADMIN") { + return NextResponse.json({ error: "权限不足" }, { status: 403 }); + } + + const data = await req.json(); + + // 如果提供了密码且不为空,进行加密 + if (data.password && data.password.trim() !== '') { + data.password = await bcrypt.hash(data.password, 10); + } else { + // 如果密码为空,则不更新密码字段 + delete data.password; + } + + const updatedUser = await prisma.user.update({ + where: { id: data.id }, + data, + select: { + id: true, + name: true, + email: true, + role: true, + createdAt: true, + updatedAt: true, + password: true, + } + }); + + // 处理返回数据 + const processedUser = { + ...updatedUser, + password: updatedUser.password ? "******" : "(无)", + createdAt: updatedUser.createdAt instanceof Date ? updatedUser.createdAt.toLocaleString() : updatedUser.createdAt, + }; + + return NextResponse.json(processedUser); + } catch (error) { + console.error("更新用户失败:", error); + return NextResponse.json({ error: "服务器错误" }, { status: 500 }); } - const user = await prisma.user.update({ - where: { id: data.id }, - data, - }); - const { password, ...userWithoutPassword } = user; - return NextResponse.json(userWithoutPassword); } -// 删除管理员 +// 删除用户 export async function DELETE(req: NextRequest) { - const { id } = await req.json(); - await prisma.user.delete({ where: { id } }); - return NextResponse.json({ success: true }); + try { + // 验证管理员权限 + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "未授权" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { role: true } + }); + + if (user?.role !== "ADMIN") { + return NextResponse.json({ error: "权限不足" }, { status: 403 }); + } + + const { id } = await req.json(); + + // 防止删除自己 + if (id === session.user.id) { + return NextResponse.json({ error: "不能删除自己的账户" }, { status: 400 }); + } + + await prisma.user.delete({ where: { id } }); + return NextResponse.json({ success: true }); + } catch (error) { + console.error("删除用户失败:", error); + return NextResponse.json({ error: "服务器错误" }, { status: 500 }); + } } \ No newline at end of file diff --git a/src/app/dashboard/admin.tsx b/src/app/dashboard/admin.tsx deleted file mode 100644 index 7959311..0000000 --- a/src/app/dashboard/admin.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import AdminTable from "@/components/user-table/admin-table"; - -export default function AdminDashboardPage() { - return ( -
-

管理员管理

- -
- ); -} \ No newline at end of file diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx deleted file mode 100644 index c842320..0000000 --- a/src/components/data-table.tsx +++ /dev/null @@ -1,917 +0,0 @@ -"use client" - -import * as React from "react" -import { - DndContext, - KeyboardSensor, - MouseSensor, - TouchSensor, - closestCenter, - useSensor, - useSensors, - type DragEndEvent, - type UniqueIdentifier, -} from "@dnd-kit/core" -import { restrictToVerticalAxis } from "@dnd-kit/modifiers" -import { - SortableContext, - arrayMove, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { - ColumnDef, - ColumnFiltersState, - Row, - SortingState, - VisibilityState, - flexRender, - getCoreRowModel, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table" -import { - ChevronDownIcon, - ChevronLeftIcon, - ChevronRightIcon, - ChevronsLeftIcon, - ChevronsRightIcon, - ColumnsIcon, - GripVerticalIcon, - LoaderIcon, - MoreHorizontalIcon, - MoreVerticalIcon, - PlusIcon, - EyeIcon, - PencilIcon, - TrashIcon, - ListFilter, -} from "lucide-react" -import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" -import { toast } from "sonner" -import { z } from "zod" -import { useState, useEffect, useRef } from "react" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" - -import { useIsMobile } from "@/hooks/use-mobile" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@/components/ui/chart" -import { Checkbox } from "@/components/ui/checkbox" -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { Separator } from "@/components/ui/separator" -import { - Sheet, - SheetClose, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet" -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs" - -import * as userApi from "@/api/user" - -// 定义管理员数据的 schema,与后端 User 类型保持一致 -const schema = z.object({ - id: z.string(), - name: z.string().optional(), - email: z.string(), - password: z.string().optional(), - role: z.string().optional(), - createdAt: z.string(), - updatedAt: z.string().optional(), -}) - -export type Admin = z.infer - -// 表单校验 schema -const addAdminSchema = z.object({ - name: z.string().optional(), - email: z.string().email("请输入有效的邮箱地址").optional().or(z.literal("")), - password: z.string().optional(), - createdAt: z.string().optional(), -}) -type AddAdminFormData = z.infer - -const editAdminSchema = z.object({ - id: z.string().min(1, "ID不能为空"), - name: z.string().optional(), - email: z.string().email("请输入有效的邮箱地址").optional().or(z.literal("")), - password: z.string().optional(), - createdAt: z.string().optional(), -}) -type EditAdminFormData = z.infer - -// 生成唯一id -function generateUniqueId(existingIds: string[]): string { - let id; - do { - id = 'c' + Math.random().toString(36).slice(2, 12) + Date.now().toString(36).slice(-4); - } while (existingIds.includes(id)); - return id; -} - -// Create a separate component for the drag handle -function DragHandle({ id }: { id: string }) { - const { attributes, listeners } = useSortable({ - id, - }) - - return ( - - ) -} - -export function DataTable({ - data: initialData, -}: { - data: Admin[] -}) { - const [data, setData] = useState(initialData) - const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) - const [editingAdmin, setEditingAdmin] = useState(null) - const [rowSelection, setRowSelection] = useState({}) - const [columnVisibility, setColumnVisibility] = useState({}) - const [columnFilters, setColumnFilters] = useState([]) - const [sorting, setSorting] = useState([]) - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10, - }) - const sortableId = React.useId() - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ) - - const dataIds = React.useMemo( - () => data?.map(({ id }) => id) || [], - [data] - ) - - // 搜索输入本地 state - const [nameSearch, setNameSearch] = useState(""); - const [emailSearch, setEmailSearch] = useState(""); - - // 删除确认对话框相关state - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) - const [deleteTargetId, setDeleteTargetId] = useState(null) - const [deleteBatch, setDeleteBatch] = useState(false) - - // 页码输入本地state - const [pageInput, setPageInput] = useState(pagination.pageIndex + 1) - useEffect(() => { - setPageInput(pagination.pageIndex + 1) - }, [pagination.pageIndex]) - - const tableColumns = React.useMemo[]>(() => [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="选择所有" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="选择行" - /> - ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "id", - header: "ID", - }, - { - accessorKey: "name", - header: ({ column }) => ( -
- 姓名 - { - const v = column.getFilterValue(); - return typeof v === 'string' ? v : ''; - })()} - onChange={e => column.setFilterValue(e.target.value)} - style={{ minWidth: 0 }} - /> -
- ), - }, - { - accessorKey: "email", - header: ({ column }) => ( -
- 邮箱 - { - const v = column.getFilterValue(); - return typeof v === 'string' ? v : ''; - })()} - onChange={e => column.setFilterValue(e.target.value)} - style={{ minWidth: 0 }} - /> -
- ), - }, - { - accessorKey: "password", - header: "密码", - cell: ({ row }) => row.getValue("password") ? "******" : "(无)", - }, - { - accessorKey: "createdAt", - header: "创建时间", - cell: ({ row }) => { - const date = new Date(row.getValue("createdAt")) - return date.toLocaleString() - }, - }, - { - id: "actions", - header: () =>
操作
, - cell: ({ row }) => { - const admin = row.original - return ( -
- - -
- ) - }, - }, - ], []); - - const table = useReactTable({ - data, - columns: tableColumns, - state: { - sorting, - columnVisibility, - rowSelection, - columnFilters, - pagination, - }, - enableRowSelection: true, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - onPaginationChange: setPagination, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - }); - - // useEffect 同步 filterValue 到本地 state - useEffect(() => { - const v = table.getColumn("name")?.getFilterValue(); - setNameSearch(typeof v === 'string' ? v : ""); - }, [table.getColumn("name")?.getFilterValue()]); - useEffect(() => { - const v = table.getColumn("email")?.getFilterValue(); - setEmailSearch(typeof v === 'string' ? v : ""); - }, [table.getColumn("email")?.getFilterValue()]); - - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event - if (active && over && active.id !== over.id) { - setData((data) => { - const oldIndex = dataIds.indexOf(active.id) - const newIndex = dataIds.indexOf(over.id) - return arrayMove(data, oldIndex, newIndex) - }) - } - } - - // 数据加载与API对接 - useEffect(() => { - userApi.getUsers() - .then(setData) - .catch(() => toast.error('获取管理员数据失败')) - }, []) - - // 添加管理员对话框组件 - function AddAdminDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { - const [isLoading, setIsLoading] = useState(false) - const form = useForm({ - resolver: zodResolver(addAdminSchema), - defaultValues: { - name: "", - email: "", - password: "", - createdAt: new Date().toISOString().slice(0, 16), - }, - }) - React.useEffect(() => { - if (open) { - form.reset({ - name: "", - email: "", - password: "", - createdAt: new Date().toISOString().slice(0, 16), - }) - } - }, [open, form]) - async function onSubmit(data: AddAdminFormData) { - try { - setIsLoading(true) - const existingIds = dataRef.current.map(item => item.id) - const id = generateUniqueId(existingIds) - const submitData = { - ...data, - id, - createdAt: data.createdAt ? new Date(data.createdAt).toISOString() : new Date().toISOString(), - }; - await userApi.createUser(submitData) - userApi.getUsers().then(setData) - onOpenChange(false) - toast.success('添加成功') - } catch (error) { - toast.error("添加管理员失败") - } finally { - setIsLoading(false) - } - } - return ( - - - - 添加管理员 - - 请填写管理员信息,ID自动生成。 - - -
-
-
- - - {form.formState.errors.name && ( -

- {form.formState.errors.name.message} -

- )} -
-
- - - {form.formState.errors.email && ( -

- {form.formState.errors.email.message} -

- )} -
-
- - - {form.formState.errors.password && ( -

- {form.formState.errors.password.message} -

- )} -
-
- - -
-
- - - -
-
-
- ) - } - - // 修改管理员对话框组件 - function EditAdminDialog({ open, onOpenChange, admin }: { open: boolean; onOpenChange: (open: boolean) => void; admin: Admin }) { - const [isLoading, setIsLoading] = useState(false) - const form = useForm({ - resolver: zodResolver(editAdminSchema), - defaultValues: { - id: admin.id, - name: admin.name || "", - email: admin.email || "", - password: admin.password || "", - createdAt: admin.createdAt ? new Date(admin.createdAt).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16), - }, - }) - React.useEffect(() => { - if (open) { - form.reset({ - id: admin.id, - name: admin.name || "", - email: admin.email || "", - password: admin.password || "", - createdAt: admin.createdAt ? new Date(admin.createdAt).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16), - }) - } - }, [open, admin, form]) - async function onSubmit(data: EditAdminFormData) { - try { - setIsLoading(true) - const submitData = { - ...data, - createdAt: data.createdAt ? new Date(data.createdAt).toISOString() : new Date().toISOString(), - }; - await userApi.updateUser(submitData) - userApi.getUsers().then(setData) - onOpenChange(false) - toast.success('修改成功') - } catch (error) { - toast.error("修改管理员失败") - } finally { - setIsLoading(false) - } - } - return ( - - - - 修改管理员 - - 修改管理员信息 - - -
-
-
- - -
-
- - - {form.formState.errors.name && ( -

- {form.formState.errors.name.message} -

- )} -
-
- - - {form.formState.errors.email && ( -

- {form.formState.errors.email.message} -

- )} -
-
- - - {form.formState.errors.password && ( -

- {form.formState.errors.password.message} -

- )} -
-
- - -
-
- - - -
-
-
- ) - } - - // 用ref保证获取最新data - const dataRef = React.useRef(data) - React.useEffect(() => { dataRef.current = data }, [data]) - - return ( - -
-
- 管理员列表 -
-
- - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - // 中文列名映射 - const columnNameMap: Record = { - select: "选择", - id: "ID", - name: "姓名", - email: "邮箱", - password: "密码", - createdAt: "创建时间", - actions: "操作", - }; - return ( - - column.toggleVisibility(!!value) - } - > - {columnNameMap[column.id] || column.id} - - ) - })} - - - - -
-
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - 暂无数据 - - - )} - -
-
- {/* 固定底部的分页和行选中信息 */} -
-
- {table.getFilteredSelectedRowModel().rows.length} 行被选中 -
-
-
- 每页 - - -
- {/* 页码跳转 */} -
- 跳转到 - setPageInput(Number(e.target.value))} - onBlur={() => { - let page = Number(pageInput) - 1 - if (isNaN(page) || page < 0) page = 0 - if (page >= table.getPageCount()) page = table.getPageCount() - 1 - setPagination(p => ({ ...p, pageIndex: page })) - }} - onKeyDown={e => { - if (e.key === 'Enter') { - let page = Number(pageInput) - 1 - if (isNaN(page) || page < 0) page = 0 - if (page >= table.getPageCount()) page = table.getPageCount() - 1 - setPagination(p => ({ ...p, pageIndex: page })) - } - }} - className="w-14 h-7 text-xs px-1" - style={{ minWidth: 0 }} - /> - -
- - -
-
-
- - {editingAdmin && ( - - )} - {/* 删除确认对话框 */} - - - - 确认删除 - - {deleteBatch - ? `确定要删除选中的所有管理员吗?` - : `确定要删除该管理员吗?`} - - - - - - - - -
- ) -} - -const chartData = [ - { month: "January", desktop: 186, mobile: 80 }, - { month: "February", desktop: 305, mobile: 200 }, - { month: "March", desktop: 237, mobile: 120 }, - { month: "April", desktop: 73, mobile: 190 }, - { month: "May", desktop: 209, mobile: 130 }, - { month: "June", desktop: 214, mobile: 140 }, -] - -const chartConfig = { - desktop: { - label: "Desktop", - color: "var(--primary)", - }, - mobile: { - label: "Mobile", - color: "var(--primary)", - }, -} satisfies ChartConfig diff --git a/src/components/user-table/admin-table.tsx b/src/components/user-table/admin-table.tsx deleted file mode 100644 index 147fcc0..0000000 --- a/src/components/user-table/admin-table.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from "zod"; -import { UserTable } from "./index"; -import type { Admin } from "@/types/user"; - -// 管理员表单校验 schema -const adminSchema = z.object({ - name: z.string().optional(), - email: z.string().email("请输入有效的邮箱地址"), - password: z.string().optional(), - role: z.string().optional(), - createdAt: z.string().optional(), - updatedAt: z.string().optional(), -}); - -// 管理员表格列配置 -const adminColumns = [ - { key: "id" as keyof Admin, label: "ID" }, - { key: "name" as keyof Admin, label: "姓名" }, - { key: "email" as keyof Admin, label: "邮箱" }, - { key: "role" as keyof Admin, label: "角色" }, - { key: "createdAt" as keyof Admin, label: "创建时间" }, -]; - -// 管理员表单字段配置 -const adminFormFields = [ - { key: "name" as keyof Admin, label: "姓名" }, - { key: "email" as keyof Admin, label: "邮箱", type: "email", required: true }, - { key: "password" as keyof Admin, label: "密码", type: "password" }, - { key: "role" as keyof Admin, label: "角色" }, -]; - -export default function AdminTable() { - return ( - - userType="admin" - columns={adminColumns} - schema={adminSchema} - formFields={adminFormFields} - /> - ); -} \ No newline at end of file diff --git a/src/components/user-table/index.tsx b/src/components/user-table/index.tsx deleted file mode 100644 index 4fa0c6d..0000000 --- a/src/components/user-table/index.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import * as React from "react"; -import { useState, useEffect, useRef } from "react"; -import { toast } from "sonner"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import * as userApi from "@/api/user"; -import type { UserBase } from "@/types/user"; - -interface UserTableProps { - userType: string; - columns: { key: keyof T; label: string; render?: (value: any, row: T) => React.ReactNode }[]; - schema: any; // zod schema - formFields: { key: keyof T; label: string; type?: string; required?: boolean }[]; -} - -export function UserTable({ userType, columns, schema, formFields }: UserTableProps) { - const [data, setData] = useState([]); - const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); - const [editingUser, setEditingUser] = useState(null); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [deleteTargetId, setDeleteTargetId] = useState(null); - const [deleteBatch, setDeleteBatch] = useState(false); - const [selectedIds, setSelectedIds] = useState([]); - - // 获取数据 - useEffect(() => { - userApi.getUsers(userType) - .then(res => setData(res as T[])) - .catch(() => toast.error('获取数据失败')) - }, [userType]) - - // 添加用户表单 - function AddUserDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { - const [isLoading, setIsLoading] = useState(false); - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: {}, - }); - React.useEffect(() => { - if (open) form.reset({}); - }, [open, form]); - async function onSubmit(values: any) { - try { - setIsLoading(true); - await userApi.createUser(userType, values); - userApi.getUsers(userType).then(res => setData(res as T[])); - onOpenChange(false); - toast.success('添加成功'); - } catch { - toast.error('添加失败'); - } finally { - setIsLoading(false); - } - } - return ( - - - - 添加 - -
- {formFields.map(field => ( -
- - -
- ))} - - - -
-
-
- ); - } - - // 编辑用户表单 - function EditUserDialog({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: T }) { - const [isLoading, setIsLoading] = useState(false); - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: user, - }); - React.useEffect(() => { - if (open) form.reset(user); - }, [open, user, form]); - async function onSubmit(values: any) { - try { - setIsLoading(true); - await userApi.updateUser(userType, { ...user, ...values }); - userApi.getUsers(userType).then(res => setData(res as T[])); - onOpenChange(false); - toast.success('修改成功'); - } catch { - toast.error('修改失败'); - } finally { - setIsLoading(false); - } - } - return ( - - - - 编辑 - -
- {formFields.map(field => ( -
- - -
- ))} - - - -
-
-
- ); - } - - // 删除确认 - async function handleDelete(ids: string[]) { - try { - await Promise.all(ids.map(id => userApi.deleteUser(userType, id))); - userApi.getUsers(userType).then(res => setData(res as T[])); - toast.success('删除成功'); - } catch { - toast.error('删除失败'); - } - } - - return ( -
-
- - -
- - - - - {columns.map(col => )} - - - - - {data.map(row => ( - - - {columns.map(col => )} - - - ))} - -
0} onChange={e => setSelectedIds(e.target.checked ? data.map(d => d.id) : [])} />{col.label}操作
setSelectedIds(ids => e.target.checked ? [...ids, row.id] : ids.filter(id => id !== row.id))} />{col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")} - - -
- - {editingUser && } - - - - 确认删除 - - - - - - - -
- ); -} \ No newline at end of file diff --git a/src/features/user-management/components/user-table.tsx b/src/features/user-management/components/user-table.tsx new file mode 100644 index 0000000..4800186 --- /dev/null +++ b/src/features/user-management/components/user-table.tsx @@ -0,0 +1,901 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + ChevronLeftIcon, + ChevronRightIcon, + ChevronsLeftIcon, + ChevronsRightIcon, + PlusIcon, + PencilIcon, + TrashIcon, + ListFilter, +} from "lucide-react" +import { toast } from "sonner" +import { useState, useEffect } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tabs, +} from "@/components/ui/tabs" + +import * as userApi from "@/api/user" +import * as problemApi from "@/api/problem" + +// 通用用户类型 +export interface UserConfig { + userType: string + title: string + apiPath: string + columns: Array<{ + key: string + label: string + sortable?: boolean + searchable?: boolean + placeholder?: string + }> + formFields: Array<{ + key: string + label: string + type: string + placeholder?: string + required?: boolean + }> + actions: { + add: { label: string; icon: string } + edit: { label: string; icon: string } + delete: { label: string; icon: string } + batchDelete: { label: string; icon: string } + } + pagination: { + pageSizes: number[] + defaultPageSize: number + } +} + +interface UserTableProps { + config: UserConfig + data: any[] +} + +// 在组件内部定义 schema +const addUserSchema = z.object({ + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址"), + password: z.string().optional(), + createdAt: z.string(), +}) + +const editUserSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址"), + password: z.string().optional(), + createdAt: z.string(), +}) + +const addProblemSchema = z.object({ + displayId: z.number(), + difficulty: z.string(), +}) + +const editProblemSchema = z.object({ + id: z.string(), + displayId: z.number(), + difficulty: z.string(), +}) + +export function UserTable({ config, data: initialData }: UserTableProps) { + const [data, setData] = useState(initialData) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [editingUser, setEditingUser] = useState(null) + const [rowSelection, setRowSelection] = useState({}) + const [columnVisibility, setColumnVisibility] = useState({}) + const [columnFilters, setColumnFilters] = useState([]) + const [sorting, setSorting] = useState([]) + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: config.pagination.defaultPageSize, + }) + + // 删除确认对话框相关state + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [deleteTargetId, setDeleteTargetId] = useState(null) + const [deleteBatch, setDeleteBatch] = useState(false) + + // 页码输入本地state + const [pageInput, setPageInput] = useState(pagination.pageIndex + 1) + useEffect(() => { + setPageInput(pagination.pageIndex + 1) + }, [pagination.pageIndex]) + + // 判断是否为题目管理 + const isProblem = config.userType === "problem" + + // 动态生成表格列 + const tableColumns = React.useMemo[]>(() => { + const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="选择所有" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="选择行" + /> + ), + enableSorting: false, + enableHiding: false, + }, + ] + + // 添加配置的列 + config.columns.forEach((col) => { + const column: ColumnDef = { + accessorKey: col.key, + header: ({ column: tableColumn }) => { + if (col.searchable) { + return ( +
+ {col.label} + { + const v = tableColumn.getFilterValue() + return typeof v === 'string' ? v : '' + })()} + onChange={e => tableColumn.setFilterValue(e.target.value)} + style={{ minWidth: 0 }} + /> +
+ ) + } + return col.label + }, + cell: ({ row }) => { + const value = row.getValue(col.key) + return value + }, + enableSorting: col.sortable !== false, + filterFn: col.searchable ? (row, columnId, value) => { + const searchValue = String(value).toLowerCase() + const cellValue = String(row.getValue(columnId)).toLowerCase() + return cellValue.includes(searchValue) + } : undefined, + } + columns.push(column) + }) + + // 添加操作列 + columns.push({ + id: "actions", + header: () =>
操作
, + cell: ({ row }) => { + const user = row.original + return ( +
+ + +
+ ) + }, + }) + + return columns + }, [config]) + + const table = useReactTable({ + data, + columns: tableColumns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + pagination, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onPaginationChange: setPagination, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + // 数据加载与API对接 + useEffect(() => { + if (isProblem) { + problemApi.getProblems() + .then(setData) + .catch(() => toast.error('获取数据失败')) + } else { + userApi.getUsers(config.userType) + .then(setData) + .catch(() => toast.error('获取数据失败')) + } + }, [config.userType]) + + // 生成唯一ID + function generateUniqueId(existingIds: string[]): string { + let id: string + do { + id = Math.random().toString(36).substr(2, 9) + } while (existingIds.includes(id)) + return id + } + + // 添加用户对话框组件(仅用户) + function AddUserDialogUser({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { + const [isLoading, setIsLoading] = useState(false) + const form = useForm({ + resolver: zodResolver(addUserSchema), + defaultValues: { name: "", email: "", password: "", createdAt: new Date().toISOString().slice(0, 16) }, + }) + React.useEffect(() => { + if (open) { + form.reset({ name: "", email: "", password: "", createdAt: new Date().toISOString().slice(0, 16) }) + } + }, [open, form]) + async function onSubmit(formData: any) { + try { + setIsLoading(true) + const existingIds = dataRef.current.map(item => item.id) + const id = generateUniqueId(existingIds) + const submitData = { + ...formData, + id, + createdAt: formData.createdAt ? new Date(formData.createdAt).toISOString() : new Date().toISOString(), + } + await userApi.createUser(config.userType, submitData) + userApi.getUsers(config.userType).then(setData) + onOpenChange(false) + toast.success('添加成功') + } catch { + toast.error("添加失败") + } finally { + setIsLoading(false) + } + } + + return ( + + + + {config.actions.add.label} + + 请填写信息,ID自动生成。 + + +
+
+ {config.formFields.map((field) => ( +
+ + + {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && ( +

+ {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string} +

+ )} +
+ ))} +
+ + + +
+
+
+ ) + } + + // 添加题目对话框组件(仅题目) + function AddUserDialogProblem({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { + const [isLoading, setIsLoading] = useState(false) + const form = useForm({ + resolver: zodResolver(addProblemSchema), + defaultValues: { displayId: 0, difficulty: "" }, + }) + React.useEffect(() => { + if (open) { + form.reset({ displayId: 0, difficulty: "" }) + } + }, [open, form]) + async function onSubmit(formData: any) { + try { + setIsLoading(true) + const existingIds = dataRef.current.map(item => item.id) + const id = generateUniqueId(existingIds) + const submitData = { + ...formData, + displayId: Number(formData.displayId), + id, + } + await problemApi.createProblem(submitData) + problemApi.getProblems().then(setData) + onOpenChange(false) + toast.success('添加成功') + } catch { + toast.error("添加失败") + } finally { + setIsLoading(false) + } + } + + return ( + + + + {config.actions.add.label} + + 请填写信息,ID自动生成。 + + +
+
+ {config.formFields.map((field) => ( +
+ + + {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && ( +

+ {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string} +

+ )} +
+ ))} +
+ + + +
+
+
+ ) + } + + // 编辑用户对话框组件(仅用户) + function EditUserDialogUser({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: any }) { + const [isLoading, setIsLoading] = useState(false) + const form = useForm({ + resolver: zodResolver(editUserSchema), + defaultValues: { id: user.id, name: user.name || "", email: user.email || "", password: "", createdAt: user.createdAt ? new Date(user.createdAt).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16) }, + }) + React.useEffect(() => { + if (open) { + form.reset({ id: user.id, name: user.name || "", email: user.email || "", password: "", createdAt: user.createdAt ? new Date(user.createdAt).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16) }) + } + }, [open, user, form]) + async function onSubmit(formData: any) { + try { + setIsLoading(true) + const submitData = { + ...formData, + createdAt: formData.createdAt ? new Date(formData.createdAt).toISOString() : new Date().toISOString(), + } + if (!submitData.password) { + delete submitData.password; + } + await userApi.updateUser(config.userType, submitData) + userApi.getUsers(config.userType).then(setData) + onOpenChange(false) + toast.success('修改成功') + } catch { + toast.error("修改失败") + } finally { + setIsLoading(false) + } + } + + return ( + + + + {config.actions.edit.label} + + 修改信息 + + +
+
+ {config.formFields.map((field) => ( +
+ + + {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && ( +

+ {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string} +

+ )} +
+ ))} +
+ + + +
+
+
+ ) + } + + // 编辑题目对话框组件(仅题目) + function EditUserDialogProblem({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: any }) { + const [isLoading, setIsLoading] = useState(false) + const form = useForm({ + resolver: zodResolver(editProblemSchema), + defaultValues: { id: user.id, displayId: Number(user.displayId), difficulty: user.difficulty || "" }, + }) + React.useEffect(() => { + if (open) { + form.reset({ id: user.id, displayId: Number(user.displayId), difficulty: user.difficulty || "" }) + } + }, [open, user, form]) + async function onSubmit(formData: any) { + try { + setIsLoading(true) + const submitData = { + ...formData, + displayId: Number(formData.displayId), + } + await problemApi.updateProblem(submitData) + problemApi.getProblems().then(setData) + onOpenChange(false) + toast.success('修改成功') + } catch { + toast.error("修改失败") + } finally { + setIsLoading(false) + } + } + + return ( + + + + {config.actions.edit.label} + + 修改信息 + + +
+
+ {config.formFields.map((field) => ( +
+ + + {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && ( +

+ {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string} +

+ )} +
+ ))} +
+ + + +
+
+
+ ) + } + + // 用ref保证获取最新data + const dataRef = React.useRef(data) + React.useEffect(() => { dataRef.current = data }, [data]) + + return ( + +
+
+ {config.title} +
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + const columnNameMap: Record = { + select: "选择", + id: "ID", + name: "姓名", + email: "邮箱", + password: "密码", + createdAt: "创建时间", + actions: "操作", + } + return ( + + column.toggleVisibility(!!value) + } + > + {columnNameMap[column.id] || column.id} + + ) + })} + + + + +
+
+ +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + 暂无数据 + + + )} + +
+
+
+ +
+
+ 共 {table.getFilteredRowModel().rows.length} 条记录 +
+
+
+

每页显示

+ +
+
+ 第 {table.getState().pagination.pageIndex + 1} 页,共{" "} + {table.getPageCount()} 页 +
+
+ + +
+ 跳转到 + setPageInput(Number(e.target.value))} + onKeyDown={(e) => { + if (e.key === 'Enter') { + const page = pageInput - 1 + if (page >= 0 && page < table.getPageCount()) { + table.setPageIndex(page) + } + } + }} + className="w-16 h-8 text-sm" + /> + +
+ + +
+
+
+ + {/* 添加用户对话框 */} + {isProblem ? ( + + ) : ( + + )} + + {/* 编辑用户对话框 */} + {isProblem && editingUser ? ( + + ) : editingUser ? ( + + ) : null} + + {/* 删除确认对话框 */} + + + + 确认删除 + + {deleteBatch + ? `确定要删除选中的 ${table.getFilteredSelectedRowModel().rows.length} 条记录吗?此操作不可撤销。` + : "确定要删除这条记录吗?此操作不可撤销。" + } + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/src/features/user-management/config/admin.ts b/src/features/user-management/config/admin.ts new file mode 100644 index 0000000..f270b4a --- /dev/null +++ b/src/features/user-management/config/admin.ts @@ -0,0 +1,130 @@ +import { z } from "zod" + +// 管理员数据校验 schema +export const adminSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string(), + password: z.string().optional(), + role: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string().optional(), +}) + +export type Admin = z.infer + +// 添加管理员表单校验 schema +export const addAdminSchema = z.object({ + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址"), + password: z.string().optional(), + createdAt: z.string(), +}) + +// 编辑管理员表单校验 schema +export const editAdminSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址"), + password: z.string().optional(), + createdAt: z.string(), +}) + +export type AddAdminFormData = z.infer +export type EditAdminFormData = z.infer + +// 管理员配置 +export const adminConfig = { + userType: "admin", + title: "管理员列表", + apiPath: "/api/user", + + // 表格列配置 + columns: [ + { + key: "id", + label: "ID", + sortable: true, + }, + { + key: "name", + label: "姓名", + sortable: true, + searchable: true, + placeholder: "搜索姓名", + }, + { + key: "email", + label: "邮箱", + sortable: true, + searchable: true, + placeholder: "搜索邮箱", + }, + { + key: "password", + label: "密码", + }, + { + key: "createdAt", + label: "创建时间", + sortable: true, + }, + ], + + // 表单字段配置 + formFields: [ + { + key: "name", + label: "姓名", + type: "text", + placeholder: "请输入管理员姓名(选填)", + required: false, + }, + { + key: "email", + label: "邮箱", + type: "email", + placeholder: "请输入管理员邮箱", + required: true, + }, + { + key: "password", + label: "密码", + type: "password", + placeholder: "请输入密码(选填)", + required: false, + }, + { + key: "createdAt", + label: "创建时间", + type: "datetime-local", + required: true, + }, + ], + + // 操作按钮配置 + actions: { + add: { + label: "添加管理员", + icon: "PlusIcon", + }, + edit: { + label: "编辑", + icon: "PencilIcon", + }, + delete: { + label: "删除", + icon: "TrashIcon", + }, + batchDelete: { + label: "批量删除", + icon: "TrashIcon", + }, + }, + + // 分页配置 + pagination: { + pageSizes: [10, 50, 100, 500], + defaultPageSize: 10, + }, +} \ No newline at end of file diff --git a/src/features/user-management/config/problem.ts b/src/features/user-management/config/problem.ts new file mode 100644 index 0000000..cf459ac --- /dev/null +++ b/src/features/user-management/config/problem.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +export const problemSchema = z.object({ + id: z.string(), + displayId: z.number(), + difficulty: z.string(), + createdAt: z.string(), +}); + +export const addProblemSchema = z.object({ + displayId: z.number(), + difficulty: z.string(), +}); + +export const editProblemSchema = z.object({ + id: z.string(), + displayId: z.number(), + difficulty: z.string(), +}); + +export const problemConfig = { + userType: "problem", + title: "题目列表", + apiPath: "/api/problem", + columns: [ + { key: "id", label: "ID", sortable: true }, + { key: "displayId", label: "题目编号", sortable: true, searchable: true, placeholder: "搜索编号" }, + { key: "difficulty", label: "难度", sortable: true, searchable: true, placeholder: "搜索难度" }, + { key: "createdAt", label: "创建时间", sortable: true }, + ], + formFields: [ + { key: "displayId", label: "题目编号", type: "number", required: true }, + { key: "difficulty", label: "难度", type: "text", required: true }, + ], + actions: { + add: { label: "添加题目", icon: "PlusIcon" }, + edit: { label: "编辑", icon: "PencilIcon" }, + delete: { label: "删除", icon: "TrashIcon" }, + batchDelete: { label: "批量删除", icon: "TrashIcon" }, + }, + pagination: { pageSizes: [10, 50, 100, 500], defaultPageSize: 10 }, +}; \ No newline at end of file diff --git a/src/features/user-management/config/student.ts b/src/features/user-management/config/student.ts new file mode 100644 index 0000000..91a2f17 --- /dev/null +++ b/src/features/user-management/config/student.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +export const studentSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string(), + password: z.string().optional(), + role: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string().optional(), +}); + +export const addStudentSchema = z.object({ + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址"), + password: z.string().optional(), + createdAt: z.string(), +}); + +export const editStudentSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址"), + password: z.string().optional(), + createdAt: z.string(), +}); + +export const studentConfig = { + userType: "student", + title: "学生列表", + apiPath: "/api/user", + columns: [ + { key: "id", label: "ID", sortable: true }, + { key: "name", label: "姓名", sortable: true, searchable: true, placeholder: "搜索姓名" }, + { key: "email", label: "邮箱", sortable: true, searchable: true, placeholder: "搜索邮箱" }, + { key: "password", label: "密码" }, + { key: "createdAt", label: "创建时间", sortable: true }, + ], + formFields: [ + { key: "name", label: "姓名", type: "text", placeholder: "请输入学生姓名(选填)", required: false }, + { key: "email", label: "邮箱", type: "email", placeholder: "请输入学生邮箱", required: true }, + { key: "password", label: "密码", type: "password", placeholder: "请输入密码(选填)", required: false }, + { key: "createdAt", label: "创建时间", type: "datetime-local", required: true }, + ], + actions: { + add: { label: "添加学生", icon: "PlusIcon" }, + edit: { label: "编辑", icon: "PencilIcon" }, + delete: { label: "删除", icon: "TrashIcon" }, + batchDelete: { label: "批量删除", icon: "TrashIcon" }, + }, + pagination: { pageSizes: [10, 50, 100, 500], defaultPageSize: 10 }, +}; \ No newline at end of file diff --git a/src/features/user-management/config/teacher.ts b/src/features/user-management/config/teacher.ts new file mode 100644 index 0000000..b8046b6 --- /dev/null +++ b/src/features/user-management/config/teacher.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +export const teacherSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string(), + password: z.string().optional(), + role: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string().optional(), +}); + +export const addTeacherSchema = z.object({ + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址"), + password: z.string().optional(), + createdAt: z.string(), +}); + +export const editTeacherSchema = z.object({ + id: z.string(), + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址"), + password: z.string().optional(), + createdAt: z.string(), +}); + +export const teacherConfig = { + userType: "teacher", + title: "教师列表", + apiPath: "/api/user", + columns: [ + { key: "id", label: "ID", sortable: true }, + { key: "name", label: "姓名", sortable: true, searchable: true, placeholder: "搜索姓名" }, + { key: "email", label: "邮箱", sortable: true, searchable: true, placeholder: "搜索邮箱" }, + { key: "password", label: "密码" }, + { key: "createdAt", label: "创建时间", sortable: true }, + ], + formFields: [ + { key: "name", label: "姓名", type: "text", placeholder: "请输入教师姓名(选填)", required: false }, + { key: "email", label: "邮箱", type: "email", placeholder: "请输入教师邮箱", required: true }, + { key: "password", label: "密码", type: "password", placeholder: "请输入密码(选填)", required: false }, + { key: "createdAt", label: "创建时间", type: "datetime-local", required: true }, + ], + actions: { + add: { label: "添加教师", icon: "PlusIcon" }, + edit: { label: "编辑", icon: "PencilIcon" }, + delete: { label: "删除", icon: "TrashIcon" }, + batchDelete: { label: "批量删除", icon: "TrashIcon" }, + }, + pagination: { pageSizes: [10, 50, 100, 500], defaultPageSize: 10 }, +}; \ No newline at end of file diff --git a/src/features/user-management/index.tsx b/src/features/user-management/index.tsx new file mode 100644 index 0000000..d008b73 --- /dev/null +++ b/src/features/user-management/index.tsx @@ -0,0 +1,50 @@ +"use client" + +import { UserTable } from "./components/user-table" +import { adminConfig } from "./config/admin" +import { teacherConfig } from "./config/teacher" +import { studentConfig } from "./config/student" +import { problemConfig } from "./config/problem" + +interface UserManagementProps { + userType: "admin" | "teacher" | "student" | "problem" +} + +export function UserManagement({ userType }: UserManagementProps) { + // 根据用户类型返回对应的配置 + if (userType === "admin") { + return ( + + ) + } + if (userType === "teacher") { + return ( + + ) + } + if (userType === "student") { + return ( + + ) + } + if (userType === "problem") { + return ( + + ) + } + + // 后续可以添加 teacher 和 student 的配置 + return
暂不支持 {userType} 类型
+} \ No newline at end of file diff --git a/src/types/problem.ts b/src/types/problem.ts new file mode 100644 index 0000000..e236c91 --- /dev/null +++ b/src/types/problem.ts @@ -0,0 +1,12 @@ +export interface Problem { + id: string; + displayId: number; + difficulty: string; + isPublished: boolean; + isTrim: boolean; + timeLimit: number; + memoryLimit: number; + userId?: string | null; + createdAt: string; + updatedAt: string; +} \ No newline at end of file