feat(admin): 新增管理员管理功能

- 新增管理员管理页面和相关 API
- 实现管理员用户的增删改查功能
- 添加用户管理相关的 API 和页面组件
- 更新 VSCode 设置,添加数据库连接配置
This commit is contained in:
liguang 2025-06-18 17:52:42 +08:00
parent 6df1f64376
commit 6bd06929a7
11 changed files with 1100 additions and 330 deletions

13
.vscode/settings.json vendored
View File

@ -2,5 +2,16 @@
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"messages" "messages"
], ],
"i18n-ally.keystyle": "nested" "i18n-ally.keystyle": "nested",
"sqltools.connections": [
{
"previewLimit": 50,
"server": "localhost",
"port": 5432,
"driver": "PostgreSQL",
"username": "postgres",
"database": "abc",
"name": "beiyu"
}
]
} }

40
src/api/user.ts Normal file
View File

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

View File

@ -1,15 +1,13 @@
import { DataTable } from "@/components/data-table" import { DataTable } from "@/components/data-table"
import { SiteHeader } from "@/components/site-header" import { SiteHeader } from "@/components/site-header"
import data from "./data.json"
export default function Page() { export default function Page() {
return ( return (
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
<SiteHeader /> <SiteHeader />
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
<div className="flex flex-1 flex-col p-4"> <div className="flex flex-1 flex-col p-4">
<DataTable data={data} /> <DataTable data={[]} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,43 @@
import prisma from "@/lib/prisma";
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";
// 获取所有管理员
export async function GET() {
const users = await prisma.user.findMany({ where: { role: "ADMIN" } });
return NextResponse.json(users);
}
// 新建管理员
export async function POST(req: NextRequest) {
const data = await req.json();
if (data.password) {
data.password = await bcrypt.hash(data.password, 10);
}
data.role = "ADMIN";
const user = await prisma.user.create({ data });
const { password, ...userWithoutPassword } = user;
return NextResponse.json(userWithoutPassword);
}
// 编辑管理员
export async function PUT(req: NextRequest) {
const data = await req.json();
if (data.password) {
data.password = await bcrypt.hash(data.password, 10);
}
data.role = "ADMIN";
const user = await prisma.user.update({
where: { id: data.id },
data,
});
const { password, ...userWithoutPassword } = user;
return NextResponse.json(userWithoutPassword);
}
// 删除管理员
export async function DELETE(req: NextRequest) {
const { id } = await req.json();
await prisma.user.delete({ where: { id } });
return NextResponse.json({ success: true });
}

41
src/app/api/user/route.ts Normal file
View File

@ -0,0 +1,41 @@
import prisma from "@/lib/prisma";
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";
// 获取所有管理员
export async function GET() {
const users = await prisma.user.findMany();
return NextResponse.json(users);
}
// 新建管理员
export async function POST(req: NextRequest) {
const data = await req.json();
if (data.password) {
data.password = await bcrypt.hash(data.password, 10);
}
const user = await prisma.user.create({ data });
const { password, ...userWithoutPassword } = user;
return NextResponse.json(userWithoutPassword);
}
// 编辑管理员
export async function PUT(req: NextRequest) {
const data = await req.json();
if (data.password) {
data.password = await bcrypt.hash(data.password, 10);
}
const user = await prisma.user.update({
where: { id: data.id },
data,
});
const { password, ...userWithoutPassword } = user;
return NextResponse.json(userWithoutPassword);
}
// 删除管理员
export async function DELETE(req: NextRequest) {
const { id } = await req.json();
await prisma.user.delete({ where: { id } });
return NextResponse.json({ success: true });
}

View File

@ -0,0 +1,10 @@
import AdminTable from "@/components/user-table/admin-table";
export default function AdminDashboardPage() {
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4"></h2>
<AdminTable />
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
import { z } from "zod";
import { UserTable } from "./index";
import type { Admin } from "@/types/user";
// 管理员表单校验 schema
const adminSchema = z.object({
name: z.string().optional(),
email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(),
role: z.string().optional(),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
});
// 管理员表格列配置
const adminColumns = [
{ key: "id" as keyof Admin, label: "ID" },
{ key: "name" as keyof Admin, label: "姓名" },
{ key: "email" as keyof Admin, label: "邮箱" },
{ key: "role" as keyof Admin, label: "角色" },
{ key: "createdAt" as keyof Admin, label: "创建时间" },
];
// 管理员表单字段配置
const adminFormFields = [
{ key: "name" as keyof Admin, label: "姓名" },
{ key: "email" as keyof Admin, label: "邮箱", type: "email", required: true },
{ key: "password" as keyof Admin, label: "密码", type: "password" },
{ key: "role" as keyof Admin, label: "角色" },
];
export default function AdminTable() {
return (
<UserTable<Admin>
userType="admin"
columns={adminColumns}
schema={adminSchema}
formFields={adminFormFields}
/>
);
}

View File

@ -0,0 +1,198 @@
import * as React from "react";
import { useState, useEffect, useRef } from "react";
import { toast } from "sonner";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import * as userApi from "@/api/user";
import type { UserBase } from "@/types/user";
interface UserTableProps<T extends UserBase> {
userType: string;
columns: { key: keyof T; label: string; render?: (value: any, row: T) => React.ReactNode }[];
schema: any; // zod schema
formFields: { key: keyof T; label: string; type?: string; required?: boolean }[];
}
export function UserTable<T extends UserBase>({ userType, columns, schema, formFields }: UserTableProps<T>) {
const [data, setData] = useState<T[]>([]);
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editingUser, setEditingUser] = useState<T | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
const [deleteBatch, setDeleteBatch] = useState(false);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// 获取数据
useEffect(() => {
userApi.getUsers(userType)
.then(res => setData(res as T[]))
.catch(() => toast.error('获取数据失败'))
}, [userType])
// 添加用户表单
function AddUserDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<any>({
resolver: zodResolver(schema),
defaultValues: {},
});
React.useEffect(() => {
if (open) form.reset({});
}, [open, form]);
async function onSubmit(values: any) {
try {
setIsLoading(true);
await userApi.createUser(userType, values);
userApi.getUsers(userType).then(res => setData(res as T[]));
onOpenChange(false);
toast.success('添加成功');
} catch {
toast.error('添加失败');
} finally {
setIsLoading(false);
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{formFields.map(field => (
<div key={String(field.key)} className="grid grid-cols-4 items-center gap-4">
<Label htmlFor={String(field.key)} className="text-right">{field.label}</Label>
<Input
id={String(field.key)}
type={field.type || "text"}
{...form.register(String(field.key))}
className="col-span-3"
/>
</div>
))}
<DialogFooter>
<Button type="submit" disabled={isLoading}>{isLoading ? "添加中..." : "添加"}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
// 编辑用户表单
function EditUserDialog({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: T }) {
const [isLoading, setIsLoading] = useState(false);
const form = useForm<any>({
resolver: zodResolver(schema),
defaultValues: user,
});
React.useEffect(() => {
if (open) form.reset(user);
}, [open, user, form]);
async function onSubmit(values: any) {
try {
setIsLoading(true);
await userApi.updateUser(userType, { ...user, ...values });
userApi.getUsers(userType).then(res => setData(res as T[]));
onOpenChange(false);
toast.success('修改成功');
} catch {
toast.error('修改失败');
} finally {
setIsLoading(false);
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{formFields.map(field => (
<div key={String(field.key)} className="grid grid-cols-4 items-center gap-4">
<Label htmlFor={String(field.key)} className="text-right">{field.label}</Label>
<Input
id={String(field.key)}
type={field.type || "text"}
{...form.register(String(field.key))}
className="col-span-3"
/>
</div>
))}
<DialogFooter>
<Button type="submit" disabled={isLoading}>{isLoading ? "修改中..." : "确认修改"}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
// 删除确认
async function handleDelete(ids: string[]) {
try {
await Promise.all(ids.map(id => userApi.deleteUser(userType, id)));
userApi.getUsers(userType).then(res => setData(res as T[]));
toast.success('删除成功');
} catch {
toast.error('删除失败');
}
}
return (
<div>
<div className="flex gap-2 mb-2">
<Button onClick={() => setIsAddDialogOpen(true)}></Button>
<Button variant="destructive" disabled={selectedIds.length === 0} onClick={() => { setDeleteBatch(true); setDeleteDialogOpen(true); }}></Button>
</div>
<table className="w-full border text-sm">
<thead>
<tr>
<th><input type="checkbox" checked={selectedIds.length === data.length && data.length > 0} onChange={e => setSelectedIds(e.target.checked ? data.map(d => d.id) : [])} /></th>
{columns.map(col => <th key={String(col.key)}>{col.label}</th>)}
<th></th>
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.id}>
<td><input type="checkbox" checked={selectedIds.includes(row.id)} onChange={e => setSelectedIds(ids => e.target.checked ? [...ids, row.id] : ids.filter(id => id !== row.id))} /></td>
{columns.map(col => <td key={String(col.key)}>{col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")}</td>)}
<td>
<Button size="sm" onClick={() => { setEditingUser(row); setIsEditDialogOpen(true); }}></Button>
<Button size="sm" variant="destructive" onClick={() => { setDeleteTargetId(row.id); setDeleteDialogOpen(true); }}></Button>
</td>
</tr>
))}
</tbody>
</table>
<AddUserDialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
{editingUser && <EditUserDialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen} user={editingUser} />}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}></Button>
<Button variant="destructive" onClick={async () => {
if (deleteBatch) {
await handleDelete(selectedIds);
setSelectedIds([]);
} else if (deleteTargetId) {
await handleDelete([deleteTargetId]);
}
setDeleteDialogOpen(false);
}}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

19
src/types/user.ts Normal file
View File

@ -0,0 +1,19 @@
export interface UserBase {
id: string;
name?: string;
email: string;
password?: string;
role?: string;
createdAt: string;
updatedAt?: string;
}
export interface Admin extends UserBase {
// 管理员特有字段(如有)
}
export interface Teacher extends UserBase {
// 教师特有字段(如有)
}
export interface Student extends UserBase {
// 学生特有字段(如有)
}