mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 09:20:53 +00:00
refactor(user-management): 重构用户管理系统
- 移除原有的 user 和 problem API,改为使用 Prisma 直接操作数据库 - 新增 admin、teacher、guest 和 problem 的 CRUD 操作 - 优化用户表格组件,支持角色选择和难度选择 - 重构页面组件,使用 Prisma 查询数据 - 更新数据库迁移,增加 TEACHER 角色
This commit is contained in:
parent
db8051d1d8
commit
360399bdfb
19
prisma/migrations/20250619101509_beiyu/migration.sql
Normal file
19
prisma/migrations/20250619101509_beiyu/migration.sql
Normal 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';
|
@ -10,6 +10,7 @@ generator client {
|
|||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
ADMIN
|
ADMIN
|
||||||
|
TEACHER
|
||||||
GUEST
|
GUEST
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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("删除题目失败");
|
|
||||||
}
|
|
@ -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 || "删除用户失败");
|
|
||||||
}
|
|
||||||
}
|
|
23
src/app/(app)/usermanagement/_actions/adminActions.ts
Normal file
23
src/app/(app)/usermanagement/_actions/adminActions.ts
Normal 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')
|
||||||
|
}
|
23
src/app/(app)/usermanagement/_actions/guestActions.ts
Normal file
23
src/app/(app)/usermanagement/_actions/guestActions.ts
Normal 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')
|
||||||
|
}
|
18
src/app/(app)/usermanagement/_actions/problemActions.ts
Normal file
18
src/app/(app)/usermanagement/_actions/problemActions.ts
Normal 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')
|
||||||
|
}
|
23
src/app/(app)/usermanagement/_actions/teacherActions.ts
Normal file
23
src/app/(app)/usermanagement/_actions/teacherActions.ts
Normal 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')
|
||||||
|
}
|
@ -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} />
|
||||||
}
|
}
|
@ -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>;
|
||||||
}
|
}
|
7
src/app/(app)/usermanagement/guest/page.tsx
Normal file
7
src/app/(app)/usermanagement/guest/page.tsx
Normal 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} />
|
||||||
|
}
|
@ -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} />
|
||||||
}
|
}
|
@ -1,5 +0,0 @@
|
|||||||
import { UserManagement } from "@/features/user-management"
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return <UserManagement userType="student" />
|
|
||||||
}
|
|
@ -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} />
|
||||||
}
|
}
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
||||||
|
{field.type === 'select' && field.options ? (
|
||||||
|
<Select
|
||||||
|
value={String(form.watch(field.key as string) ?? '')}
|
||||||
|
onValueChange={value => form.setValue(field.key as string, value)}
|
||||||
|
>
|
||||||
|
<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
|
<Input
|
||||||
id={field.key}
|
id={field.key}
|
||||||
type={field.type}
|
type={field.type}
|
||||||
{...form.register(field.key as 'name' | 'email' | 'password' | 'createdAt')}
|
{...form.register(field.key as any)}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
placeholder={field.placeholder}
|
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 })
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -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" },
|
@ -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" },
|
||||||
|
@ -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" },
|
||||||
|
@ -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>
|
||||||
}
|
}
|
@ -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 {
|
||||||
// 学生特有字段(如有)
|
// 学生特有字段(如有)
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user