refactor(user-management): 优化用户管理功能和界面

- 为 toast 消息添加 1500ms 持续时间
- 修复管理员、学生和教师表单的字段和验证规则
- 优化问题表格的列配置
- 调整用户表格的样式和布局
This commit is contained in:
liguang 2025-06-19 17:52:52 +08:00
parent 42e576876e
commit db8051d1d8
5 changed files with 83 additions and 86 deletions

View File

@ -293,11 +293,11 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
if (isProblem) { if (isProblem) {
problemApi.getProblems() problemApi.getProblems()
.then(setData) .then(setData)
.catch(() => toast.error('获取数据失败')) .catch(() => toast.error('获取数据失败', { duration: 1500 }))
} else { } else {
userApi.getUsers(config.userType) userApi.getUsers(config.userType)
.then(setData) .then(setData)
.catch(() => toast.error('获取数据失败')) .catch(() => toast.error('获取数据失败', { duration: 1500 }))
} }
}, [config.userType]) }, [config.userType])
@ -335,9 +335,9 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
await userApi.createUser(config.userType, submitData) await userApi.createUser(config.userType, submitData)
userApi.getUsers(config.userType).then(setData) userApi.getUsers(config.userType).then(setData)
onOpenChange(false) onOpenChange(false)
toast.success('添加成功') toast.success('添加成功', { duration: 1500 })
} catch { } catch {
toast.error("添加失败") toast.error('添加失败', { duration: 1500 })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -410,9 +410,9 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
await problemApi.createProblem(submitData) await problemApi.createProblem(submitData)
problemApi.getProblems().then(setData) problemApi.getProblems().then(setData)
onOpenChange(false) onOpenChange(false)
toast.success('添加成功') toast.success('添加成功', { duration: 1500 })
} catch { } catch {
toast.error("添加失败") toast.error('添加失败', { duration: 1500 })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -485,9 +485,9 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
await userApi.updateUser(config.userType, submitData) await userApi.updateUser(config.userType, submitData)
userApi.getUsers(config.userType).then(setData) userApi.getUsers(config.userType).then(setData)
onOpenChange(false) onOpenChange(false)
toast.success('修改成功') toast.success('修改成功', { duration: 1500 })
} catch { } catch {
toast.error("修改失败") toast.error('修改失败', { duration: 1500 })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -558,9 +558,9 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
await problemApi.updateProblem(submitData) await problemApi.updateProblem(submitData)
problemApi.getProblems().then(setData) problemApi.getProblems().then(setData)
onOpenChange(false) onOpenChange(false)
toast.success('修改成功') toast.success('修改成功', { duration: 1500 })
} catch { } catch {
toast.error("修改失败") toast.error('修改失败', { duration: 1500 })
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -577,26 +577,21 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
</DialogHeader> </DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
{config.formFields.map((field) => ( <div className="grid grid-cols-4 items-center gap-4">
<div key={field.key} className="grid grid-cols-4 items-center gap-4"> <Label htmlFor="displayId" className="text-right"></Label>
<Label htmlFor={field.key} className="text-right"> <Input
{field.label} id="displayId"
</Label> type="number"
<Input {...form.register('displayId', { valueAsNumber: true })}
id={field.key} className="col-span-3"
type={field.type} placeholder="请输入题目编号"
{...form.register(field.key as 'displayId' | 'difficulty', field.key === 'displayId' ? { valueAsNumber: true } : {})} />
className="col-span-3" {form.formState.errors.displayId?.message && (
placeholder={field.placeholder} <p className="col-span-3 col-start-2 text-sm text-red-500">
disabled={field.key === 'id'} {form.formState.errors.displayId?.message as string}
/> </p>
{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"> </div>
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string}
</p>
)}
</div>
))}
</div> </div>
<DialogFooter> <DialogFooter>
<Button type="submit" disabled={isLoading}> <Button type="submit" disabled={isLoading}>
@ -644,6 +639,8 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
password: "密码", password: "密码",
createdAt: "创建时间", createdAt: "创建时间",
actions: "操作", actions: "操作",
displayId: "题目编号",
difficulty: "难度",
} }
return ( return (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
@ -660,15 +657,17 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
})} })}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Button {config.actions.add && (
variant="outline" <Button
size="sm" variant="outline"
className="h-7 gap-1 px-2 text-sm" size="sm"
onClick={() => setIsAddDialogOpen(true)} className="h-7 gap-1 px-2 text-sm"
> onClick={() => setIsAddDialogOpen(true)}
<PlusIcon className="h-4 w-4" /> >
{config.actions.add.label} <PlusIcon className="h-4 w-4" />
</Button> {config.actions.add.label}
</Button>
)}
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
@ -737,16 +736,16 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
</div> </div>
<div className="flex items-center justify-between px-2"> <div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground"> <div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredRowModel().rows.length} {table.getFilteredRowModel().rows.length}
</div> </div>
<div className="flex items-center space-x-6 lg:space-x-8"> <div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<p className="text-sm font-medium"></p> <p className="text-sm font-medium"></p>
<Select <Select
value={`${table.getState().pagination.pageSize}`} value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => { onValueChange={(value) => {
table.setPageSize(Number(value)) table.setPageSize(Number(value))
@ -754,20 +753,20 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
> >
<SelectTrigger className="h-8 w-[70px]"> <SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} /> <SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger> </SelectTrigger>
<SelectContent side="top"> <SelectContent side="top">
{config.pagination.pageSizes.map((pageSize) => ( {config.pagination.pageSizes.map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}> <SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize} {pageSize}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium"> <div className="flex w-[100px] items-center justify-center text-sm font-medium">
{table.getState().pagination.pageIndex + 1} {" "} {table.getState().pagination.pageIndex + 1} {" "}
{table.getPageCount()} {table.getPageCount()}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button
variant="outline" variant="outline"
@ -830,11 +829,11 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
</div> </div>
{/* 添加用户对话框 */} {/* 添加用户对话框 */}
{isProblem ? ( {isProblem && config.actions.add ? (
<AddUserDialogProblem open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} /> <AddUserDialogProblem open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
) : ( ) : !isProblem && config.actions.add ? (
<AddUserDialogUser open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} /> <AddUserDialogUser open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
)} ) : null}
{/* 编辑用户对话框 */} {/* 编辑用户对话框 */}
{isProblem && editingUser ? ( {isProblem && editingUser ? (
@ -863,7 +862,7 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
variant="destructive" variant="destructive"
onClick={async () => { onClick={async () => {
try { try {
if (deleteBatch) { if (deleteBatch) {
const selectedRows = table.getFilteredSelectedRowModel().rows const selectedRows = table.getFilteredSelectedRowModel().rows
for (const row of selectedRows) { for (const row of selectedRows) {
if (isProblem) { if (isProblem) {
@ -871,23 +870,23 @@ export function UserTable({ config, data: initialData }: UserTableProps) {
problemApi.getProblems().then(setData) problemApi.getProblems().then(setData)
} else { } else {
await userApi.deleteUser(config.userType, row.original.id) await userApi.deleteUser(config.userType, row.original.id)
userApi.getUsers(config.userType).then(setData) userApi.getUsers(config.userType).then(setData)
} }
} }
toast.success(`成功删除 ${selectedRows.length} 条记录`) toast.success(`成功删除 ${selectedRows.length} 条记录`, { duration: 1500 })
} else if (deleteTargetId) { } else if (deleteTargetId) {
if (isProblem) { if (isProblem) {
await problemApi.deleteProblem(deleteTargetId) await problemApi.deleteProblem(deleteTargetId)
problemApi.getProblems().then(setData) problemApi.getProblems().then(setData)
} else { } else {
await userApi.deleteUser(config.userType, deleteTargetId) await userApi.deleteUser(config.userType, deleteTargetId)
userApi.getUsers(config.userType).then(setData) userApi.getUsers(config.userType).then(setData)
} }
toast.success('删除成功') toast.success('删除成功', { duration: 1500 })
} }
setDeleteDialogOpen(false) setDeleteDialogOpen(false)
} catch { } catch {
toast.error("删除失败") toast.error('删除失败', { duration: 1500 })
} }
}} }}
> >

View File

@ -15,18 +15,18 @@ export type Admin = z.infer<typeof adminSchema>
// 添加管理员表单校验 schema // 添加管理员表单校验 schema
export const addAdminSchema = z.object({ export const addAdminSchema = z.object({
name: z.string().optional(), name: z.string().min(1, "姓名为必填项"),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(), password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(), createdAt: z.string(),
}) })
// 编辑管理员表单校验 schema // 编辑管理员表单校验 schema
export const editAdminSchema = z.object({ export const editAdminSchema = z.object({
id: z.string(), id: z.string(),
name: z.string().optional(), name: z.string().min(1, "姓名为必填项"),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(), password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(), createdAt: z.string(),
}) })
@ -77,8 +77,8 @@ export const adminConfig = {
key: "name", key: "name",
label: "姓名", label: "姓名",
type: "text", type: "text",
placeholder: "请输入管理员姓名(选填)", placeholder: "请输入管理员姓名",
required: false, required: true,
}, },
{ {
key: "email", key: "email",
@ -91,8 +91,8 @@ export const adminConfig = {
key: "password", key: "password",
label: "密码", label: "密码",
type: "password", type: "password",
placeholder: "请输入密码(选填)", placeholder: "请输入8-32位密码",
required: false, required: true,
}, },
{ {
key: "createdAt", key: "createdAt",

View File

@ -26,14 +26,12 @@ export const problemConfig = {
{ key: "id", label: "ID", sortable: true }, { key: "id", label: "ID", sortable: true },
{ key: "displayId", label: "题目编号", sortable: true, searchable: true, placeholder: "搜索编号" }, { key: "displayId", label: "题目编号", sortable: true, searchable: true, placeholder: "搜索编号" },
{ key: "difficulty", label: "难度", sortable: true, searchable: true, placeholder: "搜索难度" }, { key: "difficulty", label: "难度", sortable: true, searchable: true, placeholder: "搜索难度" },
{ key: "createdAt", label: "创建时间", sortable: true },
], ],
formFields: [ formFields: [
{ key: "displayId", label: "题目编号", type: "number", required: true }, { key: "displayId", label: "题目编号", type: "number", required: true },
{ key: "difficulty", label: "难度", type: "text", required: true }, { key: "difficulty", label: "难度", type: "text", required: true },
], ],
actions: { actions: {
add: { label: "添加题目", icon: "PlusIcon" },
edit: { label: "编辑", icon: "PencilIcon" }, edit: { label: "编辑", icon: "PencilIcon" },
delete: { label: "删除", icon: "TrashIcon" }, delete: { label: "删除", icon: "TrashIcon" },
batchDelete: { label: "批量删除", icon: "TrashIcon" }, batchDelete: { label: "批量删除", icon: "TrashIcon" },

View File

@ -11,17 +11,17 @@ export const studentSchema = z.object({
}); });
export const addStudentSchema = z.object({ export const addStudentSchema = z.object({
name: z.string().optional(), name: z.string().min(1, "姓名为必填项"),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(), password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(), createdAt: z.string(),
}); });
export const editStudentSchema = z.object({ export const editStudentSchema = z.object({
id: z.string(), id: z.string(),
name: z.string().optional(), name: z.string().min(1, "姓名为必填项"),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(), password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(), createdAt: z.string(),
}); });
@ -37,9 +37,9 @@ export const studentConfig = {
{ key: "createdAt", label: "创建时间", sortable: true }, { key: "createdAt", label: "创建时间", sortable: true },
], ],
formFields: [ formFields: [
{ key: "name", label: "姓名", type: "text", placeholder: "请输入学生姓名(选填)", required: false }, { 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: "请输入密码(选填)", required: false }, { 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: true },
], ],
actions: { actions: {

View File

@ -11,17 +11,17 @@ export const teacherSchema = z.object({
}); });
export const addTeacherSchema = z.object({ export const addTeacherSchema = z.object({
name: z.string().optional(), name: z.string().min(1, "姓名为必填项"),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(), password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(), createdAt: z.string(),
}); });
export const editTeacherSchema = z.object({ export const editTeacherSchema = z.object({
id: z.string(), id: z.string(),
name: z.string().optional(), name: z.string().min(1, "姓名为必填项"),
email: z.string().email("请输入有效的邮箱地址"), email: z.string().email("请输入有效的邮箱地址"),
password: z.string().optional(), password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
createdAt: z.string(), createdAt: z.string(),
}); });
@ -37,9 +37,9 @@ export const teacherConfig = {
{ key: "createdAt", label: "创建时间", sortable: true }, { key: "createdAt", label: "创建时间", sortable: true },
], ],
formFields: [ formFields: [
{ key: "name", label: "姓名", type: "text", placeholder: "请输入教师姓名(选填)", required: false }, { 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: "请输入密码(选填)", required: false }, { 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: true },
], ],
actions: { actions: {