refactor(usermanagement): 优化用户管理模块的类型定义和数据处理

- 为 adminActions、guestActions、problemActions 和 teacherActions 文件中的函数添加了明确的类型注解
- 更新了函数参数和返回值的类型,提高了代码的可读性和可维护性
- 在相关页面组件中添加了类型注解,明确了数据结构
- 移除了未使用的 Separator 组件导入
This commit is contained in:
liguang 2025-06-20 20:26:28 +08:00
parent 759aaae94f
commit b3525aee7d
11 changed files with 291 additions and 253 deletions

View File

@ -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')
} }

View File

@ -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')
} }

View File

@ -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')
} }

View File

@ -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')
} }

View File

@ -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} />
} }

View File

@ -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} />
} }

View File

@ -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} />
} }

View File

@ -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} />
} }

View File

@ -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">

View File

@ -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>
) )
} }

View File

@ -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: {