refactor(user-management): 重构用户管理系统

- 移除原有的 user 和 problem API,改为使用 Prisma 直接操作数据库
- 新增 admin、teacher、guest 和 problem 的 CRUD 操作
- 优化用户表格组件,支持角色选择和难度选择
- 重构页面组件,使用 Prisma 查询数据
- 更新数据库迁移,增加 TEACHER 角色
This commit is contained in:
liguang 2025-06-20 15:29:50 +08:00
parent db8051d1d8
commit 360399bdfb
23 changed files with 315 additions and 471 deletions

View File

@ -0,0 +1,19 @@
/*
Warnings:
- The values [GUEST] on the enum `Role` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "Role_new" AS ENUM ('ADMIN', 'TEACHER', 'GUEST');
ALTER TABLE "User" ALTER COLUMN "role" DROP DEFAULT;
ALTER TABLE "User" ALTER COLUMN "role" TYPE "Role_new" USING ("role"::text::"Role_new");
ALTER TYPE "Role" RENAME TO "Role_old";
ALTER TYPE "Role_new" RENAME TO "Role";
DROP TYPE "Role_old";
ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'GUEST';
COMMIT;
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'GUEST';

View File

@ -10,6 +10,7 @@ generator client {
enum Role { enum Role {
ADMIN ADMIN
TEACHER
GUEST GUEST
} }

View File

@ -1,40 +0,0 @@
import type { Problem } from "@/types/problem";
// 获取所有题目
export async function getProblems(): Promise<Problem[]> {
const res = await fetch("/api/problem");
if (!res.ok) throw new Error("获取题目失败");
return res.json();
}
// 新建题目
export async function createProblem(data: Partial<Problem>): Promise<Problem> {
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<Problem>): Promise<Problem> {
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<void> {
const res = await fetch("/api/problem", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
if (!res.ok) throw new Error("删除题目失败");
}

View File

@ -1,52 +0,0 @@
import type { UserBase } from "@/types/user";
// 获取所有用户
export async function getUsers(userType: string): Promise<UserBase[]> {
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<UserBase>): Promise<UserBase> {
const res = await fetch(`/api/user`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || "新建用户失败");
}
return res.json();
}
// 更新用户
export async function updateUser(userType: string, data: Partial<UserBase>): Promise<UserBase> {
const res = await fetch(`/api/user`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
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<void> {
const res = await fetch(`/api/user`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.error || "删除用户失败");
}
}

View File

@ -0,0 +1,23 @@
'use server'
import prisma from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import bcrypt from 'bcryptjs'
export async function createAdmin(data) {
let password = data.password
if (password) {
password = await bcrypt.hash(password, 10)
}
await prisma.user.create({ data: { ...data, password, role: 'ADMIN' } })
revalidatePath('/usermanagement/admin')
}
export async function updateAdmin(id, data) {
await prisma.user.update({ where: { id }, data })
revalidatePath('/usermanagement/admin')
}
export async function deleteAdmin(id) {
await prisma.user.delete({ where: { id } })
revalidatePath('/usermanagement/admin')
}

View File

@ -0,0 +1,23 @@
'use server'
import prisma from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import bcrypt from 'bcryptjs'
export async function createGuest(data) {
let password = data.password
if (password) {
password = await bcrypt.hash(password, 10)
}
await prisma.user.create({ data: { ...data, password, role: 'GUEST' } })
revalidatePath('/usermanagement/guest')
}
export async function updateGuest(id, data) {
await prisma.user.update({ where: { id }, data })
revalidatePath('/usermanagement/guest')
}
export async function deleteGuest(id) {
await prisma.user.delete({ where: { id } })
revalidatePath('/usermanagement/guest')
}

View File

@ -0,0 +1,18 @@
'use server'
import prisma from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function createProblem(data) {
await prisma.problem.create({ data })
revalidatePath('/usermanagement/problem')
}
export async function updateProblem(id, data) {
await prisma.problem.update({ where: { id }, data })
revalidatePath('/usermanagement/problem')
}
export async function deleteProblem(id) {
await prisma.problem.delete({ where: { id } })
revalidatePath('/usermanagement/problem')
}

View File

@ -0,0 +1,23 @@
'use server'
import prisma from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import bcrypt from 'bcryptjs'
export async function createTeacher(data) {
let password = data.password
if (password) {
password = await bcrypt.hash(password, 10)
}
await prisma.user.create({ data: { ...data, password, role: 'TEACHER' } })
revalidatePath('/usermanagement/teacher')
}
export async function updateTeacher(id, data) {
await prisma.user.update({ where: { id }, data })
revalidatePath('/usermanagement/teacher')
}
export async function deleteTeacher(id) {
await prisma.user.delete({ where: { id } })
revalidatePath('/usermanagement/teacher')
}

View File

@ -1,5 +1,8 @@
import { UserManagement } from "@/features/user-management" import { UserTable } from '@/features/user-management/components/user-table'
import { adminConfig } from '@/features/user-management/config/admin'
import prisma from '@/lib/prisma'
export default function Page() { export default async function AdminPage() {
return <UserManagement userType="admin" /> const data = await prisma.user.findMany({ where: { role: 'ADMIN' } })
return <UserTable config={adminConfig} data={data} />
} }

View File

@ -1,5 +1,5 @@
import ProtectedLayout from "../_components/ProtectedLayout"; import ProtectedLayout from "../_components/ProtectedLayout";
export default function StudentLayout({ children }: { children: React.ReactNode }) { export default function GuestLayout({ children }: { children: React.ReactNode }) {
return <ProtectedLayout allowedRoles={["ADMIN", "TEACHER"]}>{children}</ProtectedLayout>; return <ProtectedLayout allowedRoles={["ADMIN", "TEACHER"]}>{children}</ProtectedLayout>;
} }

View File

@ -0,0 +1,7 @@
import { UserTable } from '@/features/user-management/components/user-table'
import { guestConfig } from '@/features/user-management/config/guest'
import prisma from '@/lib/prisma'
export default async function GuestPage() {
const data = await prisma.user.findMany({ where: { role: 'GUEST' as any } })
return <UserTable config={guestConfig} data={data} />
}

View File

@ -1,5 +1,8 @@
import { UserManagement } from "@/features/user-management" import { UserTable } from '@/features/user-management/components/user-table'
import { problemConfig } from '@/features/user-management/config/problem'
import prisma from '@/lib/prisma'
export default function Page() { export default async function ProblemPage() {
return <UserManagement userType="problem" /> const data = await prisma.problem.findMany({})
return <UserTable config={problemConfig} data={data} />
} }

View File

@ -1,5 +0,0 @@
import { UserManagement } from "@/features/user-management"
export default function Page() {
return <UserManagement userType="student" />
}

View File

@ -1,5 +1,8 @@
import { UserManagement } from "@/features/user-management" import { UserTable } from '@/features/user-management/components/user-table'
import { teacherConfig } from '@/features/user-management/config/teacher'
import prisma from '@/lib/prisma'
export default function Page() { export default async function TeacherPage() {
return <UserManagement userType="teacher" /> const data = await prisma.user.findMany({ where: { role: 'TEACHER' as any } })
return <UserTable config={teacherConfig} data={data} />
} }

View File

@ -1,96 +0,0 @@
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 });
}
}

View File

@ -1,189 +0,0 @@
import prisma from "@/lib/prisma";
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";
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") {
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) {
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 });
}
}
// 编辑用户
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") {
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 });
}
}
// 删除用户
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") {
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 });
}
}

View File

@ -30,6 +30,7 @@ import { useState, useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod" import { z } from "zod"
import { useRouter } from "next/navigation"
import { import {
Dialog, Dialog,
@ -68,8 +69,10 @@ import {
Tabs, Tabs,
} from "@/components/ui/tabs" } from "@/components/ui/tabs"
import * as userApi from "@/api/user" import { createAdmin, updateAdmin, deleteAdmin } from '@/app/(app)/usermanagement/_actions/adminActions'
import * as problemApi from "@/api/problem" import { createTeacher, updateTeacher, deleteTeacher } from '@/app/(app)/usermanagement/_actions/teacherActions'
import { createGuest, updateGuest, deleteGuest } from '@/app/(app)/usermanagement/_actions/guestActions'
import { createProblem, updateProblem, deleteProblem } from '@/app/(app)/usermanagement/_actions/problemActions'
// 通用用户类型 // 通用用户类型
export interface UserConfig { export interface UserConfig {
@ -89,6 +92,7 @@ export interface UserConfig {
type: string type: string
placeholder?: string placeholder?: string
required?: boolean required?: boolean
options?: Array<{ value: string; label: string }>
}> }>
actions: { actions: {
add: { label: string; icon: string } add: { label: string; icon: string }
@ -112,7 +116,7 @@ const addUserSchema = z.object({
name: z.string().optional(), name: z.string().optional(),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(), password: z.string().optional(),
createdAt: z.string(), createdAt: z.string().optional(),
}) })
const editUserSchema = z.object({ const editUserSchema = z.object({
@ -120,6 +124,7 @@ const editUserSchema = z.object({
name: z.string().optional(), name: z.string().optional(),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(), password: z.string().optional(),
role: z.string().optional(),
createdAt: z.string(), createdAt: z.string(),
}) })
@ -134,8 +139,7 @@ const editProblemSchema = z.object({
difficulty: z.string(), difficulty: z.string(),
}) })
export function UserTable({ config, data: initialData }: UserTableProps) { export function UserTable({ config, data }: UserTableProps) {
const [data, setData] = useState<any[]>(initialData)
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [editingUser, setEditingUser] = useState<any>(null) const [editingUser, setEditingUser] = useState<any>(null)
@ -212,6 +216,14 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
}, },
cell: ({ row }) => { cell: ({ row }) => {
const value = row.getValue(col.key) const value = row.getValue(col.key)
if (col.key === 'createdAt' || col.key === 'updatedAt') {
if (value instanceof Date) {
return value.toLocaleString()
}
if (typeof value === 'string' && !isNaN(Date.parse(value))) {
return new Date(value).toLocaleString()
}
}
return value return value
}, },
enableSorting: col.sortable !== false, enableSorting: col.sortable !== false,
@ -288,19 +300,6 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
getFacetedUniqueValues: getFacetedUniqueValues(), getFacetedUniqueValues: getFacetedUniqueValues(),
}) })
// 数据加载与API对接
useEffect(() => {
if (isProblem) {
problemApi.getProblems()
.then(setData)
.catch(() => toast.error('获取数据失败', { duration: 1500 }))
} else {
userApi.getUsers(config.userType)
.then(setData)
.catch(() => toast.error('获取数据失败', { duration: 1500 }))
}
}, [config.userType])
// 生成唯一ID // 生成唯一ID
function generateUniqueId(existingIds: string[]): string { function generateUniqueId(existingIds: string[]): string {
let id: string let id: string
@ -315,28 +314,41 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const form = useForm({ const form = useForm({
resolver: zodResolver(addUserSchema), resolver: zodResolver(addUserSchema),
defaultValues: { name: "", email: "", password: "", createdAt: new Date().toISOString().slice(0, 16) }, defaultValues: { name: "", email: "", password: "", createdAt: "" },
}) })
React.useEffect(() => { React.useEffect(() => {
if (open) { if (open) {
form.reset({ name: "", email: "", password: "", createdAt: new Date().toISOString().slice(0, 16) }) form.reset({ name: "", email: "", password: "", createdAt: "" })
} }
}, [open, form]) }, [open, form])
async function onSubmit(formData: any) { async function onSubmit(formData: any) {
try { try {
setIsLoading(true) setIsLoading(true)
const existingIds = dataRef.current.map(item => item.id)
const id = generateUniqueId(existingIds)
const submitData = { const submitData = {
...formData, ...formData,
id, // 移除手动生成的 id让数据库自动生成
createdAt: formData.createdAt ? new Date(formData.createdAt).toISOString() : new Date().toISOString(), // 移除 createdAt让数据库自动设置
} }
await userApi.createUser(config.userType, submitData) // 清理空字段
userApi.getUsers(config.userType).then(setData) if (!submitData.name) delete submitData.name
if (!submitData.password) delete submitData.password
if (!submitData.createdAt) delete submitData.createdAt
// 如果用户提供了创建时间,转换为完整的 ISO-8601 格式
if (submitData.createdAt) {
const date = new Date(submitData.createdAt)
submitData.createdAt = date.toISOString()
}
if (config.userType === 'admin') await createAdmin(submitData)
else if (config.userType === 'teacher') await createTeacher(submitData)
else if (config.userType === 'guest') await createGuest(submitData)
else if (config.userType === 'problem') await createProblem(submitData)
onOpenChange(false) onOpenChange(false)
toast.success('添加成功', { duration: 1500 }) toast.success('添加成功', { duration: 1500 })
} catch { router.refresh()
} catch (error) {
console.error('添加失败:', error)
toast.error('添加失败', { duration: 1500 }) toast.error('添加失败', { duration: 1500 })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@ -359,13 +371,29 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
<Label htmlFor={field.key} className="text-right"> <Label htmlFor={field.key} className="text-right">
{field.label} {field.label}
</Label> </Label>
<Input {field.type === 'select' && field.options ? (
id={field.key} <Select
type={field.type} value={String(form.watch(field.key as string) ?? '')}
{...form.register(field.key as 'name' | 'email' | 'password' | 'createdAt')} onValueChange={value => form.setValue(field.key as string, value)}
className="col-span-3" >
placeholder={field.placeholder} <SelectTrigger className="col-span-3">
/> <SelectValue placeholder={`请选择${field.label}`} />
</SelectTrigger>
<SelectContent>
{field.options.map((opt: any) => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
id={field.key}
type={field.type}
{...form.register(field.key as any)}
className="col-span-3"
placeholder={field.placeholder}
/>
)}
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && ( {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && (
<p className="col-span-3 col-start-2 text-sm text-red-500"> <p className="col-span-3 col-start-2 text-sm text-red-500">
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string} {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string}
@ -400,18 +428,20 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
async function onSubmit(formData: any) { async function onSubmit(formData: any) {
try { try {
setIsLoading(true) setIsLoading(true)
const existingIds = dataRef.current.map(item => item.id)
const id = generateUniqueId(existingIds)
const submitData = { const submitData = {
...formData, ...formData,
displayId: Number(formData.displayId), displayId: Number(formData.displayId),
id, // 移除手动生成的 id让数据库自动生成
} }
await problemApi.createProblem(submitData) if (config.userType === 'admin') await createAdmin(submitData)
problemApi.getProblems().then(setData) else if (config.userType === 'teacher') await createTeacher(submitData)
else if (config.userType === 'guest') await createGuest(submitData)
else if (config.userType === 'problem') await createProblem(submitData)
onOpenChange(false) onOpenChange(false)
toast.success('添加成功', { duration: 1500 }) toast.success('添加成功', { duration: 1500 })
} catch { router.refresh()
} catch (error) {
console.error('添加失败:', error)
toast.error('添加失败', { duration: 1500 }) toast.error('添加失败', { duration: 1500 })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@ -465,11 +495,11 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const form = useForm({ const form = useForm({
resolver: zodResolver(editUserSchema), 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) }, defaultValues: { id: user.id, name: user.name || "", email: user.email || "", password: "", role: user.role || "", createdAt: user.createdAt ? new Date(user.createdAt).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16) },
}) })
React.useEffect(() => { React.useEffect(() => {
if (open) { 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) }) form.reset({ id: user.id, name: user.name || "", email: user.email || "", password: "", role: user.role || "", createdAt: user.createdAt ? new Date(user.createdAt).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16) })
} }
}, [open, user, form]) }, [open, user, form])
async function onSubmit(formData: any) { async function onSubmit(formData: any) {
@ -482,8 +512,10 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
if (!submitData.password) { if (!submitData.password) {
delete submitData.password; delete submitData.password;
} }
await userApi.updateUser(config.userType, submitData) if (config.userType === 'admin') await updateAdmin(submitData.id, submitData)
userApi.getUsers(config.userType).then(setData) else if (config.userType === 'teacher') await updateTeacher(submitData.id, submitData)
else if (config.userType === 'guest') await updateGuest(submitData.id, submitData)
else if (config.userType === 'problem') await updateProblem(submitData.id, submitData)
onOpenChange(false) onOpenChange(false)
toast.success('修改成功', { duration: 1500 }) toast.success('修改成功', { duration: 1500 })
} catch { } catch {
@ -524,6 +556,43 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
)} )}
</div> </div>
))} ))}
{/* 编辑时显示角色选择 */}
{config.userType !== 'problem' && (
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="role" className="text-right">
</Label>
<Select
value={form.watch('role' as 'role')}
onValueChange={value => form.setValue('role' as 'role', value)}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="请选择角色" />
</SelectTrigger>
<SelectContent>
{config.userType === 'guest' && (
<>
<SelectItem value="GUEST"></SelectItem>
<SelectItem value="TEACHER"></SelectItem>
</>
)}
{(config.userType === 'teacher' || config.userType === 'admin') && (
<>
<SelectItem value="ADMIN"></SelectItem>
<SelectItem value="TEACHER"></SelectItem>
<SelectItem value="GUEST"></SelectItem>
</>
)}
</SelectContent>
</Select>
{form.formState.errors.role?.message && (
<p className="col-span-3 col-start-2 text-sm text-red-500">
{form.formState.errors.role?.message as string}
</p>
)}
</div>
)}
</div> </div>
<DialogFooter> <DialogFooter>
<Button type="submit" disabled={isLoading}> <Button type="submit" disabled={isLoading}>
@ -555,8 +624,10 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
...formData, ...formData,
displayId: Number(formData.displayId), displayId: Number(formData.displayId),
} }
await problemApi.updateProblem(submitData) if (config.userType === 'admin') await updateAdmin(submitData.id, submitData)
problemApi.getProblems().then(setData) else if (config.userType === 'teacher') await updateTeacher(submitData.id, submitData)
else if (config.userType === 'guest') await updateGuest(submitData.id, submitData)
else if (config.userType === 'problem') await updateProblem(submitData.id, submitData)
onOpenChange(false) onOpenChange(false)
toast.success('修改成功', { duration: 1500 }) toast.success('修改成功', { duration: 1500 })
} catch { } catch {
@ -592,6 +663,27 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
</p> </p>
)} )}
</div> </div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="difficulty" className="text-right"></Label>
<Select
value={form.watch('difficulty')}
onValueChange={value => form.setValue('difficulty', value)}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="请选择难度" />
</SelectTrigger>
<SelectContent>
<SelectItem value="EASY"></SelectItem>
<SelectItem value="MEDIUM"></SelectItem>
<SelectItem value="HARD"></SelectItem>
</SelectContent>
</Select>
{form.formState.errors.difficulty?.message && (
<p className="col-span-3 col-start-2 text-sm text-red-500">
{form.formState.errors.difficulty?.message as string}
</p>
)}
</div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button type="submit" disabled={isLoading}> <Button type="submit" disabled={isLoading}>
@ -608,6 +700,8 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
const dataRef = React.useRef<any[]>(data) const dataRef = React.useRef<any[]>(data)
React.useEffect(() => { dataRef.current = data }, [data]) React.useEffect(() => { dataRef.current = data }, [data])
const router = useRouter()
return ( return (
<Tabs defaultValue="outline" className="flex w-full flex-col gap-6"> <Tabs defaultValue="outline" className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between px-2 lg:px-4 py-2"> <div className="flex items-center justify-between px-2 lg:px-4 py-2">
@ -657,7 +751,25 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
})} })}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{config.actions.add && ( {isProblem && config.actions.add && (
<Button
variant="outline"
size="sm"
className="h-7 gap-1 px-2 text-sm"
onClick={async () => {
// 获取当前最大 displayId
const maxDisplayId = Array.isArray(data) && data.length > 0
? Math.max(...data.map(item => Number(item.displayId) || 0), 1000)
: 1000;
await createProblem({ displayId: maxDisplayId + 1, difficulty: "EASY" });
router.refresh();
}}
>
<PlusIcon className="h-4 w-4" />
{config.actions.add.label}
</Button>
)}
{!isProblem && config.actions.add && (
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -866,25 +978,22 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
const selectedRows = table.getFilteredSelectedRowModel().rows const selectedRows = table.getFilteredSelectedRowModel().rows
for (const row of selectedRows) { for (const row of selectedRows) {
if (isProblem) { if (isProblem) {
await problemApi.deleteProblem(row.original.id) await deleteProblem(row.original.id)
problemApi.getProblems().then(setData)
} else { } else {
await userApi.deleteUser(config.userType, row.original.id) await deleteAdmin(row.original.id)
userApi.getUsers(config.userType).then(setData)
} }
} }
toast.success(`成功删除 ${selectedRows.length} 条记录`, { duration: 1500 }) toast.success(`成功删除 ${selectedRows.length} 条记录`, { duration: 1500 })
} else if (deleteTargetId) { } else if (deleteTargetId) {
if (isProblem) { if (isProblem) {
await problemApi.deleteProblem(deleteTargetId) await deleteProblem(deleteTargetId)
problemApi.getProblems().then(setData)
} else { } else {
await userApi.deleteUser(config.userType, deleteTargetId) await deleteAdmin(deleteTargetId)
userApi.getUsers(config.userType).then(setData)
} }
toast.success('删除成功', { duration: 1500 }) toast.success('删除成功', { duration: 1500 })
} }
setDeleteDialogOpen(false) setDeleteDialogOpen(false)
router.refresh()
} catch { } catch {
toast.error('删除失败', { duration: 1500 }) toast.error('删除失败', { duration: 1500 })
} }

View File

@ -60,10 +60,6 @@ export const adminConfig = {
searchable: true, searchable: true,
placeholder: "搜索邮箱", placeholder: "搜索邮箱",
}, },
{
key: "password",
label: "密码",
},
{ {
key: "createdAt", key: "createdAt",
label: "创建时间", label: "创建时间",
@ -98,7 +94,7 @@ export const adminConfig = {
key: "createdAt", key: "createdAt",
label: "创建时间", label: "创建时间",
type: "datetime-local", type: "datetime-local",
required: true, required: false,
}, },
], ],

View File

@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
export const studentSchema = z.object({ export const guestSchema = z.object({
id: z.string(), id: z.string(),
name: z.string().optional(), name: z.string().optional(),
email: z.string(), email: z.string(),
@ -10,14 +10,14 @@ export const studentSchema = z.object({
updatedAt: z.string().optional(), updatedAt: z.string().optional(),
}); });
export const addStudentSchema = z.object({ export const addGuestSchema = z.object({
name: z.string().min(1, "姓名为必填项"), name: z.string().min(1, "姓名为必填项"),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"), password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(), createdAt: z.string(),
}); });
export const editStudentSchema = z.object({ export const editGuestSchema = z.object({
id: z.string(), id: z.string(),
name: z.string().min(1, "姓名为必填项"), name: z.string().min(1, "姓名为必填项"),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
@ -25,25 +25,24 @@ export const editStudentSchema = z.object({
createdAt: z.string(), createdAt: z.string(),
}); });
export const studentConfig = { export const guestConfig = {
userType: "student", userType: "guest",
title: "学生列表", title: "客户列表",
apiPath: "/api/user", apiPath: "/api/user",
columns: [ columns: [
{ key: "id", label: "ID", sortable: true }, { key: "id", label: "ID", sortable: true },
{ key: "name", label: "姓名", sortable: true, searchable: true, placeholder: "搜索姓名" }, { key: "name", label: "姓名", sortable: true, searchable: true, placeholder: "搜索姓名" },
{ key: "email", label: "邮箱", sortable: true, searchable: true, placeholder: "搜索邮箱" }, { key: "email", label: "邮箱", sortable: true, searchable: true, placeholder: "搜索邮箱" },
{ key: "password", label: "密码" },
{ key: "createdAt", label: "创建时间", sortable: true }, { key: "createdAt", label: "创建时间", sortable: true },
], ],
formFields: [ formFields: [
{ key: "name", label: "姓名", type: "text", placeholder: "请输入学生姓名", required: true }, { key: "name", label: "姓名", type: "text", placeholder: "请输入客户姓名", required: true },
{ key: "email", label: "邮箱", type: "email", placeholder: "请输入学生邮箱", required: true }, { key: "email", label: "邮箱", type: "email", placeholder: "请输入客户邮箱", required: true },
{ key: "password", label: "密码", type: "password", placeholder: "请输入8-32位密码", required: true }, { key: "password", label: "密码", type: "password", placeholder: "请输入8-32位密码", required: true },
{ key: "createdAt", label: "创建时间", type: "datetime-local", required: true }, { key: "createdAt", label: "创建时间", type: "datetime-local", required: false },
], ],
actions: { actions: {
add: { label: "添加学生", icon: "PlusIcon" }, add: { label: "添加客户", icon: "PlusIcon" },
edit: { label: "编辑", icon: "PencilIcon" }, edit: { label: "编辑", icon: "PencilIcon" },
delete: { label: "删除", icon: "TrashIcon" }, delete: { label: "删除", icon: "TrashIcon" },
batchDelete: { label: "批量删除", icon: "TrashIcon" }, batchDelete: { label: "批量删除", icon: "TrashIcon" },

View File

@ -32,6 +32,7 @@ export const problemConfig = {
{ key: "difficulty", label: "难度", type: "text", required: true }, { key: "difficulty", label: "难度", type: "text", required: true },
], ],
actions: { actions: {
add: { label: "添加题目", icon: "PlusIcon" },
edit: { label: "编辑", icon: "PencilIcon" }, edit: { label: "编辑", icon: "PencilIcon" },
delete: { label: "删除", icon: "TrashIcon" }, delete: { label: "删除", icon: "TrashIcon" },
batchDelete: { label: "批量删除", icon: "TrashIcon" }, batchDelete: { label: "批量删除", icon: "TrashIcon" },

View File

@ -33,14 +33,13 @@ export const teacherConfig = {
{ key: "id", label: "ID", sortable: true }, { key: "id", label: "ID", sortable: true },
{ key: "name", label: "姓名", sortable: true, searchable: true, placeholder: "搜索姓名" }, { key: "name", label: "姓名", sortable: true, searchable: true, placeholder: "搜索姓名" },
{ key: "email", label: "邮箱", sortable: true, searchable: true, placeholder: "搜索邮箱" }, { key: "email", label: "邮箱", sortable: true, searchable: true, placeholder: "搜索邮箱" },
{ key: "password", label: "密码" },
{ key: "createdAt", label: "创建时间", sortable: true }, { key: "createdAt", label: "创建时间", sortable: true },
], ],
formFields: [ formFields: [
{ key: "name", label: "姓名", type: "text", placeholder: "请输入教师姓名", required: true }, { key: "name", label: "姓名", type: "text", placeholder: "请输入教师姓名", required: true },
{ key: "email", label: "邮箱", type: "email", placeholder: "请输入教师邮箱", required: true }, { key: "email", label: "邮箱", type: "email", placeholder: "请输入教师邮箱", required: true },
{ key: "password", label: "密码", type: "password", placeholder: "请输入8-32位密码", required: true }, { key: "password", label: "密码", type: "password", placeholder: "请输入8-32位密码", required: true },
{ key: "createdAt", label: "创建时间", type: "datetime-local", required: true }, { key: "createdAt", label: "创建时间", type: "datetime-local", required: false },
], ],
actions: { actions: {
add: { label: "添加教师", icon: "PlusIcon" }, add: { label: "添加教师", icon: "PlusIcon" },

View File

@ -3,11 +3,11 @@
import { UserTable } from "./components/user-table" import { UserTable } from "./components/user-table"
import { adminConfig } from "./config/admin" import { adminConfig } from "./config/admin"
import { teacherConfig } from "./config/teacher" import { teacherConfig } from "./config/teacher"
import { studentConfig } from "./config/student" import { guestConfig } from "./config/guest"
import { problemConfig } from "./config/problem" import { problemConfig } from "./config/problem"
interface UserManagementProps { interface UserManagementProps {
userType: "admin" | "teacher" | "student" | "problem" userType: "admin" | "teacher" | "guest" | "problem"
} }
export function UserManagement({ userType }: UserManagementProps) { export function UserManagement({ userType }: UserManagementProps) {
@ -28,10 +28,10 @@ export function UserManagement({ userType }: UserManagementProps) {
/> />
) )
} }
if (userType === "student") { if (userType === "guest") {
return ( return (
<UserTable <UserTable
config={studentConfig} config={guestConfig}
data={[]} data={[]}
/> />
) )
@ -45,6 +45,5 @@ export function UserManagement({ userType }: UserManagementProps) {
) )
} }
// 后续可以添加 teacher 和 student 的配置
return <div> {userType} </div> return <div> {userType} </div>
} }

View File

@ -14,6 +14,6 @@ export interface Admin extends UserBase {
export interface Teacher extends UserBase { export interface Teacher extends UserBase {
// 教师特有字段(如有) // 教师特有字段(如有)
} }
export interface Student extends UserBase { export interface Guest extends UserBase {
// 学生特有字段(如有) // 学生特有字段(如有)
} }