208 lines
5.5 KiB
TypeScript
208 lines
5.5 KiB
TypeScript
"use client"
|
||
|
||
import type { ReactNode } from "react"
|
||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { cn } from "@/lib/utils"
|
||
|
||
export interface FormSection {
|
||
title?: string
|
||
description?: string
|
||
children: ReactNode
|
||
}
|
||
|
||
export interface FormLayoutProps {
|
||
/** 表单标题 */
|
||
title?: string
|
||
/** 表单描述 */
|
||
description?: string
|
||
/** 表单部分 */
|
||
sections?: FormSection[]
|
||
/** 表单内容 */
|
||
children?: ReactNode
|
||
/** 提交按钮文本 */
|
||
submitText?: string
|
||
/** 取消按钮文本 */
|
||
cancelText?: string
|
||
/** 是否显示取消按钮 */
|
||
showCancel?: boolean
|
||
/** 是否显示重置按钮 */
|
||
showReset?: boolean
|
||
/** 重置按钮文本 */
|
||
resetText?: string
|
||
/** 提交处理函数 */
|
||
onSubmit?: () => void
|
||
/** 取消处理函数 */
|
||
onCancel?: () => void
|
||
/** 重置处理函数 */
|
||
onReset?: () => void
|
||
/** 是否禁用提交按钮 */
|
||
submitDisabled?: boolean
|
||
/** 是否显示加载状态 */
|
||
loading?: boolean
|
||
/** 自定义底部内容 */
|
||
footer?: ReactNode
|
||
/** 自定义类名 */
|
||
className?: string
|
||
/** 是否使用卡片包装 */
|
||
withCard?: boolean
|
||
/** 表单布局方向 */
|
||
direction?: "vertical" | "horizontal"
|
||
/** 表单标签宽度 (仅在水平布局时有效) */
|
||
labelWidth?: string
|
||
}
|
||
|
||
/**
|
||
* 统一的表单布局组件
|
||
*/
|
||
export function FormLayout({
|
||
title,
|
||
description,
|
||
sections = [],
|
||
children,
|
||
submitText = "提交",
|
||
cancelText = "取消",
|
||
showCancel = true,
|
||
showReset = false,
|
||
resetText = "重置",
|
||
onSubmit,
|
||
onCancel,
|
||
onReset,
|
||
submitDisabled = false,
|
||
loading = false,
|
||
footer,
|
||
className,
|
||
withCard = true,
|
||
direction = "vertical",
|
||
labelWidth = "120px",
|
||
}: FormLayoutProps) {
|
||
const FormContent = () => (
|
||
<>
|
||
{/* 表单内容 */}
|
||
<div className={cn("space-y-6", direction === "horizontal" && "form-horizontal")}>
|
||
{/* 如果有sections,渲染sections */}
|
||
{sections.length > 0
|
||
? sections.map((section, index) => (
|
||
<div key={index} className="space-y-4">
|
||
{(section.title || section.description) && (
|
||
<div className="mb-4">
|
||
{section.title && <h3 className="text-lg font-medium">{section.title}</h3>}
|
||
{section.description && <p className="text-sm text-gray-500">{section.description}</p>}
|
||
</div>
|
||
)}
|
||
<div>{section.children}</div>
|
||
</div>
|
||
))
|
||
: // 否则直接渲染children
|
||
children}
|
||
</div>
|
||
|
||
{/* 表单底部 */}
|
||
{(onSubmit || onCancel || onReset || footer) && (
|
||
<div className="flex justify-end space-x-2 pt-6">
|
||
{footer || (
|
||
<>
|
||
{showReset && onReset && (
|
||
<Button type="button" variant="outline" onClick={onReset}>
|
||
{resetText}
|
||
</Button>
|
||
)}
|
||
{showCancel && onCancel && (
|
||
<Button type="button" variant="outline" onClick={onCancel}>
|
||
{cancelText}
|
||
</Button>
|
||
)}
|
||
{onSubmit && (
|
||
<Button
|
||
type="submit"
|
||
disabled={submitDisabled || loading}
|
||
onClick={onSubmit}
|
||
className={loading ? "opacity-70" : ""}
|
||
>
|
||
{loading ? "处理中..." : submitText}
|
||
</Button>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</>
|
||
)
|
||
|
||
// 添加水平布局的样式
|
||
if (direction === "horizontal") {
|
||
const style = document.createElement("style")
|
||
style.textContent = `
|
||
.form-horizontal .form-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.form-horizontal .form-label {
|
||
width: ${labelWidth};
|
||
flex-shrink: 0;
|
||
padding-top: 0.5rem;
|
||
}
|
||
.form-horizontal .form-field {
|
||
flex: 1;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.form-horizontal .form-item {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
.form-horizontal .form-label {
|
||
width: 100%;
|
||
margin-bottom: 0.5rem;
|
||
padding-top: 0;
|
||
}
|
||
}
|
||
`
|
||
document.head.appendChild(style)
|
||
}
|
||
|
||
// 根据是否需要卡片包装返回不同的渲染结果
|
||
if (withCard) {
|
||
return (
|
||
<Card className={className}>
|
||
{(title || description) && (
|
||
<CardHeader>
|
||
{title && <CardTitle>{title}</CardTitle>}
|
||
{description && <CardDescription>{description}</CardDescription>}
|
||
</CardHeader>
|
||
)}
|
||
<CardContent>
|
||
<FormContent />
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className={className}>
|
||
{(title || description) && (
|
||
<div className="mb-6">
|
||
{title && <h2 className="text-xl font-semibold">{title}</h2>}
|
||
{description && <p className="text-sm text-gray-500 mt-1">{description}</p>}
|
||
</div>
|
||
)}
|
||
<FormContent />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 表单项组件 - 用于水平布局
|
||
*/
|
||
export function FormItem({ label, required, children }: { label: string; required?: boolean; children: ReactNode }) {
|
||
return (
|
||
<div className="form-item">
|
||
<div className="form-label">
|
||
{required && <span className="text-red-500 mr-1">*</span>}
|
||
<span>{label}:</span>
|
||
</div>
|
||
<div className="form-field">{children}</div>
|
||
</div>
|
||
)
|
||
}
|