Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑
This commit is contained in:
316
src/components/common/form-dialog.tsx
Normal file
316
src/components/common/form-dialog.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
'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'
|
||||
|
||||
// FormDialog Context
|
||||
export interface FormDialogContextValue {
|
||||
form: UseFormReturn<any>
|
||||
close: () => void
|
||||
fields: FormFieldConfig[]
|
||||
}
|
||||
|
||||
const FormDialogContext = createContext<FormDialogContextValue | null>(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<any, any> }) => React.ReactNode // 将...field传递给UI控件交给react-hook-form管理
|
||||
className?: string // 允许为单个字段指定自定义样式
|
||||
}
|
||||
|
||||
// 取消按钮组件
|
||||
export interface FormCancelActionProps extends Omit<React.ComponentProps<typeof Button>, '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 (
|
||||
<Button type="button" variant={variant} onClick={handleClick} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// 重置按钮组件
|
||||
export interface FormResetActionProps extends Omit<React.ComponentProps<typeof Button>, 'onClick' | 'type'> {
|
||||
children?: React.ReactNode
|
||||
onReset?: () => void
|
||||
confirmTitle?: string
|
||||
confirmDescription?: string
|
||||
}
|
||||
|
||||
export function FormResetAction({
|
||||
children = '重置',
|
||||
variant = 'outline',
|
||||
onReset,
|
||||
confirmTitle = '确认重置',
|
||||
confirmDescription = '确定要重置表单吗?表单将回到打开时的状态。',
|
||||
...props
|
||||
}: FormResetActionProps) {
|
||||
const { form } = useFormDialogContext()
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (onReset) {
|
||||
onReset()
|
||||
} else {
|
||||
form.reset()
|
||||
}
|
||||
setShowConfirm(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="button" variant={variant} onClick={() => setShowConfirm(true)} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
<AlertDialog open={showConfirm} onOpenChange={setShowConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{confirmTitle}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{confirmDescription}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirm}>确认</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// 提交按钮组件
|
||||
export interface FormSubmitActionProps extends Omit<React.ComponentProps<typeof Button>, 'onClick' | 'type'> {
|
||||
onSubmit: (data: any) => Promise<void> | 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 } = useFormDialogContext()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
disabled={isSubmitting || disabled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{isSubmitting && showSpinningLoader && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// 操作按钮栏组件
|
||||
export interface FormActionBarProps {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function FormActionBar({ children }: FormActionBarProps) {
|
||||
return (
|
||||
<div className="flex justify-end space-x-2">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// FormGridContent 组件
|
||||
export interface FormGridContentProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function FormGridContent({ className = 'grid grid-cols-1 gap-4' }: FormGridContentProps) {
|
||||
const { form, fields } = useFormDialogContext()
|
||||
|
||||
return (
|
||||
<div className={cn("p-1", className)}>
|
||||
{fields.map((fieldConfig) => (
|
||||
<FormField
|
||||
key={fieldConfig.name}
|
||||
control={form.control}
|
||||
name={fieldConfig.name}
|
||||
render={({ field }) => (
|
||||
<FormItem className={fieldConfig.className || ''}>
|
||||
<FormLabel className="flex items-center gap-1">
|
||||
{fieldConfig.label}
|
||||
{fieldConfig.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
{fieldConfig.render({ field })}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface FormDialogProps {
|
||||
isOpen: boolean
|
||||
title: string
|
||||
description: string
|
||||
form: UseFormReturn<any>
|
||||
fields: FormFieldConfig[]
|
||||
onClose: () => void
|
||||
className?: string // 允许自定义对话框内容样式,可控制宽度
|
||||
formClassName?: string // 允许自定义表格样式
|
||||
children: React.ReactNode // 操作按钮区域内容
|
||||
}
|
||||
|
||||
export function FormDialog({
|
||||
isOpen,
|
||||
title,
|
||||
description,
|
||||
form,
|
||||
fields,
|
||||
onClose,
|
||||
className = 'max-w-md',
|
||||
formClassName,
|
||||
children,
|
||||
}: FormDialogProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
// 当对话框打开时,自动聚焦到第一个表单输入控件
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// 使当前拥有焦点的元素(通常是用来触发打开这个drawer的控件)失去焦点,不然控制台会警告焦点在一个要被隐藏于屏幕阅读器的控件上
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
// 使用 setTimeout 确保 DOM 已完全渲染
|
||||
const timer = setTimeout(() => {
|
||||
if (formRef.current) {
|
||||
// 查找第一个可聚焦的输入元素
|
||||
const firstInput = formRef.current.querySelector<HTMLElement>(
|
||||
'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])'
|
||||
)
|
||||
if (firstInput) {
|
||||
firstInput.focus()
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const close = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
// Context 值
|
||||
const contextValue: FormDialogContextValue = {
|
||||
form,
|
||||
close,
|
||||
fields
|
||||
}
|
||||
|
||||
// 表单内容组件,在 Dialog 和 Drawer 中复用
|
||||
const formContent = (
|
||||
<FormDialogContext.Provider value={contextValue}>
|
||||
<Form {...form}>
|
||||
<form ref={formRef} className={cn("space-y-6", formClassName)}>
|
||||
{children}
|
||||
</form>
|
||||
</Form>
|
||||
</FormDialogContext.Provider>
|
||||
)
|
||||
|
||||
// 根据设备类型渲染不同的组件
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer open={isOpen} onOpenChange={(open) => !open && close()}>
|
||||
<DrawerContent className={className}>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{title}</DrawerTitle>
|
||||
<DrawerDescription>{description}</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="px-4 pb-4 overflow-y-auto max-h-[70vh]">
|
||||
{formContent}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
|
||||
<DialogContent className={className} showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className='overflow-y-auto max-h-[70vh]'>
|
||||
{formContent}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user