mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 01:10: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 {
|
||||
ADMIN
|
||||
TEACHER
|
||||
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() {
|
||||
return <UserManagement userType="admin" />
|
||||
export default async function AdminPage() {
|
||||
const data = await prisma.user.findMany({ where: { role: 'ADMIN' } })
|
||||
return <UserTable config={adminConfig} data={data} />
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
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>;
|
||||
}
|
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() {
|
||||
return <UserManagement userType="problem" />
|
||||
export default async function ProblemPage() {
|
||||
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() {
|
||||
return <UserManagement userType="teacher" />
|
||||
export default async function TeacherPage() {
|
||||
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 { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { z } from "zod"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@ -68,8 +69,10 @@ import {
|
||||
Tabs,
|
||||
} from "@/components/ui/tabs"
|
||||
|
||||
import * as userApi from "@/api/user"
|
||||
import * as problemApi from "@/api/problem"
|
||||
import { createAdmin, updateAdmin, deleteAdmin } from '@/app/(app)/usermanagement/_actions/adminActions'
|
||||
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 {
|
||||
@ -89,6 +92,7 @@ export interface UserConfig {
|
||||
type: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
options?: Array<{ value: string; label: string }>
|
||||
}>
|
||||
actions: {
|
||||
add: { label: string; icon: string }
|
||||
@ -112,7 +116,7 @@ const addUserSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().email("请输入有效的邮箱地址"),
|
||||
password: z.string().optional(),
|
||||
createdAt: z.string(),
|
||||
createdAt: z.string().optional(),
|
||||
})
|
||||
|
||||
const editUserSchema = z.object({
|
||||
@ -120,6 +124,7 @@ const editUserSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().email("请输入有效的邮箱地址"),
|
||||
password: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
createdAt: z.string(),
|
||||
})
|
||||
|
||||
@ -134,8 +139,7 @@ const editProblemSchema = z.object({
|
||||
difficulty: z.string(),
|
||||
})
|
||||
|
||||
export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
const [data, setData] = useState<any[]>(initialData)
|
||||
export function UserTable({ config, data }: UserTableProps) {
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState<any>(null)
|
||||
@ -212,6 +216,14 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
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
|
||||
},
|
||||
enableSorting: col.sortable !== false,
|
||||
@ -288,19 +300,6 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
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
|
||||
function generateUniqueId(existingIds: string[]): string {
|
||||
let id: string
|
||||
@ -315,28 +314,41 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const form = useForm({
|
||||
resolver: zodResolver(addUserSchema),
|
||||
defaultValues: { name: "", email: "", password: "", createdAt: new Date().toISOString().slice(0, 16) },
|
||||
defaultValues: { name: "", email: "", password: "", createdAt: "" },
|
||||
})
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
form.reset({ name: "", email: "", password: "", createdAt: new Date().toISOString().slice(0, 16) })
|
||||
form.reset({ name: "", email: "", password: "", createdAt: "" })
|
||||
}
|
||||
}, [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(),
|
||||
// 移除手动生成的 id,让数据库自动生成
|
||||
// 移除 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)
|
||||
toast.success('添加成功', { duration: 1500 })
|
||||
} catch {
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error('添加失败:', error)
|
||||
toast.error('添加失败', { duration: 1500 })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
@ -359,13 +371,29 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
<Label htmlFor={field.key} className="text-right">
|
||||
{field.label}
|
||||
</Label>
|
||||
<Input
|
||||
id={field.key}
|
||||
type={field.type}
|
||||
{...form.register(field.key as 'name' | 'email' | 'password' | 'createdAt')}
|
||||
className="col-span-3"
|
||||
placeholder={field.placeholder}
|
||||
/>
|
||||
{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
|
||||
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 && (
|
||||
<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}
|
||||
@ -400,18 +428,20 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
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,
|
||||
// 移除手动生成的 id,让数据库自动生成
|
||||
}
|
||||
await problemApi.createProblem(submitData)
|
||||
problemApi.getProblems().then(setData)
|
||||
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)
|
||||
toast.success('添加成功', { duration: 1500 })
|
||||
} catch {
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error('添加失败:', error)
|
||||
toast.error('添加失败', { duration: 1500 })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
@ -465,11 +495,11 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
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) },
|
||||
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(() => {
|
||||
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])
|
||||
async function onSubmit(formData: any) {
|
||||
@ -482,8 +512,10 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
if (!submitData.password) {
|
||||
delete submitData.password;
|
||||
}
|
||||
await userApi.updateUser(config.userType, submitData)
|
||||
userApi.getUsers(config.userType).then(setData)
|
||||
if (config.userType === 'admin') await updateAdmin(submitData.id, submitData)
|
||||
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)
|
||||
toast.success('修改成功', { duration: 1500 })
|
||||
} catch {
|
||||
@ -524,6 +556,43 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
)}
|
||||
</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>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
@ -555,8 +624,10 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
...formData,
|
||||
displayId: Number(formData.displayId),
|
||||
}
|
||||
await problemApi.updateProblem(submitData)
|
||||
problemApi.getProblems().then(setData)
|
||||
if (config.userType === 'admin') await updateAdmin(submitData.id, submitData)
|
||||
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)
|
||||
toast.success('修改成功', { duration: 1500 })
|
||||
} catch {
|
||||
@ -592,6 +663,27 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
</p>
|
||||
)}
|
||||
</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>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
@ -608,6 +700,8 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
const dataRef = React.useRef<any[]>(data)
|
||||
React.useEffect(() => { dataRef.current = data }, [data])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<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">
|
||||
@ -657,7 +751,25 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@ -866,25 +978,22 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
|
||||
const selectedRows = table.getFilteredSelectedRowModel().rows
|
||||
for (const row of selectedRows) {
|
||||
if (isProblem) {
|
||||
await problemApi.deleteProblem(row.original.id)
|
||||
problemApi.getProblems().then(setData)
|
||||
await deleteProblem(row.original.id)
|
||||
} else {
|
||||
await userApi.deleteUser(config.userType, row.original.id)
|
||||
userApi.getUsers(config.userType).then(setData)
|
||||
await deleteAdmin(row.original.id)
|
||||
}
|
||||
}
|
||||
toast.success(`成功删除 ${selectedRows.length} 条记录`, { duration: 1500 })
|
||||
} else if (deleteTargetId) {
|
||||
if (isProblem) {
|
||||
await problemApi.deleteProblem(deleteTargetId)
|
||||
problemApi.getProblems().then(setData)
|
||||
await deleteProblem(deleteTargetId)
|
||||
} else {
|
||||
await userApi.deleteUser(config.userType, deleteTargetId)
|
||||
userApi.getUsers(config.userType).then(setData)
|
||||
await deleteAdmin(deleteTargetId)
|
||||
}
|
||||
toast.success('删除成功', { duration: 1500 })
|
||||
}
|
||||
setDeleteDialogOpen(false)
|
||||
router.refresh()
|
||||
} catch {
|
||||
toast.error('删除失败', { duration: 1500 })
|
||||
}
|
||||
|
@ -60,10 +60,6 @@ export const adminConfig = {
|
||||
searchable: true,
|
||||
placeholder: "搜索邮箱",
|
||||
},
|
||||
{
|
||||
key: "password",
|
||||
label: "密码",
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
@ -98,7 +94,7 @@ export const adminConfig = {
|
||||
key: "createdAt",
|
||||
label: "创建时间",
|
||||
type: "datetime-local",
|
||||
required: true,
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const studentSchema = z.object({
|
||||
export const guestSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
email: z.string(),
|
||||
@ -10,14 +10,14 @@ export const studentSchema = z.object({
|
||||
updatedAt: z.string().optional(),
|
||||
});
|
||||
|
||||
export const addStudentSchema = z.object({
|
||||
export const addGuestSchema = z.object({
|
||||
name: z.string().min(1, "姓名为必填项"),
|
||||
email: z.string().email("请输入有效的邮箱地址"),
|
||||
password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
|
||||
createdAt: z.string(),
|
||||
});
|
||||
|
||||
export const editStudentSchema = z.object({
|
||||
export const editGuestSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1, "姓名为必填项"),
|
||||
email: z.string().email("请输入有效的邮箱地址"),
|
||||
@ -25,25 +25,24 @@ export const editStudentSchema = z.object({
|
||||
createdAt: z.string(),
|
||||
});
|
||||
|
||||
export const studentConfig = {
|
||||
userType: "student",
|
||||
title: "学生列表",
|
||||
export const guestConfig = {
|
||||
userType: "guest",
|
||||
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: true },
|
||||
{ key: "email", label: "邮箱", type: "email", placeholder: "请输入学生邮箱", required: true },
|
||||
{ key: "name", label: "姓名", type: "text", placeholder: "请输入客户姓名", required: true },
|
||||
{ key: "email", label: "邮箱", type: "email", placeholder: "请输入客户邮箱", 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: {
|
||||
add: { label: "添加学生", icon: "PlusIcon" },
|
||||
add: { label: "添加客户", icon: "PlusIcon" },
|
||||
edit: { label: "编辑", icon: "PencilIcon" },
|
||||
delete: { label: "删除", icon: "TrashIcon" },
|
||||
batchDelete: { label: "批量删除", icon: "TrashIcon" },
|
@ -32,6 +32,7 @@ export const problemConfig = {
|
||||
{ key: "difficulty", label: "难度", type: "text", required: true },
|
||||
],
|
||||
actions: {
|
||||
add: { label: "添加题目", icon: "PlusIcon" },
|
||||
edit: { label: "编辑", icon: "PencilIcon" },
|
||||
delete: { label: "删除", icon: "TrashIcon" },
|
||||
batchDelete: { label: "批量删除", icon: "TrashIcon" },
|
||||
|
@ -33,14 +33,13 @@ export const teacherConfig = {
|
||||
{ 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: true },
|
||||
{ key: "email", label: "邮箱", type: "email", placeholder: "请输入教师邮箱", 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: {
|
||||
add: { label: "添加教师", icon: "PlusIcon" },
|
||||
|
@ -3,11 +3,11 @@
|
||||
import { UserTable } from "./components/user-table"
|
||||
import { adminConfig } from "./config/admin"
|
||||
import { teacherConfig } from "./config/teacher"
|
||||
import { studentConfig } from "./config/student"
|
||||
import { guestConfig } from "./config/guest"
|
||||
import { problemConfig } from "./config/problem"
|
||||
|
||||
interface UserManagementProps {
|
||||
userType: "admin" | "teacher" | "student" | "problem"
|
||||
userType: "admin" | "teacher" | "guest" | "problem"
|
||||
}
|
||||
|
||||
export function UserManagement({ userType }: UserManagementProps) {
|
||||
@ -28,10 +28,10 @@ export function UserManagement({ userType }: UserManagementProps) {
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (userType === "student") {
|
||||
if (userType === "guest") {
|
||||
return (
|
||||
<UserTable
|
||||
config={studentConfig}
|
||||
config={guestConfig}
|
||||
data={[]}
|
||||
/>
|
||||
)
|
||||
@ -45,6 +45,5 @@ export function UserManagement({ userType }: UserManagementProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// 后续可以添加 teacher 和 student 的配置
|
||||
return <div>暂不支持 {userType} 类型</div>
|
||||
}
|
@ -14,6 +14,6 @@ export interface Admin 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