Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑

This commit is contained in:
2025-11-13 15:24:54 +08:00
commit 42be39b343
249 changed files with 38843 additions and 0 deletions

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