mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 17:30:52 +00:00
refactor(usermanagement): 优化用户管理模块的类型定义和数据处理
- 为 adminActions、guestActions、problemActions 和 teacherActions 文件中的函数添加了明确的类型注解 - 更新了函数参数和返回值的类型,提高了代码的可读性和可维护性 - 在相关页面组件中添加了类型注解,明确了数据结构 - 移除了未使用的 Separator 组件导入
This commit is contained in:
parent
759aaae94f
commit
b3525aee7d
@ -2,8 +2,9 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
|
import type { User } from '@/generated/client'
|
||||||
|
|
||||||
export async function createAdmin(data) {
|
export async function createAdmin(data: Omit<User, 'id'|'createdAt'|'updatedAt'> & { password?: string }) {
|
||||||
let password = data.password
|
let password = data.password
|
||||||
if (password) {
|
if (password) {
|
||||||
password = await bcrypt.hash(password, 10)
|
password = await bcrypt.hash(password, 10)
|
||||||
@ -12,12 +13,12 @@ export async function createAdmin(data) {
|
|||||||
revalidatePath('/usermanagement/admin')
|
revalidatePath('/usermanagement/admin')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateAdmin(id, data) {
|
export async function updateAdmin(id: string, data: Partial<Omit<User, 'id'|'createdAt'|'updatedAt'>>) {
|
||||||
await prisma.user.update({ where: { id }, data })
|
await prisma.user.update({ where: { id }, data })
|
||||||
revalidatePath('/usermanagement/admin')
|
revalidatePath('/usermanagement/admin')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteAdmin(id) {
|
export async function deleteAdmin(id: string) {
|
||||||
await prisma.user.delete({ where: { id } })
|
await prisma.user.delete({ where: { id } })
|
||||||
revalidatePath('/usermanagement/admin')
|
revalidatePath('/usermanagement/admin')
|
||||||
}
|
}
|
@ -2,8 +2,9 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
|
import type { User } from '@/generated/client'
|
||||||
|
|
||||||
export async function createGuest(data) {
|
export async function createGuest(data: Omit<User, 'id'|'createdAt'|'updatedAt'> & { password?: string }) {
|
||||||
let password = data.password
|
let password = data.password
|
||||||
if (password) {
|
if (password) {
|
||||||
password = await bcrypt.hash(password, 10)
|
password = await bcrypt.hash(password, 10)
|
||||||
@ -12,12 +13,12 @@ export async function createGuest(data) {
|
|||||||
revalidatePath('/usermanagement/guest')
|
revalidatePath('/usermanagement/guest')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateGuest(id, data) {
|
export async function updateGuest(id: string, data: Partial<Omit<User, 'id'|'createdAt'|'updatedAt'>>) {
|
||||||
await prisma.user.update({ where: { id }, data })
|
await prisma.user.update({ where: { id }, data })
|
||||||
revalidatePath('/usermanagement/guest')
|
revalidatePath('/usermanagement/guest')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteGuest(id) {
|
export async function deleteGuest(id: string) {
|
||||||
await prisma.user.delete({ where: { id } })
|
await prisma.user.delete({ where: { id } })
|
||||||
revalidatePath('/usermanagement/guest')
|
revalidatePath('/usermanagement/guest')
|
||||||
}
|
}
|
@ -1,18 +1,19 @@
|
|||||||
'use server'
|
'use server'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
|
import type { Problem } from '@/generated/client'
|
||||||
|
|
||||||
export async function createProblem(data) {
|
export async function createProblem(data: Omit<Problem, 'id'|'createdAt'|'updatedAt'>) {
|
||||||
await prisma.problem.create({ data })
|
await prisma.problem.create({ data })
|
||||||
revalidatePath('/usermanagement/problem')
|
revalidatePath('/usermanagement/problem')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProblem(id, data) {
|
export async function updateProblem(id: string, data: Partial<Omit<Problem, 'id'|'createdAt'|'updatedAt'>>) {
|
||||||
await prisma.problem.update({ where: { id }, data })
|
await prisma.problem.update({ where: { id }, data })
|
||||||
revalidatePath('/usermanagement/problem')
|
revalidatePath('/usermanagement/problem')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProblem(id) {
|
export async function deleteProblem(id: string) {
|
||||||
await prisma.problem.delete({ where: { id } })
|
await prisma.problem.delete({ where: { id } })
|
||||||
revalidatePath('/usermanagement/problem')
|
revalidatePath('/usermanagement/problem')
|
||||||
}
|
}
|
@ -2,8 +2,9 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
|
import type { User } from '@/generated/client'
|
||||||
|
|
||||||
export async function createTeacher(data) {
|
export async function createTeacher(data: Omit<User, 'id'|'createdAt'|'updatedAt'> & { password?: string }) {
|
||||||
let password = data.password
|
let password = data.password
|
||||||
if (password) {
|
if (password) {
|
||||||
password = await bcrypt.hash(password, 10)
|
password = await bcrypt.hash(password, 10)
|
||||||
@ -12,12 +13,12 @@ export async function createTeacher(data) {
|
|||||||
revalidatePath('/usermanagement/teacher')
|
revalidatePath('/usermanagement/teacher')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateTeacher(id, data) {
|
export async function updateTeacher(id: string, data: Partial<Omit<User, 'id'|'createdAt'|'updatedAt'>>) {
|
||||||
await prisma.user.update({ where: { id }, data })
|
await prisma.user.update({ where: { id }, data })
|
||||||
revalidatePath('/usermanagement/teacher')
|
revalidatePath('/usermanagement/teacher')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTeacher(id) {
|
export async function deleteTeacher(id: string) {
|
||||||
await prisma.user.delete({ where: { id } })
|
await prisma.user.delete({ where: { id } })
|
||||||
revalidatePath('/usermanagement/teacher')
|
revalidatePath('/usermanagement/teacher')
|
||||||
}
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import { UserTable } from '@/features/user-management/components/user-table'
|
import { UserTable } from '@/features/user-management/components/user-table'
|
||||||
import { adminConfig } from '@/features/user-management/config/admin'
|
import { adminConfig } from '@/features/user-management/config/admin'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
|
import type { User } from '@/generated/client'
|
||||||
|
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
const data = await prisma.user.findMany({ where: { role: 'ADMIN' } })
|
const data: User[] = await prisma.user.findMany({ where: { role: 'ADMIN' } })
|
||||||
return <UserTable config={adminConfig} data={data} />
|
return <UserTable config={adminConfig} data={data} />
|
||||||
}
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { UserTable } from '@/features/user-management/components/user-table'
|
import { UserTable } from '@/features/user-management/components/user-table'
|
||||||
import { guestConfig } from '@/features/user-management/config/guest'
|
import { guestConfig } from '@/features/user-management/config/guest'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
|
import type { User } from '@/generated/client'
|
||||||
|
|
||||||
export default async function GuestPage() {
|
export default async function GuestPage() {
|
||||||
const data = await prisma.user.findMany({ where: { role: 'GUEST' as any } })
|
const data: User[] = await prisma.user.findMany({ where: { role: 'GUEST' } })
|
||||||
return <UserTable config={guestConfig} data={data} />
|
return <UserTable config={guestConfig} data={data} />
|
||||||
}
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import { UserTable } from '@/features/user-management/components/user-table'
|
import { UserTable } from '@/features/user-management/components/user-table'
|
||||||
import { problemConfig } from '@/features/user-management/config/problem'
|
import { problemConfig } from '@/features/user-management/config/problem'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
|
import type { Problem } from '@/generated/client'
|
||||||
|
|
||||||
export default async function ProblemPage() {
|
export default async function ProblemPage() {
|
||||||
const data = await prisma.problem.findMany({})
|
const data: Problem[] = await prisma.problem.findMany({})
|
||||||
return <UserTable config={problemConfig} data={data} />
|
return <UserTable config={problemConfig} data={data} />
|
||||||
}
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import { UserTable } from '@/features/user-management/components/user-table'
|
import { UserTable } from '@/features/user-management/components/user-table'
|
||||||
import { teacherConfig } from '@/features/user-management/config/teacher'
|
import { teacherConfig } from '@/features/user-management/config/teacher'
|
||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
|
import type { User } from '@/generated/client'
|
||||||
|
|
||||||
export default async function TeacherPage() {
|
export default async function TeacherPage() {
|
||||||
const data = await prisma.user.findMany({ where: { role: 'TEACHER' as any } })
|
const data: User[] = await prisma.user.findMany({ where: { role: 'TEACHER' } })
|
||||||
return <UserTable config={teacherConfig} data={data} />
|
return <UserTable config={teacherConfig} data={data} />
|
||||||
}
|
}
|
@ -1,5 +1,3 @@
|
|||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
return (
|
return (
|
||||||
<header className="group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 flex h-12 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear">
|
<header className="group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 flex h-12 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear">
|
||||||
|
@ -70,11 +70,12 @@ import {
|
|||||||
} from "@/components/ui/tabs"
|
} from "@/components/ui/tabs"
|
||||||
|
|
||||||
import { createAdmin, updateAdmin, deleteAdmin } from '@/app/(app)/usermanagement/_actions/adminActions'
|
import { createAdmin, updateAdmin, deleteAdmin } from '@/app/(app)/usermanagement/_actions/adminActions'
|
||||||
import { createTeacher, updateTeacher, deleteTeacher } from '@/app/(app)/usermanagement/_actions/teacherActions'
|
import { createTeacher, updateTeacher } from '@/app/(app)/usermanagement/_actions/teacherActions'
|
||||||
import { createGuest, updateGuest, deleteGuest } from '@/app/(app)/usermanagement/_actions/guestActions'
|
import { createGuest, updateGuest } from '@/app/(app)/usermanagement/_actions/guestActions'
|
||||||
import { createProblem, updateProblem, deleteProblem } from '@/app/(app)/usermanagement/_actions/problemActions'
|
import { createProblem, updateProblem, deleteProblem } from '@/app/(app)/usermanagement/_actions/problemActions'
|
||||||
|
import type { User, Problem } from '@/generated/client'
|
||||||
|
import { Difficulty, Role } from '@/generated/client'
|
||||||
|
|
||||||
// 通用用户类型
|
|
||||||
export interface UserConfig {
|
export interface UserConfig {
|
||||||
userType: string
|
userType: string
|
||||||
title: string
|
title: string
|
||||||
@ -106,69 +107,85 @@ export interface UserConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserTableProps {
|
type UserTableProps =
|
||||||
config: UserConfig
|
| { config: UserConfig; data: User[] }
|
||||||
data: any[]
|
| { config: UserConfig; data: Problem[] }
|
||||||
|
|
||||||
|
type UserForm = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt: string
|
||||||
|
role: Role
|
||||||
|
image: string | null
|
||||||
|
emailVerified: Date | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在组件内部定义 schema
|
// 新增用户表单类型
|
||||||
|
type AddUserForm = Omit<UserForm, 'id'>
|
||||||
|
|
||||||
const addUserSchema = z.object({
|
const addUserSchema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string(),
|
||||||
email: z.string().email("请输入有效的邮箱地址"),
|
email: z.string().email(),
|
||||||
password: z.string().optional(),
|
password: z.string(),
|
||||||
createdAt: z.string().optional(),
|
createdAt: z.string(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
emailVerified: z.date().nullable(),
|
||||||
|
role: z.nativeEnum(Role),
|
||||||
})
|
})
|
||||||
|
|
||||||
const editUserSchema = z.object({
|
const editUserSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string().default(''),
|
||||||
name: z.string().optional(),
|
name: z.string(),
|
||||||
email: z.string().email("请输入有效的邮箱地址"),
|
email: z.string().email(),
|
||||||
password: z.string().optional(),
|
password: z.string(),
|
||||||
role: z.string().optional(),
|
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
|
image: z.string().nullable(),
|
||||||
|
emailVerified: z.date().nullable(),
|
||||||
|
role: z.nativeEnum(Role),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 题目表单 schema 兼容 null/undefined
|
||||||
const addProblemSchema = z.object({
|
const addProblemSchema = z.object({
|
||||||
displayId: z.number(),
|
displayId: z.number().optional().default(0),
|
||||||
difficulty: z.string(),
|
difficulty: z.nativeEnum(Difficulty).default(Difficulty.EASY),
|
||||||
})
|
})
|
||||||
|
|
||||||
const editProblemSchema = z.object({
|
const editProblemSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string().default(''),
|
||||||
displayId: z.number(),
|
displayId: z.number().optional().default(0),
|
||||||
difficulty: z.string(),
|
difficulty: z.nativeEnum(Difficulty).default(Difficulty.EASY),
|
||||||
})
|
})
|
||||||
|
|
||||||
export function UserTable({ config, data }: UserTableProps) {
|
export function UserTable(props: UserTableProps) {
|
||||||
|
const isProblem = props.config.userType === 'problem'
|
||||||
|
const router = useRouter()
|
||||||
|
const problemData = isProblem ? (props.data as Problem[]) : undefined
|
||||||
|
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||||
const [editingUser, setEditingUser] = useState<any>(null)
|
const [editingUser, setEditingUser] = useState<User | Problem | null>(null)
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
|
const [deleteBatch, setDeleteBatch] = useState(false)
|
||||||
const [rowSelection, setRowSelection] = useState({})
|
const [rowSelection, setRowSelection] = useState({})
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||||
const [sorting, setSorting] = useState<SortingState>([])
|
const [sorting, setSorting] = useState<SortingState>([])
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: config.pagination.defaultPageSize,
|
pageSize: props.config.pagination.defaultPageSize,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 删除确认对话框相关state
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
|
||||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
|
|
||||||
const [deleteBatch, setDeleteBatch] = useState(false)
|
|
||||||
|
|
||||||
// 页码输入本地state
|
|
||||||
const [pageInput, setPageInput] = useState(pagination.pageIndex + 1)
|
const [pageInput, setPageInput] = useState(pagination.pageIndex + 1)
|
||||||
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
|
||||||
|
const [pendingDeleteItem, setPendingDeleteItem] = useState<User | Problem | null>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPageInput(pagination.pageIndex + 1)
|
setPageInput(pagination.pageIndex + 1)
|
||||||
}, [pagination.pageIndex])
|
}, [pagination.pageIndex])
|
||||||
|
|
||||||
// 判断是否为题目管理
|
// 表格列
|
||||||
const isProblem = config.userType === "problem"
|
const tableColumns = React.useMemo<ColumnDef<User | Problem>[]>(() => {
|
||||||
|
const columns: ColumnDef<User | Problem>[] = [
|
||||||
// 动态生成表格列
|
|
||||||
const tableColumns = React.useMemo<ColumnDef<any>[]>(() => {
|
|
||||||
const columns: ColumnDef<any>[] = [
|
|
||||||
{
|
{
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@ -189,34 +206,17 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
props.config.columns.forEach((col) => {
|
||||||
// 添加配置的列
|
const column: ColumnDef<User | Problem> = {
|
||||||
config.columns.forEach((col) => {
|
|
||||||
const column: ColumnDef<any> = {
|
|
||||||
accessorKey: col.key,
|
accessorKey: col.key,
|
||||||
header: ({ column: tableColumn }) => {
|
header: col.label,
|
||||||
if (col.searchable) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span>{col.label}</span>
|
|
||||||
<Input
|
|
||||||
placeholder={col.placeholder || "搜索"}
|
|
||||||
className="h-6 w-24 text-xs px-1"
|
|
||||||
value={(() => {
|
|
||||||
const v = tableColumn.getFilterValue()
|
|
||||||
return typeof v === 'string' ? v : ''
|
|
||||||
})()}
|
|
||||||
onChange={e => tableColumn.setFilterValue(e.target.value)}
|
|
||||||
style={{ minWidth: 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return col.label
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
// 类型安全分流
|
||||||
|
if (col.key === 'displayId' && isProblem) {
|
||||||
|
return (row.original as Problem).displayId
|
||||||
|
}
|
||||||
|
if ((col.key === 'createdAt' || col.key === 'updatedAt')) {
|
||||||
const value = row.getValue(col.key)
|
const value = row.getValue(col.key)
|
||||||
if (col.key === 'createdAt' || col.key === 'updatedAt') {
|
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return value.toLocaleString()
|
return value.toLocaleString()
|
||||||
}
|
}
|
||||||
@ -224,7 +224,7 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
return new Date(value).toLocaleString()
|
return new Date(value).toLocaleString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return row.getValue(col.key)
|
||||||
},
|
},
|
||||||
enableSorting: col.sortable !== false,
|
enableSorting: col.sortable !== false,
|
||||||
filterFn: col.searchable ? (row, columnId, value) => {
|
filterFn: col.searchable ? (row, columnId, value) => {
|
||||||
@ -235,13 +235,11 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
}
|
}
|
||||||
columns.push(column)
|
columns.push(column)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 添加操作列
|
|
||||||
columns.push({
|
columns.push({
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: () => <div className="text-right">操作</div>,
|
header: () => <div className="text-right">操作</div>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const user = row.original
|
const item = row.original
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
@ -249,35 +247,33 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 gap-1"
|
className="h-8 gap-1"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingUser(user)
|
setEditingUser(item)
|
||||||
setIsEditDialogOpen(true)
|
setIsEditDialogOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PencilIcon className="size-4 mr-1" /> {config.actions.edit.label}
|
<PencilIcon className="size-4 mr-1" /> {props.config.actions.edit.label}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 gap-1 text-destructive hover:text-destructive"
|
className="h-8 gap-1 text-destructive hover:text-destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteTargetId(user.id)
|
setPendingDeleteItem(item)
|
||||||
setDeleteBatch(false)
|
setDeleteConfirmOpen(true)
|
||||||
setDeleteDialogOpen(true)
|
|
||||||
}}
|
}}
|
||||||
aria-label="Delete"
|
aria-label="Delete"
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-4 mr-1" /> {config.actions.delete.label}
|
<TrashIcon className="size-4 mr-1" /> {props.config.actions.delete.label}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
}, [config])
|
}, [props.config, router, isProblem])
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data: props.data,
|
||||||
columns: tableColumns,
|
columns: tableColumns,
|
||||||
state: {
|
state: {
|
||||||
sorting,
|
sorting,
|
||||||
@ -300,50 +296,33 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 生成唯一ID
|
|
||||||
function generateUniqueId(existingIds: string[]): string {
|
|
||||||
let id: string
|
|
||||||
do {
|
|
||||||
id = Math.random().toString(36).substr(2, 9)
|
|
||||||
} while (existingIds.includes(id))
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加用户对话框组件(仅用户)
|
// 添加用户对话框组件(仅用户)
|
||||||
function AddUserDialogUser({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
function AddUserDialogUser({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const form = useForm({
|
const form = useForm<AddUserForm>({
|
||||||
resolver: zodResolver(addUserSchema),
|
resolver: zodResolver(addUserSchema),
|
||||||
defaultValues: { name: "", email: "", password: "", createdAt: "" },
|
defaultValues: { name: '', email: '', password: '', createdAt: '', image: null, emailVerified: null, role: Role.GUEST },
|
||||||
})
|
})
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
form.reset({ name: "", email: "", password: "", createdAt: "" })
|
form.reset({ name: '', email: '', password: '', createdAt: '', image: null, emailVerified: null, role: Role.GUEST })
|
||||||
}
|
}
|
||||||
}, [open, form])
|
}, [open, form])
|
||||||
async function onSubmit(formData: any) {
|
async function onSubmit(data: AddUserForm) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...formData,
|
...data,
|
||||||
// 移除手动生成的 id,让数据库自动生成
|
image: data.image ?? null,
|
||||||
// 移除 createdAt,让数据库自动设置
|
emailVerified: data.emailVerified ?? null,
|
||||||
|
role: data.role ?? Role.GUEST,
|
||||||
}
|
}
|
||||||
// 清理空字段
|
if (!submitData.name) submitData.name = ''
|
||||||
if (!submitData.name) delete submitData.name
|
if (!submitData.createdAt) submitData.createdAt = new Date().toISOString()
|
||||||
if (!submitData.password) delete submitData.password
|
else submitData.createdAt = new Date(submitData.createdAt).toISOString()
|
||||||
if (!submitData.createdAt) delete submitData.createdAt
|
if (props.config.userType === 'admin') await createAdmin(submitData)
|
||||||
|
else if (props.config.userType === 'teacher') await createTeacher(submitData)
|
||||||
// 如果用户提供了创建时间,转换为完整的 ISO-8601 格式
|
else if (props.config.userType === 'guest') await createGuest(submitData)
|
||||||
if (submitData.createdAt) {
|
|
||||||
const date = new Date(submitData.createdAt)
|
|
||||||
submitData.createdAt = date.toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.userType === 'admin') await createAdmin(submitData)
|
|
||||||
else if (config.userType === 'teacher') await createTeacher(submitData)
|
|
||||||
else if (config.userType === 'guest') await createGuest(submitData)
|
|
||||||
else if (config.userType === 'problem') await createProblem(submitData)
|
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
toast.success('添加成功', { duration: 1500 })
|
toast.success('添加成功', { duration: 1500 })
|
||||||
router.refresh()
|
router.refresh()
|
||||||
@ -354,33 +333,32 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{config.actions.add.label}</DialogTitle>
|
<DialogTitle>{props.config.actions.add.label}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
请填写信息,ID自动生成。
|
请填写信息,ID自动生成。
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</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) => (
|
{props.config.formFields.filter(field => field.key !== 'id').map((field) => (
|
||||||
<div key={field.key} className="grid grid-cols-4 items-center gap-4">
|
<div key={field.key} className="grid grid-cols-4 items-center gap-4">
|
||||||
<Label htmlFor={field.key} className="text-right">
|
<Label htmlFor={field.key} className="text-right">
|
||||||
{field.label}
|
{field.label}
|
||||||
</Label>
|
</Label>
|
||||||
{field.type === 'select' && field.options ? (
|
{field.type === 'select' && field.options ? (
|
||||||
<Select
|
<Select
|
||||||
value={String(form.watch(field.key as string) ?? '')}
|
value={form.watch(field.key as 'name' | 'email' | 'password' | 'createdAt' | 'role') ?? ''}
|
||||||
onValueChange={value => form.setValue(field.key as string, value)}
|
onValueChange={value => form.setValue(field.key as 'name' | 'email' | 'password' | 'createdAt' | 'role', value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="col-span-3">
|
<SelectTrigger className="col-span-3">
|
||||||
<SelectValue placeholder={`请选择${field.label}`} />
|
<SelectValue placeholder={`请选择${field.label}`} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{field.options.map((opt: any) => (
|
{field.options.map((opt) => (
|
||||||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -389,7 +367,7 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
<Input
|
<Input
|
||||||
id={field.key}
|
id={field.key}
|
||||||
type={field.type}
|
type={field.type}
|
||||||
{...form.register(field.key as any)}
|
{...form.register(field.key as 'name' | 'email' | 'password' | 'createdAt' | 'role')}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
/>
|
/>
|
||||||
@ -416,27 +394,28 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
// 添加题目对话框组件(仅题目)
|
// 添加题目对话框组件(仅题目)
|
||||||
function AddUserDialogProblem({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
function AddUserDialogProblem({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const form = useForm({
|
const form = useForm<Partial<Problem>>({
|
||||||
resolver: zodResolver(addProblemSchema),
|
resolver: zodResolver(addProblemSchema),
|
||||||
defaultValues: { displayId: 0, difficulty: "" },
|
defaultValues: { displayId: 0, difficulty: Difficulty.EASY },
|
||||||
})
|
})
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
form.reset({ displayId: 0, difficulty: "" })
|
form.reset({ displayId: 0, difficulty: Difficulty.EASY })
|
||||||
}
|
}
|
||||||
}, [open, form])
|
}, [open, form])
|
||||||
async function onSubmit(formData: any) {
|
async function onSubmit(formData: Partial<Problem>) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const submitData = {
|
const submitData: Partial<Problem> = { ...formData, displayId: Number(formData.displayId) }
|
||||||
...formData,
|
await createProblem({
|
||||||
displayId: Number(formData.displayId),
|
displayId: Number(submitData.displayId),
|
||||||
// 移除手动生成的 id,让数据库自动生成
|
difficulty: submitData.difficulty ?? Difficulty.EASY,
|
||||||
}
|
isPublished: false,
|
||||||
if (config.userType === 'admin') await createAdmin(submitData)
|
isTrim: false,
|
||||||
else if (config.userType === 'teacher') await createTeacher(submitData)
|
timeLimit: 1000,
|
||||||
else if (config.userType === 'guest') await createGuest(submitData)
|
memoryLimit: 134217728,
|
||||||
else if (config.userType === 'problem') await createProblem(submitData)
|
userId: null,
|
||||||
|
})
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
toast.success('添加成功', { duration: 1500 })
|
toast.success('添加成功', { duration: 1500 })
|
||||||
router.refresh()
|
router.refresh()
|
||||||
@ -447,30 +426,45 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{config.actions.add.label}</DialogTitle>
|
<DialogTitle>{props.config.actions.add.label}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
请填写信息,ID自动生成。
|
请填写信息,ID自动生成。
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</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) => (
|
{props.config.formFields.map((field) => (
|
||||||
<div key={field.key} className="grid grid-cols-4 items-center gap-4">
|
<div key={field.key} className="grid grid-cols-4 items-center gap-4">
|
||||||
<Label htmlFor={field.key} className="text-right">
|
<Label htmlFor={field.key} className="text-right">
|
||||||
{field.label}
|
{field.label}
|
||||||
</Label>
|
</Label>
|
||||||
|
{field.key === 'difficulty' ? (
|
||||||
|
<Select
|
||||||
|
value={form.watch('difficulty') ?? Difficulty.EASY}
|
||||||
|
onValueChange={value => form.setValue('difficulty', value as typeof Difficulty.EASY | typeof Difficulty.MEDIUM | typeof Difficulty.HARD)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="col-span-3">
|
||||||
|
<SelectValue placeholder="请选择难度" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={Difficulty.EASY}>简单</SelectItem>
|
||||||
|
<SelectItem value={Difficulty.MEDIUM}>中等</SelectItem>
|
||||||
|
<SelectItem value={Difficulty.HARD}>困难</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
<Input
|
<Input
|
||||||
id={field.key}
|
id={field.key}
|
||||||
type={field.type}
|
type={field.type}
|
||||||
{...form.register(field.key as 'displayId' | 'difficulty', field.key === 'displayId' ? { valueAsNumber: true } : {})}
|
{...form.register(field.key as 'displayId' | 'difficulty' | 'id', field.key === 'displayId' ? { valueAsNumber: true } : {})}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && (
|
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && (
|
||||||
<p className="col-span-3 col-start-2 text-sm text-red-500">
|
<p className="col-span-3 col-start-2 text-sm text-red-500">
|
||||||
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string}
|
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string}
|
||||||
@ -491,31 +485,49 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 编辑用户对话框组件(仅用户)
|
// 编辑用户对话框组件(仅用户)
|
||||||
function EditUserDialogUser({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: any }) {
|
function EditUserDialogUser({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: User }) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const form = useForm({
|
const editForm = useForm<UserForm>({
|
||||||
resolver: zodResolver(editUserSchema),
|
resolver: zodResolver(editUserSchema),
|
||||||
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) },
|
defaultValues: {
|
||||||
|
id: typeof user.id === 'string' ? user.id : '',
|
||||||
|
name: user.name ?? '',
|
||||||
|
email: user.email ?? '',
|
||||||
|
password: '',
|
||||||
|
role: user.role ?? Role.GUEST,
|
||||||
|
createdAt: user.createdAt ? new Date(user.createdAt).toISOString().slice(0, 16) : '',
|
||||||
|
image: user.image ?? null,
|
||||||
|
emailVerified: user.emailVerified ?? null,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
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) })
|
editForm.reset({
|
||||||
|
id: typeof user.id === 'string' ? user.id : '',
|
||||||
|
name: user.name ?? '',
|
||||||
|
email: user.email ?? '',
|
||||||
|
password: '',
|
||||||
|
role: user.role ?? Role.GUEST,
|
||||||
|
createdAt: user.createdAt ? new Date(user.createdAt).toISOString().slice(0, 16) : '',
|
||||||
|
image: user.image ?? null,
|
||||||
|
emailVerified: user.emailVerified ?? null,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [open, user, form])
|
}, [open, user, editForm])
|
||||||
async function onSubmit(formData: any) {
|
async function onSubmit(data: UserForm) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...formData,
|
...data,
|
||||||
createdAt: formData.createdAt ? new Date(formData.createdAt).toISOString() : new Date().toISOString(),
|
createdAt: data.createdAt ? new Date(data.createdAt).toISOString() : new Date().toISOString(),
|
||||||
|
image: data.image ?? null,
|
||||||
|
emailVerified: data.emailVerified ?? null,
|
||||||
|
role: data.role ?? Role.GUEST,
|
||||||
}
|
}
|
||||||
if (!submitData.password) {
|
const id = typeof submitData.id === 'string' ? submitData.id : ''
|
||||||
delete submitData.password;
|
if (props.config.userType === 'admin') await updateAdmin(id, submitData)
|
||||||
}
|
else if (props.config.userType === 'teacher') await updateTeacher(id, submitData)
|
||||||
if (config.userType === 'admin') await updateAdmin(submitData.id, submitData)
|
else if (props.config.userType === 'guest') await updateGuest(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)
|
onOpenChange(false)
|
||||||
toast.success('修改成功', { duration: 1500 })
|
toast.success('修改成功', { duration: 1500 })
|
||||||
} catch {
|
} catch {
|
||||||
@ -524,19 +536,18 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{config.actions.edit.label}</DialogTitle>
|
<DialogTitle>{props.config.actions.edit.label}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
修改信息
|
修改信息
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={editForm.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
{config.formFields.map((field) => (
|
{props.config.formFields.map((field) => (
|
||||||
<div key={field.key} className="grid grid-cols-4 items-center gap-4">
|
<div key={field.key} className="grid grid-cols-4 items-center gap-4">
|
||||||
<Label htmlFor={field.key} className="text-right">
|
<Label htmlFor={field.key} className="text-right">
|
||||||
{field.label}
|
{field.label}
|
||||||
@ -544,40 +555,39 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
<Input
|
<Input
|
||||||
id={field.key}
|
id={field.key}
|
||||||
type={field.type}
|
type={field.type}
|
||||||
{...form.register(field.key as 'name' | 'email' | 'password' | 'createdAt' | 'id')}
|
{...editForm.register(field.key as 'name' | 'email' | 'password' | 'createdAt' | 'role')}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
disabled={field.key === 'id'}
|
disabled={field.key === 'id'}
|
||||||
/>
|
/>
|
||||||
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && (
|
{editForm.formState.errors[field.key as keyof typeof editForm.formState.errors]?.message && (
|
||||||
<p className="col-span-3 col-start-2 text-sm text-red-500">
|
<p className="col-span-3 col-start-2 text-sm text-red-500">
|
||||||
{form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string}
|
{editForm.formState.errors[field.key as keyof typeof editForm.formState.errors]?.message as string}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* 编辑时显示角色选择 */}
|
{/* 编辑时显示角色选择 */}
|
||||||
{config.userType !== 'problem' && (
|
{props.config.userType !== 'problem' && (
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
<Label htmlFor="role" className="text-right">
|
<Label htmlFor="role" className="text-right">
|
||||||
角色
|
角色
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={form.watch('role' as 'role')}
|
value={editForm.watch('role') ?? ''}
|
||||||
onValueChange={value => form.setValue('role' as 'role', value)}
|
onValueChange={value => editForm.setValue('role', value as Role)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="col-span-3">
|
<SelectTrigger className="col-span-3">
|
||||||
<SelectValue placeholder="请选择角色" />
|
<SelectValue placeholder="请选择角色" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{config.userType === 'guest' && (
|
{props.config.userType === 'guest' && (
|
||||||
<>
|
<>
|
||||||
<SelectItem value="GUEST">学生</SelectItem>
|
<SelectItem value="GUEST">学生</SelectItem>
|
||||||
<SelectItem value="TEACHER">老师</SelectItem>
|
<SelectItem value="TEACHER">老师</SelectItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(config.userType === 'teacher' || config.userType === 'admin') && (
|
{(props.config.userType === 'teacher' || props.config.userType === 'admin') && (
|
||||||
<>
|
<>
|
||||||
<SelectItem value="ADMIN">管理员</SelectItem>
|
<SelectItem value="ADMIN">管理员</SelectItem>
|
||||||
<SelectItem value="TEACHER">老师</SelectItem>
|
<SelectItem value="TEACHER">老师</SelectItem>
|
||||||
@ -586,9 +596,9 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{form.formState.errors.role?.message && (
|
{editForm.formState.errors.role?.message && (
|
||||||
<p className="col-span-3 col-start-2 text-sm text-red-500">
|
<p className="col-span-3 col-start-2 text-sm text-red-500">
|
||||||
{form.formState.errors.role?.message as string}
|
{editForm.formState.errors.role?.message as string}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -606,28 +616,30 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 编辑题目对话框组件(仅题目)
|
// 编辑题目对话框组件(仅题目)
|
||||||
function EditUserDialogProblem({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: any }) {
|
function EditUserDialogProblem({ open, onOpenChange, user }: { open: boolean; onOpenChange: (open: boolean) => void; user: Problem }) {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const form = useForm({
|
const form = useForm<Partial<Problem>>({
|
||||||
resolver: zodResolver(editProblemSchema),
|
resolver: zodResolver(editProblemSchema),
|
||||||
defaultValues: { id: user.id, displayId: Number(user.displayId), difficulty: user.difficulty || "" },
|
defaultValues: {
|
||||||
|
id: user.id,
|
||||||
|
displayId: user.displayId ?? 0,
|
||||||
|
difficulty: user.difficulty ?? Difficulty.EASY,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
form.reset({ id: user.id, displayId: Number(user.displayId), difficulty: user.difficulty || "" })
|
form.reset({
|
||||||
|
id: user.id,
|
||||||
|
displayId: user.displayId ?? 0,
|
||||||
|
difficulty: user.difficulty ?? Difficulty.EASY,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [open, user, form])
|
}, [open, user, form])
|
||||||
async function onSubmit(formData: any) {
|
async function onSubmit(formData: Partial<Problem>) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const submitData = {
|
const submitData: Partial<Problem> = { ...formData, displayId: Number(formData.displayId) }
|
||||||
...formData,
|
await updateProblem(submitData.id!, submitData)
|
||||||
displayId: Number(formData.displayId),
|
|
||||||
}
|
|
||||||
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)
|
onOpenChange(false)
|
||||||
toast.success('修改成功', { duration: 1500 })
|
toast.success('修改成功', { duration: 1500 })
|
||||||
} catch {
|
} catch {
|
||||||
@ -636,12 +648,11 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{config.actions.edit.label}</DialogTitle>
|
<DialogTitle>{props.config.actions.edit.label}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
修改信息
|
修改信息
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
@ -666,16 +677,16 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
<Label htmlFor="difficulty" className="text-right">难度</Label>
|
<Label htmlFor="difficulty" className="text-right">难度</Label>
|
||||||
<Select
|
<Select
|
||||||
value={form.watch('difficulty')}
|
value={form.watch('difficulty') ?? Difficulty.EASY}
|
||||||
onValueChange={value => form.setValue('difficulty', value)}
|
onValueChange={value => form.setValue('difficulty', value as typeof Difficulty.EASY | typeof Difficulty.MEDIUM | typeof Difficulty.HARD)}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="col-span-3">
|
<SelectTrigger className="col-span-3">
|
||||||
<SelectValue placeholder="请选择难度" />
|
<SelectValue placeholder="请选择难度" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="EASY">简单</SelectItem>
|
<SelectItem value={Difficulty.EASY}>简单</SelectItem>
|
||||||
<SelectItem value="MEDIUM">中等</SelectItem>
|
<SelectItem value={Difficulty.MEDIUM}>中等</SelectItem>
|
||||||
<SelectItem value="HARD">困难</SelectItem>
|
<SelectItem value={Difficulty.HARD}>困难</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{form.formState.errors.difficulty?.message && (
|
{form.formState.errors.difficulty?.message && (
|
||||||
@ -697,16 +708,14 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 用ref保证获取最新data
|
// 用ref保证获取最新data
|
||||||
const dataRef = React.useRef<any[]>(data)
|
const dataRef = React.useRef<User[] | Problem[]>(props.data)
|
||||||
React.useEffect(() => { dataRef.current = data }, [data])
|
React.useEffect(() => { dataRef.current = props.data }, [props.data])
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="outline" className="flex w-full flex-col gap-6">
|
<Tabs defaultValue="outline" className="flex w-full flex-col gap-6">
|
||||||
<div className="flex items-center justify-between px-2 lg:px-4 py-2">
|
<div className="flex items-center justify-between px-2 lg:px-4 py-2">
|
||||||
<div className="flex items-center gap-1 text-sm font-medium">
|
<div className="flex items-center gap-1 text-sm font-medium">
|
||||||
{config.title}
|
{props.config.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -751,25 +760,32 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
})}
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
{isProblem && config.actions.add && (
|
{isProblem && props.config.actions.add && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 gap-1 px-2 text-sm"
|
className="h-7 gap-1 px-2 text-sm"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
// 获取当前最大 displayId
|
const maxDisplayId = Array.isArray(problemData) && problemData.length > 0
|
||||||
const maxDisplayId = Array.isArray(data) && data.length > 0
|
? Math.max(...problemData.map(item => Number(item.displayId) || 0), 1000)
|
||||||
? Math.max(...data.map(item => Number(item.displayId) || 0), 1000)
|
|
||||||
: 1000;
|
: 1000;
|
||||||
await createProblem({ displayId: maxDisplayId + 1, difficulty: "EASY" });
|
await createProblem({
|
||||||
|
displayId: maxDisplayId + 1,
|
||||||
|
difficulty: Difficulty.EASY,
|
||||||
|
isPublished: false,
|
||||||
|
isTrim: false,
|
||||||
|
timeLimit: 1000,
|
||||||
|
memoryLimit: 134217728,
|
||||||
|
userId: null,
|
||||||
|
});
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
{config.actions.add.label}
|
{props.config.actions.add.label}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!isProblem && config.actions.add && (
|
{!isProblem && props.config.actions.add && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -777,7 +793,7 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
onClick={() => setIsAddDialogOpen(true)}
|
onClick={() => setIsAddDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
{config.actions.add.label}
|
{props.config.actions.add.label}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@ -791,11 +807,10 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-4 w-4" />
|
||||||
{config.actions.batchDelete.label}
|
{props.config.actions.batchDelete.label}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<div style={{ maxHeight: 500, overflowY: 'auto' }}>
|
<div style={{ maxHeight: 500, overflowY: 'auto' }}>
|
||||||
<Table className="text-sm">
|
<Table className="text-sm">
|
||||||
@ -849,7 +864,6 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
</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} 条记录
|
||||||
@ -867,7 +881,7 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
<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) => (
|
{props.config.pagination.pageSizes.map((pageSize) => (
|
||||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
{pageSize}
|
{pageSize}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -939,21 +953,18 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 添加用户对话框 */}
|
{/* 添加用户对话框 */}
|
||||||
{isProblem && config.actions.add ? (
|
{isProblem && props.config.actions.add ? (
|
||||||
<AddUserDialogProblem open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
|
<AddUserDialogProblem open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
|
||||||
) : !isProblem && config.actions.add ? (
|
) : !isProblem && props.config.actions.add ? (
|
||||||
<AddUserDialogUser open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
|
<AddUserDialogUser open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* 编辑用户对话框 */}
|
{/* 编辑用户对话框 */}
|
||||||
{isProblem && editingUser ? (
|
{isProblem && editingUser ? (
|
||||||
<EditUserDialogProblem open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen} user={editingUser} />
|
<EditUserDialogProblem open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen} user={editingUser as Problem} />
|
||||||
) : editingUser ? (
|
) : editingUser ? (
|
||||||
<EditUserDialogUser open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen} user={editingUser} />
|
<EditUserDialogUser open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen} user={editingUser as User} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
{/* 删除确认对话框 */}
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@ -978,19 +989,12 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
const selectedRows = table.getFilteredSelectedRowModel().rows
|
const selectedRows = table.getFilteredSelectedRowModel().rows
|
||||||
for (const row of selectedRows) {
|
for (const row of selectedRows) {
|
||||||
if (isProblem) {
|
if (isProblem) {
|
||||||
await deleteProblem(row.original.id)
|
await deleteProblem((row.original as Problem).id)
|
||||||
} else {
|
} else {
|
||||||
await deleteAdmin(row.original.id)
|
await deleteAdmin((row.original as User).id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toast.success(`成功删除 ${selectedRows.length} 条记录`, { duration: 1500 })
|
toast.success(`成功删除 ${selectedRows.length} 条记录`, { duration: 1500 })
|
||||||
} else if (deleteTargetId) {
|
|
||||||
if (isProblem) {
|
|
||||||
await deleteProblem(deleteTargetId)
|
|
||||||
} else {
|
|
||||||
await deleteAdmin(deleteTargetId)
|
|
||||||
}
|
|
||||||
toast.success('删除成功', { duration: 1500 })
|
|
||||||
}
|
}
|
||||||
setDeleteDialogOpen(false)
|
setDeleteDialogOpen(false)
|
||||||
router.refresh()
|
router.refresh()
|
||||||
@ -1004,6 +1008,35 @@ export function UserTable({ config, data }: UserTableProps) {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>确认删除</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div>确定要删除该条数据吗?此操作不可撤销。</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteConfirmOpen(false)}>取消</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
if (pendingDeleteItem) {
|
||||||
|
if (isProblem) {
|
||||||
|
await deleteProblem((pendingDeleteItem as Problem).id)
|
||||||
|
} else {
|
||||||
|
await deleteAdmin((pendingDeleteItem as User).id)
|
||||||
|
}
|
||||||
|
toast.success('删除成功', { duration: 1500 })
|
||||||
|
router.refresh()
|
||||||
|
}
|
||||||
|
setDeleteConfirmOpen(false)
|
||||||
|
setPendingDeleteItem(null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -63,8 +63,6 @@ export default {
|
|||||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||||
border: 'hsl(var(--sidebar-border))',
|
border: 'hsl(var(--sidebar-border))',
|
||||||
ring: 'hsl(var(--sidebar-ring))',
|
ring: 'hsl(var(--sidebar-ring))',
|
||||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
|
||||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
|
Loading…
Reference in New Issue
Block a user