forked from admin/hair-keeper
feat: 新增多步表单控件
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useRef } from 'react'
|
||||
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'
|
||||
@@ -19,20 +19,43 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Loader2, ChevronLeft, ChevronRight, Check } from 'lucide-react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
// 字段配置类型定义
|
||||
export interface StepFieldConfig {
|
||||
name: string
|
||||
label: string
|
||||
required?: boolean
|
||||
render: (props: { field: ControllerRenderProps<any, any> }) => React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
// 步骤配置类型定义
|
||||
export interface StepConfig {
|
||||
name: string
|
||||
title: string
|
||||
description?: string
|
||||
fields: StepFieldConfig[]
|
||||
className?: string // 步骤内容区域的网格样式
|
||||
}
|
||||
|
||||
// MultiStepFormDialog Context
|
||||
export interface MultiStepFormDialogContextValue {
|
||||
onCancel: () => void
|
||||
onPrevious: () => void
|
||||
onNext: () => void
|
||||
isSubmitting: boolean
|
||||
isValidating: boolean
|
||||
submitButtonText: string
|
||||
form: UseFormReturn<any>
|
||||
close: () => void
|
||||
steps: StepConfig[]
|
||||
currentStep: number
|
||||
totalSteps: number
|
||||
isFirstStep: boolean
|
||||
isLastStep: boolean
|
||||
goToStep: (step: number) => void
|
||||
goNext: () => void
|
||||
goPrev: () => void
|
||||
isLoading?: boolean
|
||||
canGoNext: () => Promise<boolean>
|
||||
}
|
||||
|
||||
const MultiStepFormDialogContext = createContext<MultiStepFormDialogContextValue | null>(null)
|
||||
@@ -45,223 +68,99 @@ export function useMultiStepFormDialogContext() {
|
||||
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 StepIndicatorProps {
|
||||
className?: string
|
||||
showLabels?: boolean
|
||||
clickable?: boolean
|
||||
}
|
||||
|
||||
// 步骤配置类型定义
|
||||
export interface StepConfig {
|
||||
title: string
|
||||
description?: string
|
||||
fields: FormFieldConfig[]
|
||||
}
|
||||
|
||||
// 多步骤表单操作按钮栏组件
|
||||
export function MultiStepFormActionBar() {
|
||||
const {
|
||||
onCancel,
|
||||
onPrevious,
|
||||
onNext,
|
||||
isSubmitting,
|
||||
isValidating,
|
||||
submitButtonText,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
} = useMultiStepFormDialogContext()
|
||||
export function StepIndicator({ className, showLabels = false, clickable = false }: StepIndicatorProps) {
|
||||
const { steps, currentStep, goToStep, isLoading } = useMultiStepFormDialogContext()
|
||||
|
||||
return (
|
||||
<div className="flex justify-between pt-6 border-t">
|
||||
<div className="flex space-x-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
<div className={cn("flex items-center justify-center gap-2", className)}>
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = index < currentStep
|
||||
const isCurrent = index === currentStep
|
||||
const isClickable = clickable && !isLoading && index <= currentStep
|
||||
|
||||
<div className="flex space-x-2">
|
||||
{!isFirstStep && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onPrevious}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||
上一步
|
||||
</Button>
|
||||
return (
|
||||
<React.Fragment key={step.name}>
|
||||
{index > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"h-0.5 w-8 transition-colors",
|
||||
index <= currentStep ? "bg-primary" : "bg-muted"
|
||||
)}
|
||||
{!isLastStep && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onNext}
|
||||
disabled={isSubmitting || isValidating}
|
||||
>
|
||||
{isValidating ? '验证中...' : '下一步'}
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isClickable}
|
||||
onClick={() => isClickable && goToStep(index)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 transition-colors",
|
||||
isClickable && "cursor-pointer hover:opacity-80",
|
||||
!isClickable && "cursor-default"
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={!isLastStep ? 'hidden' : ''}
|
||||
>
|
||||
{isSubmitting ? `${submitButtonText}中...` : submitButtonText}
|
||||
</Button>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center rounded-full border-2 text-sm font-medium transition-colors",
|
||||
isCompleted && "border-primary bg-primary text-primary-foreground",
|
||||
isCurrent && "border-primary text-primary",
|
||||
!isCompleted && !isCurrent && "border-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{isCompleted ? <Check className="h-4 w-4" /> : index + 1}
|
||||
</div>
|
||||
{showLabels && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-medium hidden sm:inline",
|
||||
isCurrent && "text-primary",
|
||||
!isCurrent && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface MultiStepFormDialogProps {
|
||||
isOpen: boolean
|
||||
title: string
|
||||
description: string
|
||||
form: UseFormReturn<any>
|
||||
onSubmit: (data: any) => Promise<void> | void
|
||||
steps: StepConfig[]
|
||||
contentClassName?: string
|
||||
gridClassName?: string
|
||||
|
||||
/* action */
|
||||
onClose: () => void
|
||||
isSubmitting: boolean
|
||||
submitButtonText: string
|
||||
// 步骤内容组件
|
||||
export interface StepContentProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MultiStepFormDialog({
|
||||
isOpen,
|
||||
title,
|
||||
description,
|
||||
form,
|
||||
onSubmit,
|
||||
steps,
|
||||
contentClassName = 'max-w-4xl',
|
||||
gridClassName = 'grid grid-cols-1 md:grid-cols-2 gap-4',
|
||||
onClose,
|
||||
isSubmitting,
|
||||
submitButtonText,
|
||||
}: MultiStepFormDialogProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [isValidating, setIsValidating] = useState(false)
|
||||
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]), button[role="combobox"]:not([disabled])'
|
||||
)
|
||||
if (firstInput) {
|
||||
firstInput.focus()
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isOpen, currentStep])
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
await onSubmit(data)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setCurrentStep(0)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleNext = async () => {
|
||||
if (currentStep < steps.length - 1) {
|
||||
setIsValidating(true)
|
||||
try {
|
||||
// 验证当前步骤的字段,只有验证通过才能跳转到下一步
|
||||
const currentStepFields = currentStepConfig.fields.map(field => field.name)
|
||||
const isValid = await form.trigger(currentStepFields)
|
||||
if (isValid) {
|
||||
setCurrentStep(currentStep + 1)
|
||||
}
|
||||
} finally {
|
||||
setIsValidating(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const isFirstStep = currentStep === 0
|
||||
const isLastStep = currentStep === steps.length - 1
|
||||
export function StepContent({ className = 'grid grid-cols-1 gap-4' }: StepContentProps) {
|
||||
const { form, steps, currentStep, isLoading } = useMultiStepFormDialogContext()
|
||||
const currentStepConfig = steps[currentStep]
|
||||
|
||||
// 步骤指示器组件
|
||||
const stepIndicator = (
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
{steps.map((step, index) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
|
||||
index <= currentStep
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="ml-2 text-sm">
|
||||
<div className={index <= currentStep ? 'text-primary font-medium' : 'text-muted-foreground'}>
|
||||
{step.title}
|
||||
</div>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`w-12 h-0.5 mx-4 ${index < currentStep ? 'bg-primary' : 'bg-muted'}`} />
|
||||
)}
|
||||
if (!currentStepConfig) return null
|
||||
|
||||
// 如果正在加载,显示骨架屏
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={cn("p-1", className, currentStepConfig.className)}>
|
||||
{currentStepConfig.fields.map((fieldConfig) => (
|
||||
<div key={fieldConfig.name} className={cn("space-y-2", fieldConfig.className || '')}>
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Context 值
|
||||
const contextValue: MultiStepFormDialogContextValue = {
|
||||
onCancel: handleClose,
|
||||
onPrevious: handlePrevious,
|
||||
onNext: handleNext,
|
||||
isSubmitting,
|
||||
isValidating,
|
||||
submitButtonText,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}
|
||||
|
||||
// 表单内容组件
|
||||
const formContent = (
|
||||
<MultiStepFormDialogContext.Provider value={contextValue}>
|
||||
<div className="space-y-4">
|
||||
<Form {...form}>
|
||||
<form ref={formRef} onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
{/* 当前步骤标题和描述 */}
|
||||
<div className="border-b pb-4">
|
||||
<h3 className="text-lg font-medium">{currentStepConfig.title}</h3>
|
||||
{currentStepConfig.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{currentStepConfig.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 当前步骤的字段 */}
|
||||
<div className={cn("p-1", gridClassName)}>
|
||||
return (
|
||||
<div className={cn("p-1", className, currentStepConfig.className)}>
|
||||
{currentStepConfig.fields.map((fieldConfig) => (
|
||||
<FormField
|
||||
key={fieldConfig.name}
|
||||
@@ -282,26 +181,345 @@ export function MultiStepFormDialog({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<MultiStepFormActionBar />
|
||||
// 步骤标题组件(显示当前步骤的标题和描述)
|
||||
export interface StepHeaderProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function StepHeader({ className }: StepHeaderProps) {
|
||||
const { steps, currentStep } = useMultiStepFormDialogContext()
|
||||
const currentStepConfig = steps[currentStep]
|
||||
|
||||
if (!currentStepConfig) return null
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-1", className)}>
|
||||
<h4 className="font-medium">{currentStepConfig.title}</h4>
|
||||
{currentStepConfig.description && (
|
||||
<p className="text-sm text-muted-foreground">{currentStepConfig.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 上一步按钮组件
|
||||
export interface PrevStepActionProps extends Omit<React.ComponentProps<typeof Button>, 'onClick' | 'type'> {
|
||||
children?: React.ReactNode
|
||||
onPrev?: () => void
|
||||
}
|
||||
|
||||
export function PrevStepAction({
|
||||
children,
|
||||
variant = 'outline',
|
||||
onPrev,
|
||||
...props
|
||||
}: PrevStepActionProps) {
|
||||
const { goPrev, isFirstStep, isLoading } = useMultiStepFormDialogContext()
|
||||
|
||||
const handleClick = () => {
|
||||
if (onPrev) {
|
||||
onPrev()
|
||||
} else {
|
||||
goPrev()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
onClick={handleClick}
|
||||
disabled={isFirstStep || isLoading || props.disabled}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||
上一步
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// 下一步按钮组件
|
||||
export interface NextStepActionProps extends Omit<React.ComponentProps<typeof Button>, 'onClick' | 'type'> {
|
||||
children?: React.ReactNode
|
||||
onNext?: () => Promise<boolean> | boolean
|
||||
validateCurrentStep?: boolean
|
||||
}
|
||||
|
||||
export function NextStepAction({
|
||||
children,
|
||||
variant = 'default',
|
||||
onNext,
|
||||
validateCurrentStep = true,
|
||||
...props
|
||||
}: NextStepActionProps) {
|
||||
const { goNext, isLastStep, isLoading, canGoNext } = useMultiStepFormDialogContext()
|
||||
const [isValidating, setIsValidating] = useState(false)
|
||||
|
||||
const handleClick = async () => {
|
||||
setIsValidating(true)
|
||||
try {
|
||||
if (validateCurrentStep) {
|
||||
const canProceed = await canGoNext()
|
||||
if (!canProceed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (onNext) {
|
||||
const shouldProceed = await onNext()
|
||||
if (!shouldProceed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
goNext()
|
||||
} finally {
|
||||
setIsValidating(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLastStep) return null
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
onClick={handleClick}
|
||||
disabled={isLoading || isValidating || props.disabled}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
下一步
|
||||
<ChevronRight className="ml-1 h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
{isValidating && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// 取消按钮组件
|
||||
export interface StepCancelActionProps extends Omit<React.ComponentProps<typeof Button>, 'onClick' | 'type'> {
|
||||
children?: React.ReactNode
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function StepCancelAction({ children = '取消', variant = 'outline', onCancel, ...props }: StepCancelActionProps) {
|
||||
const { close } = useMultiStepFormDialogContext()
|
||||
|
||||
const handleClick = () => {
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button type="button" variant={variant} onClick={handleClick} disabled={props.disabled} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// 提交按钮组件(仅在最后一步显示)
|
||||
export interface StepSubmitActionProps extends Omit<React.ComponentProps<typeof Button>, 'onClick' | 'type'> {
|
||||
onSubmit: (data: any) => Promise<void> | void
|
||||
children?: React.ReactNode
|
||||
disabled?: boolean
|
||||
isSubmitting?: boolean
|
||||
showSpinningLoader?: boolean
|
||||
showOnlyOnLastStep?: boolean
|
||||
}
|
||||
|
||||
export function StepSubmitAction({
|
||||
onSubmit,
|
||||
children = '提交',
|
||||
disabled = false,
|
||||
isSubmitting = false,
|
||||
showSpinningLoader = true,
|
||||
showOnlyOnLastStep = true,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: StepSubmitActionProps) {
|
||||
const { form, isLastStep, isLoading } = useMultiStepFormDialogContext()
|
||||
|
||||
if (showOnlyOnLastStep && !isLastStep) return null
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
disabled={isSubmitting || disabled || isLoading}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{isSubmitting && showSpinningLoader && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// 操作按钮栏组件
|
||||
export interface StepActionBarProps {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function StepActionBar({ children, className }: StepActionBarProps) {
|
||||
return (
|
||||
<div className={cn("flex justify-between", className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 左侧操作区
|
||||
export function StepActionBarLeft({ children, className }: { children?: React.ReactNode; className?: string }) {
|
||||
return <div className={cn("flex gap-2", className)}>{children}</div>
|
||||
}
|
||||
|
||||
// 右侧操作区
|
||||
export function StepActionBarRight({ children, className }: { children?: React.ReactNode; className?: string }) {
|
||||
return <div className={cn("flex gap-2", className)}>{children}</div>
|
||||
}
|
||||
|
||||
// 主对话框组件
|
||||
export interface MultiStepFormDialogProps {
|
||||
isOpen: boolean
|
||||
title: string
|
||||
description: string
|
||||
form: UseFormReturn<any>
|
||||
steps: StepConfig[]
|
||||
onClose: () => void
|
||||
className?: string
|
||||
formClassName?: string
|
||||
children: React.ReactNode
|
||||
isLoading?: boolean
|
||||
initialStep?: number
|
||||
}
|
||||
|
||||
export function MultiStepFormDialog({
|
||||
isOpen,
|
||||
title,
|
||||
description,
|
||||
form,
|
||||
steps,
|
||||
onClose,
|
||||
className = 'max-w-lg',
|
||||
formClassName,
|
||||
children,
|
||||
isLoading = false,
|
||||
initialStep = 0,
|
||||
}: MultiStepFormDialogProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const [currentStep, setCurrentStep] = useState(initialStep)
|
||||
|
||||
// 重置步骤当对话框关闭或重新打开时
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setCurrentStep(initialStep)
|
||||
}
|
||||
}, [isOpen, initialStep])
|
||||
|
||||
// 当对话框打开或步骤变化时,自动聚焦到第一个表单输入控件
|
||||
useEffect(() => {
|
||||
if (isOpen && !isLoading) {
|
||||
(document.activeElement as HTMLElement)?.blur()
|
||||
const timer = setTimeout(() => {
|
||||
if (formRef.current) {
|
||||
const firstInput = formRef.current.querySelector<HTMLElement>(
|
||||
'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button[role="combobox"]:not([disabled])'
|
||||
)
|
||||
if (firstInput) {
|
||||
firstInput.focus()
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isOpen, isLoading, currentStep])
|
||||
|
||||
const close = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
const totalSteps = steps.length
|
||||
const isFirstStep = currentStep === 0
|
||||
const isLastStep = currentStep === totalSteps - 1
|
||||
|
||||
const goToStep = (step: number) => {
|
||||
if (step >= 0 && step < totalSteps) {
|
||||
setCurrentStep(step)
|
||||
}
|
||||
}
|
||||
|
||||
const goNext = () => {
|
||||
if (!isLastStep) {
|
||||
setCurrentStep((prev) => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const goPrev = () => {
|
||||
if (!isFirstStep) {
|
||||
setCurrentStep((prev) => prev - 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证当前步骤的字段
|
||||
const canGoNext = async (): Promise<boolean> => {
|
||||
const currentStepConfig = steps[currentStep]
|
||||
if (!currentStepConfig) return true
|
||||
|
||||
const fieldNames = currentStepConfig.fields.map((f) => f.name)
|
||||
const result = await form.trigger(fieldNames as any)
|
||||
return result
|
||||
}
|
||||
|
||||
const contextValue: MultiStepFormDialogContextValue = {
|
||||
form,
|
||||
close,
|
||||
steps,
|
||||
currentStep,
|
||||
totalSteps,
|
||||
isFirstStep,
|
||||
isLastStep,
|
||||
goToStep,
|
||||
goNext,
|
||||
goPrev,
|
||||
isLoading,
|
||||
canGoNext,
|
||||
}
|
||||
|
||||
const formContent = (
|
||||
<MultiStepFormDialogContext.Provider value={contextValue}>
|
||||
<Form {...form}>
|
||||
<form ref={formRef} className={cn("space-y-6", formClassName)}>
|
||||
{children}
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</MultiStepFormDialogContext.Provider>
|
||||
)
|
||||
|
||||
// 根据设备类型渲染不同的组件
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer open={isOpen} onOpenChange={handleClose}>
|
||||
<DrawerContent>
|
||||
<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]">
|
||||
{stepIndicator}
|
||||
{formContent}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
@@ -310,14 +528,13 @@ export function MultiStepFormDialog({
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className={contentClassName}>
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
|
||||
<DialogContent className={className} showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{stepIndicator}
|
||||
<div className='overflow-y-auto max-h-[70vh]'>
|
||||
<div className="overflow-y-auto max-h-[70vh]">
|
||||
{formContent}
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user