'use client' import React, { createContext, useContext, useEffect, useRef, useState } from 'react' import { UseFormReturn, ControllerRenderProps } from 'react-hook-form' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, } from '@/components/ui/drawer' import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { useIsMobile } from '@/hooks/use-mobile' import { cn } from '@/lib/utils' import { Loader2 } from 'lucide-react' import { Skeleton } from '@/components/ui/skeleton' // FormDialog Context export interface FormDialogContextValue { form: UseFormReturn close: () => void fields: FormFieldConfig[] isLoading?: boolean } const FormDialogContext = createContext(null) export function useFormDialogContext() { const context = useContext(FormDialogContext) if (!context) { throw new Error('useFormDialogContext must be used within FormDialog') } return context } // 字段配置类型定义 export interface FormFieldConfig { name: string label: string required?: boolean render: (props: { field: ControllerRenderProps }) => React.ReactNode // 将...field传递给UI控件交给react-hook-form管理 className?: string // 允许为单个字段指定自定义样式 } // 取消按钮组件 export interface FormCancelActionProps extends Omit, 'onClick' | 'type'> { children?: React.ReactNode onCancel?: () => void } export function FormCancelAction({ children = '取消', variant = 'outline', onCancel, ...props }: FormCancelActionProps) { const { close } = useFormDialogContext() const handleClick = () => { if (onCancel) { onCancel() } else { close() } } return ( ) } // 重置按钮组件 export interface FormResetActionProps extends Omit, 'onClick' | 'type'> { children?: React.ReactNode onReset?: () => void confirmTitle?: string confirmDescription?: string } export function FormResetAction({ children = '重置', variant = 'outline', onReset, confirmTitle = '确认重置', confirmDescription = '确定要重置表单吗?表单将回到打开时的状态。', ...props }: FormResetActionProps) { const { form, isLoading } = useFormDialogContext() const [showConfirm, setShowConfirm] = useState(false) const handleConfirm = () => { if (onReset) { onReset() } else { form.reset() } setShowConfirm(false) } return ( <> {confirmTitle} {confirmDescription} 取消 确认 ) } // 提交按钮组件 export interface FormSubmitActionProps extends Omit, 'onClick' | 'type'> { onSubmit: (data: any) => Promise | void children?: React.ReactNode disabled?: boolean isSubmitting?: boolean showSpinningLoader?: boolean } export function FormSubmitAction({ onSubmit, children = '提交', disabled = false, isSubmitting = false, showSpinningLoader = true, variant = 'default', ...props }: FormSubmitActionProps) { const { form, isLoading } = useFormDialogContext() return ( ) } // 操作按钮栏组件 export interface FormActionBarProps { children?: React.ReactNode } export function FormActionBar({ children }: FormActionBarProps) { return (
{children}
) } // FormGridContent 组件 export interface FormGridContentProps { className?: string } export function FormGridContent({ className = 'grid grid-cols-1 gap-4' }: FormGridContentProps) { const { form, fields, isLoading } = useFormDialogContext() // 如果正在加载,显示骨架屏 if (isLoading) { return (
{fields.map((fieldConfig) => (
))}
) } return (
{fields.map((fieldConfig) => ( ( {fieldConfig.label} {fieldConfig.required && *} {fieldConfig.render({ field })} )} /> ))}
) } export interface FormDialogProps { isOpen: boolean title: string description: string form: UseFormReturn fields: FormFieldConfig[] onClose: () => void className?: string // 允许自定义对话框内容样式,可控制宽度 formClassName?: string // 允许自定义表格样式 children: React.ReactNode // 操作按钮区域内容 isLoading?: boolean // 是否正在加载数据 } export function FormDialog({ isOpen, title, description, form, fields, onClose, className = 'max-w-md', formClassName, children, isLoading = false, }: FormDialogProps) { const isMobile = useIsMobile() const formRef = useRef(null) // 当对话框打开时,自动聚焦到第一个表单输入控件 useEffect(() => { if (isOpen && !isLoading) { // 使当前拥有焦点的元素(通常是用来触发打开这个drawer的控件)失去焦点,不然控制台会警告焦点在一个要被隐藏于屏幕阅读器的控件上 (document.activeElement as HTMLElement)?.blur(); // 使用 setTimeout 确保 DOM 已完全渲染 const timer = setTimeout(() => { if (formRef.current) { // 查找第一个可聚焦的输入元素 const firstInput = formRef.current.querySelector( 'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])' ) if (firstInput) { firstInput.focus() } } }, 100) return () => clearTimeout(timer) } }, [isOpen, isLoading]) const close = () => { onClose() } // Context 值 const contextValue: FormDialogContextValue = { form, close, fields, isLoading } // 表单内容组件,在 Dialog 和 Drawer 中复用 const formContent = (
{children}
) // 根据设备类型渲染不同的组件 if (isMobile) { return ( !open && close()}> {title} {description}
{formContent}
) } return ( !open && close()}> {title} {description}
{formContent}
) }