From 6bd06929a7511d7dbee8460f414845b8fd93f1f0 Mon Sep 17 00:00:00 2001 From: liguang <1590686939@qq.com> Date: Wed, 18 Jun 2025 17:52:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=96=B0=E5=A2=9E=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增管理员管理页面和相关 API - 实现管理员用户的增删改查功能 - 添加用户管理相关的 API 和页面组件 - 更新 VSCode 设置,添加数据库连接配置 --- .vscode/settings.json | 13 +- src/api/user.ts | 40 + src/app/{dashboard => admin}/data.json | 0 src/app/{dashboard => admin}/page.tsx | 4 +- src/app/api/admin/route.ts | 43 + src/app/api/user/route.ts | 41 + src/app/dashboard/admin.tsx | 10 + src/components/data-table.tsx | 1021 ++++++++++++++------- src/components/user-table/admin-table.tsx | 41 + src/components/user-table/index.tsx | 198 ++++ src/types/user.ts | 19 + 11 files changed, 1100 insertions(+), 330 deletions(-) create mode 100644 src/api/user.ts rename src/app/{dashboard => admin}/data.json (100%) rename src/app/{dashboard => admin}/page.tsx (83%) create mode 100644 src/app/api/admin/route.ts create mode 100644 src/app/api/user/route.ts create mode 100644 src/app/dashboard/admin.tsx create mode 100644 src/components/user-table/admin-table.tsx create mode 100644 src/components/user-table/index.tsx create mode 100644 src/types/user.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index c99754e..c48ef70 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,16 @@ "i18n-ally.localesPaths": [ "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" + } + ] } \ No newline at end of file diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..6638a98 --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,40 @@ +import type { UserBase } from "@/types/user"; + +// 获取所有用户 +export async function getUsers(userType: string): Promise { + const res = await fetch(`/api/${userType}`); + if (!res.ok) throw new Error("获取用户失败"); + return res.json(); +} + +// 新建用户 +export async function createUser(userType: string, data: Partial): Promise { + 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): Promise { + 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 { + const res = await fetch(`/api/${userType}`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id }), + }); + if (!res.ok) throw new Error("删除用户失败"); +} \ No newline at end of file diff --git a/src/app/dashboard/data.json b/src/app/admin/data.json similarity index 100% rename from src/app/dashboard/data.json rename to src/app/admin/data.json diff --git a/src/app/dashboard/page.tsx b/src/app/admin/page.tsx similarity index 83% rename from src/app/dashboard/page.tsx rename to src/app/admin/page.tsx index 6128e58..2c1c7a6 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/admin/page.tsx @@ -1,15 +1,13 @@ import { DataTable } from "@/components/data-table" import { SiteHeader } from "@/components/site-header" -import data from "./data.json" - export default function Page() { return (
- +
diff --git a/src/app/api/admin/route.ts b/src/app/api/admin/route.ts new file mode 100644 index 0000000..b945dbf --- /dev/null +++ b/src/app/api/admin/route.ts @@ -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 }); +} \ No newline at end of file diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts new file mode 100644 index 0000000..720dc83 --- /dev/null +++ b/src/app/api/user/route.ts @@ -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 }); +} \ No newline at end of file diff --git a/src/app/dashboard/admin.tsx b/src/app/dashboard/admin.tsx new file mode 100644 index 0000000..7959311 --- /dev/null +++ b/src/app/dashboard/admin.tsx @@ -0,0 +1,10 @@ +import AdminTable from "@/components/user-table/admin-table"; + +export default function AdminDashboardPage() { + return ( +
+

管理员管理

+ +
+ ); +} \ No newline at end of file diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx index 9252210..c842320 100644 --- a/src/components/data-table.tsx +++ b/src/components/data-table.tsx @@ -50,10 +50,22 @@ import { EyeIcon, PencilIcon, TrashIcon, + ListFilter, } from "lucide-react" import { Area, AreaChart, CartesianGrid, XAxis } from "recharts" import { toast } from "sonner" import { z } from "zod" +import { useState, useEffect, useRef } from "react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" import { useIsMobile } from "@/hooks/use-mobile" import { Badge } from "@/components/ui/badge" @@ -108,15 +120,50 @@ import { TabsTrigger, } from "@/components/ui/tabs" -export const schema = z.object({ - id: z.number(), - name: z.string(), +import * as userApi from "@/api/user" + +// 定义管理员数据的 schema,与后端 User 类型保持一致 +const schema = z.object({ + id: z.string(), + name: z.string().optional(), email: z.string(), - createdAt: z.string().datetime(), + password: z.string().optional(), + role: z.string().optional(), + createdAt: z.string(), + updatedAt: z.string().optional(), }) +export type Admin = z.infer + +// 表单校验 schema +const addAdminSchema = z.object({ + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址").optional().or(z.literal("")), + password: z.string().optional(), + createdAt: z.string().optional(), +}) +type AddAdminFormData = z.infer + +const editAdminSchema = z.object({ + id: z.string().min(1, "ID不能为空"), + name: z.string().optional(), + email: z.string().email("请输入有效的邮箱地址").optional().or(z.literal("")), + password: z.string().optional(), + createdAt: z.string().optional(), +}) +type EditAdminFormData = z.infer + +// 生成唯一id +function generateUniqueId(existingIds: string[]): string { + let id; + do { + id = 'c' + Math.random().toString(36).slice(2, 12) + Date.now().toString(36).slice(-4); + } while (existingIds.includes(id)); + return id; +} + // Create a separate component for the drag handle -function DragHandle({ id }: { id: number }) { +function DragHandle({ id }: { id: string }) { const { attributes, listeners } = useSortable({ id, }) @@ -135,187 +182,20 @@ function DragHandle({ id }: { id: number }) { ) } -const columns: ColumnDef>[] = [ - { - id: "select", - header: ({ table }) => ( -
- table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> -
- ), - cell: ({ row }) => ( -
- row.toggleSelected(!!value)} - aria-label={`Select row ${row.original.id}`} - /> -
- ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "id", - header: "ID", - cell: ({ row }) =>
{row.original.id}
, - }, - { - accessorKey: "name", - header: "姓名", - cell: ({ row }) => , - }, - { - accessorKey: "email", - header: "邮箱", - cell: ({ row }) =>
{row.original.email}
, - }, - { - accessorKey: "createdAt", - header: "创建时间", - cell: ({ row }) => ( -
- {new Date(row.original.createdAt).toLocaleDateString()} -
- ), - }, - { - id: "actions", - enableHiding: false, - header: () =>
操作
, - cell: ({ row }) => { - const item = row.original - return ( -
- - - -
- ) - }, - } -] - -function DraggableRow({ row }: { row: Row> }) { - const { transform, transition, setNodeRef, isDragging } = useSortable({ - id: row.original.id, - }) - - return ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ) -} - -function TableCellViewer({ item }: { item: z.infer }) { - const isMobile = useIsMobile() - - return ( - - - - - - - ID: {item.id} - 姓名: {item.name} - -
-
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
-
- - - - - - -
-
- ) -} - export function DataTable({ data: initialData, }: { - data: z.infer[] + data: Admin[] }) { - const [data, setData] = React.useState(() => initialData) - const [rowSelection, setRowSelection] = React.useState({}) - const [columnVisibility, setColumnVisibility] = React.useState({ - drag: false, - // 仅保留必要列可见性控制 - }) - const [columnFilters, setColumnFilters] = React.useState([]) - const [sorting, setSorting] = React.useState([]) - const [pagination, setPagination] = React.useState({ + const [data, setData] = useState(initialData) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [editingAdmin, setEditingAdmin] = useState(null) + const [rowSelection, setRowSelection] = useState({}) + const [columnVisibility, setColumnVisibility] = useState({}) + const [columnFilters, setColumnFilters] = useState([]) + const [sorting, setSorting] = useState([]) + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }) @@ -331,9 +211,134 @@ export function DataTable({ [data] ) + // 搜索输入本地 state + const [nameSearch, setNameSearch] = useState(""); + const [emailSearch, setEmailSearch] = useState(""); + + // 删除确认对话框相关state + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [deleteTargetId, setDeleteTargetId] = useState(null) + const [deleteBatch, setDeleteBatch] = useState(false) + + // 页码输入本地state + const [pageInput, setPageInput] = useState(pagination.pageIndex + 1) + useEffect(() => { + setPageInput(pagination.pageIndex + 1) + }, [pagination.pageIndex]) + + const tableColumns = React.useMemo[]>(() => [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="选择所有" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="选择行" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "id", + header: "ID", + }, + { + accessorKey: "name", + header: ({ column }) => ( +
+ 姓名 + { + const v = column.getFilterValue(); + return typeof v === 'string' ? v : ''; + })()} + onChange={e => column.setFilterValue(e.target.value)} + style={{ minWidth: 0 }} + /> +
+ ), + }, + { + accessorKey: "email", + header: ({ column }) => ( +
+ 邮箱 + { + const v = column.getFilterValue(); + return typeof v === 'string' ? v : ''; + })()} + onChange={e => column.setFilterValue(e.target.value)} + style={{ minWidth: 0 }} + /> +
+ ), + }, + { + accessorKey: "password", + header: "密码", + cell: ({ row }) => row.getValue("password") ? "******" : "(无)", + }, + { + accessorKey: "createdAt", + header: "创建时间", + cell: ({ row }) => { + const date = new Date(row.getValue("createdAt")) + return date.toLocaleString() + }, + }, + { + id: "actions", + header: () =>
操作
, + cell: ({ row }) => { + const admin = row.original + return ( +
+ + +
+ ) + }, + }, + ], []); + const table = useReactTable({ data, - columns, + columns: tableColumns, state: { sorting, columnVisibility, @@ -341,7 +346,6 @@ export function DataTable({ columnFilters, pagination, }, - getRowId: (row) => row.id.toString(), enableRowSelection: true, onRowSelectionChange: setRowSelection, onSortingChange: setSorting, @@ -354,7 +358,17 @@ export function DataTable({ getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), - }) + }); + + // useEffect 同步 filterValue 到本地 state + useEffect(() => { + const v = table.getColumn("name")?.getFilterValue(); + setNameSearch(typeof v === 'string' ? v : ""); + }, [table.getColumn("name")?.getFilterValue()]); + useEffect(() => { + const v = table.getColumn("email")?.getFilterValue(); + setEmailSearch(typeof v === 'string' ? v : ""); + }, [table.getColumn("email")?.getFilterValue()]); function handleDragEnd(event: DragEndEvent) { const { active, over } = event @@ -367,162 +381,517 @@ export function DataTable({ } } + // 数据加载与API对接 + useEffect(() => { + userApi.getUsers() + .then(setData) + .catch(() => toast.error('获取管理员数据失败')) + }, []) + + // 添加管理员对话框组件 + function AddAdminDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { + const [isLoading, setIsLoading] = useState(false) + const form = useForm({ + resolver: zodResolver(addAdminSchema), + defaultValues: { + name: "", + email: "", + password: "", + createdAt: new Date().toISOString().slice(0, 16), + }, + }) + React.useEffect(() => { + if (open) { + form.reset({ + name: "", + email: "", + password: "", + createdAt: new Date().toISOString().slice(0, 16), + }) + } + }, [open, form]) + async function onSubmit(data: AddAdminFormData) { + try { + setIsLoading(true) + const existingIds = dataRef.current.map(item => item.id) + const id = generateUniqueId(existingIds) + const submitData = { + ...data, + id, + createdAt: data.createdAt ? new Date(data.createdAt).toISOString() : new Date().toISOString(), + }; + await userApi.createUser(submitData) + userApi.getUsers().then(setData) + onOpenChange(false) + toast.success('添加成功') + } catch (error) { + toast.error("添加管理员失败") + } finally { + setIsLoading(false) + } + } + return ( + + + + 添加管理员 + + 请填写管理员信息,ID自动生成。 + + +
+
+
+ + + {form.formState.errors.name && ( +

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

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

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

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

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

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

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

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

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

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

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

+ )} +
+
+ + +
+
+ + + +
+
+
+ ) + } + + // 用ref保证获取最新data + const dataRef = React.useRef(data) + React.useEffect(() => { dataRef.current = data }, [data]) + return ( -
- {/* 修改视图标题区域为左侧固定内容 */} -
+
+
管理员列表
- -
- -
- - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - - {table.getRowModel().rows.map((row) => ( - - ))} - - ) : ( - - - 暂无数据 - - - )} - -
-
-
- {/* 仅调整背景色,保持原有分页结构 */} -
-
- {table.getFilteredSelectedRowModel().rows.length} / {table.getFilteredRowModel().rows.length} 行已选择 -
-
-
- - setPagination(p => ({ ...p, pageSize: Number(value), pageIndex: 0 }))} + > + + - - {[10, 20, 30, 40, 50].map((pageSize) => ( - - 每页{pageSize}条 - - ))} + + 10 + 50 + 100 + 500 +
-
- 第 {table.getState().pagination.pageIndex + 1} 页 - 共 {table.getPageCount()} 页 -
-
- - - - + {/* 页码跳转 */} +
+ 跳转到 + setPageInput(Number(e.target.value))} + onBlur={() => { + let page = Number(pageInput) - 1 + if (isNaN(page) || page < 0) page = 0 + if (page >= table.getPageCount()) page = table.getPageCount() - 1 + setPagination(p => ({ ...p, pageIndex: page })) + }} + onKeyDown={e => { + if (e.key === 'Enter') { + let page = Number(pageInput) - 1 + if (isNaN(page) || page < 0) page = 0 + if (page >= table.getPageCount()) page = table.getPageCount() - 1 + setPagination(p => ({ ...p, pageIndex: page })) + } + }} + className="w-14 h-7 text-xs px-1" + style={{ minWidth: 0 }} + /> +
+ +
- - -
-
- -
-
- -
-
+
+ + {editingAdmin && ( + + )} + {/* 删除确认对话框 */} + + + + 确认删除 + + {deleteBatch + ? `确定要删除选中的所有管理员吗?` + : `确定要删除该管理员吗?`} + + + + + + + + ) } diff --git a/src/components/user-table/admin-table.tsx b/src/components/user-table/admin-table.tsx new file mode 100644 index 0000000..147fcc0 --- /dev/null +++ b/src/components/user-table/admin-table.tsx @@ -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 ( + + userType="admin" + columns={adminColumns} + schema={adminSchema} + formFields={adminFormFields} + /> + ); +} \ No newline at end of file diff --git a/src/components/user-table/index.tsx b/src/components/user-table/index.tsx new file mode 100644 index 0000000..4fa0c6d --- /dev/null +++ b/src/components/user-table/index.tsx @@ -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 { + userType: string; + columns: { key: keyof T; label: string; render?: (value: any, row: T) => React.ReactNode }[]; + schema: any; // zod schema + formFields: { key: keyof T; label: string; type?: string; required?: boolean }[]; +} + +export function UserTable({ userType, columns, schema, formFields }: UserTableProps) { + const [data, setData] = useState([]); + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + const [deleteBatch, setDeleteBatch] = useState(false); + const [selectedIds, setSelectedIds] = useState([]); + + // 获取数据 + useEffect(() => { + userApi.getUsers(userType) + .then(res => setData(res as T[])) + .catch(() => toast.error('获取数据失败')) + }, [userType]) + + // 添加用户表单 + function AddUserDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { + const [isLoading, setIsLoading] = useState(false); + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: {}, + }); + React.useEffect(() => { + if (open) form.reset({}); + }, [open, form]); + async function onSubmit(values: any) { + try { + setIsLoading(true); + await userApi.createUser(userType, values); + userApi.getUsers(userType).then(res => setData(res as T[])); + onOpenChange(false); + toast.success('添加成功'); + } catch { + toast.error('添加失败'); + } finally { + setIsLoading(false); + } + } + return ( + + + + 添加 + +
+ {formFields.map(field => ( +
+ + +
+ ))} + + + +
+
+
+ ); + } + + // 编辑用户表单 + function EditUserDialog({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: T }) { + const [isLoading, setIsLoading] = useState(false); + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: user, + }); + React.useEffect(() => { + if (open) form.reset(user); + }, [open, user, form]); + async function onSubmit(values: any) { + try { + setIsLoading(true); + await userApi.updateUser(userType, { ...user, ...values }); + userApi.getUsers(userType).then(res => setData(res as T[])); + onOpenChange(false); + toast.success('修改成功'); + } catch { + toast.error('修改失败'); + } finally { + setIsLoading(false); + } + } + return ( + + + + 编辑 + +
+ {formFields.map(field => ( +
+ + +
+ ))} + + + +
+
+
+ ); + } + + // 删除确认 + async function handleDelete(ids: string[]) { + try { + await Promise.all(ids.map(id => userApi.deleteUser(userType, id))); + userApi.getUsers(userType).then(res => setData(res as T[])); + toast.success('删除成功'); + } catch { + toast.error('删除失败'); + } + } + + return ( +
+
+ + +
+ + + + + {columns.map(col => )} + + + + + {data.map(row => ( + + + {columns.map(col => )} + + + ))} + +
0} onChange={e => setSelectedIds(e.target.checked ? data.map(d => d.id) : [])} />{col.label}操作
setSelectedIds(ids => e.target.checked ? [...ids, row.id] : ids.filter(id => id !== row.id))} />{col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")} + + +
+ + {editingUser && } + + + + 确认删除 + + + + + + + +
+ ); +} \ No newline at end of file diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..e8724fd --- /dev/null +++ b/src/types/user.ts @@ -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 { + // 学生特有字段(如有) +} \ No newline at end of file