基于 Shadcn 的可配置表单解决方案

在现代前端开发中,表单是用户交互最常见的组件之一。传统的表单开发往往需要大量重复代码,维护性差,扩展困难。本文将分析一套基于 shadcnReact Hook Form 的可配置表单系统,通过配置驱动的方式,大幅提升表单开发效率。

🎯 设计目标

这套表单系统解决了以下痛点:

  • 重复代码过多:每个表单都需要编写类似的验证、布局、提交逻辑
  • 维护困难:表单字段变更需要修改多处代码
  • 扩展性差:新增字段类型需要大量改动
  • 一致性问题:不同开发者实现的表单样式和交互不统一

🏗️ 系统架构

系统采用分层架构设计,各层职责清晰:

  • 配置层(Config):定义表单结构、字段类型、验证规则
  • 渲染层(Dialog):通用表单对话框组件,处理状态管理和生命周期
  • 字段层(Fields):具体的表单字段组件实现
  • 基础层(UI):基于 shadcn 的基础组件

📁 核心文件结构

bash 复制代码
dialog/
├── types.ts              # 类型定义
├── CreateSimpleDialog.tsx # 通用表单组件
├── configs/              # 表单配置
│   ├── articleConfig.ts
│   ├── mediaFileConfig.ts
│   └── socialAccountConfig.ts
└── fields/
    └── index.tsx         # 字段组件库

🧩 核心类型定义

系统的类型设计非常关键,确保了类型安全和开发体验:

typescript 复制代码
// 字段配置接口
export interface FieldConfig {
  name: string; // 字段名
  label: string; // 显示标签
  type: FieldType; // 字段类型
  required: boolean; // 是否必填
  component: React.ComponentType<any>; // 对应的组件
  props?: Record<string, unknown>; // 组件属性
  layout?: 'horizontal' | 'vertical'; // 布局方式
  controlWidth?: string; // 控件宽度
}

// 表单配置接口
export interface ContentConfig<T = Record<string, unknown>> {
  type: ContentType; // 表单类型
  title: string; // 标题
  buttonText: string; // 按钮文字
  fields: FieldConfig[]; // 字段配置
  defaultValues?: Partial<T>; // 默认值
  validate?: ValidationFunction<T>; // 验证函数
}

📝 配置驱动的表单定义

示例:文章表单配置

typescript 复制代码
export const articleConfig: ContentConfig<ArticleFormValues> = {
  type: 'article',
  title: '文章',
  buttonText: '添加文章',
  defaultValues: {
    title: '',
    content: null,
    status: 1,
    remark: null,
    externalUrl: null,
    priority: 1,
    authorId: '',
  },
  fields: [
    {
      name: 'title',
      label: '文章标题',
      type: 'input',
      required: true,
      component: TextInputField,
      layout: 'horizontal',
      controlWidth: '270px',
      props: {
        placeholder: '请输入文章标题',
      },
    },
    {
      name: 'authorId',
      label: '所属作者',
      type: 'select',
      required: true,
      component: AuthorSelectField,
      layout: 'horizontal',
      controlWidth: '270px',
    },
    // ... 更多字段配置
  ],
  validate: validateArticleForm,
};

示例:媒体文件表单配置

typescript 复制代码
export const mediaFileConfig: ContentConfig<MediaFileFormValues> = {
  type: 'mediaFile',
  title: '上传媒体文件',
  buttonText: '添加媒体文件',
  fields: [
    {
      name: 'selectedCategoryName',
      label: '资源库',
      type: 'searchSelect',
      required: true,
      component: CategorySearchSelectField,
    },
    {
      name: 'selectedAuthorId',
      label: '作者',
      type: 'select',
      required: true,
      component: MediaAuthorSelectField,
    },
    {
      name: 'selectedFile',
      label: '媒体文件',
      type: 'filePicker',
      required: true,
      component: MediaFilePickerField,
    },
  ],
  validate: validateMediaFileForm,
};

🎨 通用表单组件实现

CreateSimpleDialog 是系统的核心组件,它接收配置并渲染完整的表单:

typescript 复制代码
export const CreateSimpleDialog = <T = Record<string, unknown>,>({
  config,
  open: externalOpen,
  onOpenChange: externalOnOpenChange,
  onSubmit,
  isSubmitting = false,
  submitText,
  currentData,
}: CreateSimpleDialogProps<T>) => {

  // 创建动态表单的默认值
  const createDefaultValues = useCallback(() => {
    if (currentData) return currentData;

    const defaults: Record<string, unknown> = {};
    for (const field of config.fields) {
      if (config.defaultValues && field.name in config.defaultValues) {
        defaults[field.name] = config.defaultValues[field.name];
      } else {
        // 根据字段类型设置默认值
        switch (field.type) {
          case 'filePicker':
            defaults[field.name] = null;
            break;
          case 'number':
            defaults[field.name] = 0;
            break;
          default:
            defaults[field.name] = '';
        }
      }
    }
    return defaults;
  }, [config.fields, config.defaultValues, currentData]);

  const form = useForm({ defaultValues: createDefaultValues() });

  return (
    <Dialog open={open} onOpenChange={handleOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{config.title}</DialogTitle>
        </DialogHeader>

        <Form {...form}>
          <form onSubmit={handleSubmit}>
            {/* 动态渲染表单字段 */}
            {config.fields.map((fieldConfig) => (
              <ProFormField
                key={fieldConfig.name}
                layout={fieldConfig?.layout ?? 'horizontal'}
                name={fieldConfig.name}
                form={form}
                label={fieldConfig.label}
                controlWidth={fieldConfig?.controlWidth ?? '200px'}
                renderFormControl={(field) => {
                  const FieldComponent = fieldConfig.component;
                  return (
                    <FieldComponent
                      form={form}
                      value={field.value}
                      onChange={field.onChange}
                      disabled={isSubmitting}
                      {...(fieldConfig.props || {})}
                    />
                  );
                }}
              />
            ))}

            {/* 表单按钮 */}
            <div className="flex gap-4">
              <Button variant="ghost" onClick={() => setOpen(false)}>
                取消
              </Button>
              <Button type="submit" disabled={isSubmitting || !isFormValid()}>
                {isSubmitting ? '处理中...' : submitText || '确认'}
              </Button>
            </div>
          </form>
        </Form>
      </DialogContent>
    </Dialog>
  );
};

🛠️ 字段组件库

系统提供了丰富的字段组件,每个组件都遵循统一的接口规范:

文本输入字段

typescript 复制代码
export const TextInputField: React.FC<{
  value: string | null;
  onChange: (value: string | null) => void;
  disabled?: boolean;
  placeholder?: string;
}> = ({ value, onChange, disabled, placeholder }) => {
  return (
    <Input
      value={value || ''}
      onChange={(e) => onChange(e.target.value || null)}
      disabled={disabled}
      placeholder={placeholder}
      className="w-full"
    />
  );
};

选择器字段

typescript 复制代码
export const AuthorSelectField: React.FC<{
  value: string;
  onChange: (value: string) => void;
  disabled?: boolean;
}> = ({ value, onChange, disabled }) => {
  const { data: authors = [], isLoading } = useAuthorsQuery();

  if (isLoading) {
    return <Skeleton className="h-12 w-full rounded-md" />;
  }

  return (
    <Select value={value} onValueChange={onChange} disabled={disabled}>
      <SelectTrigger>
        <SelectValue placeholder="选择作者" />
      </SelectTrigger>
      <SelectContent>
        {authors.map((author) => (
          <SelectItem key={author.id} value={author.id}>
            {author.name}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
};

文件上传字段

typescript 复制代码
export const MediaFilePickerField: React.FC<{
  value: File | File[] | null;
  onChange: (file: File | File[] | null) => void;
  disabled?: boolean;
  maxFiles?: number;
}> = ({ value, onChange, disabled, maxFiles = 10 }) => {
  return (
    <FilePicker
      value={value}
      onChange={onChange}
      accept="image/*,video/*,audio/*"
      multiple={maxFiles > 1}
      maxFiles={maxFiles}
      disabled={disabled}
      className="w-full"
    />
  );
};

🔗 使用方式

使用这套系统非常简单,只需要引入配置和组件:

typescript 复制代码
import { CreateSimpleDialog } from '@/components/biz/dialog';
import { articleConfig } from '@/components/biz/dialog/configs';

function ArticleManagement() {
  const [isOpen, setIsOpen] = useState(false);

  const handleSubmit = async (values: ArticleFormValues) => {
    // 处理表单提交
    console.log('表单值:', values);
    // 调用 API 保存数据
    await saveArticle(values);
    setIsOpen(false);
  };

  return (
    <div>
      <CreateSimpleDialog
        config={articleConfig}
        open={isOpen}
        onOpenChange={setIsOpen}
        onSubmit={handleSubmit}
      />
    </div>
  );
}

🚀 扩展新字段类型

系统的扩展性很强,新增字段类型只需要三步:

1. 定义字段组件

typescript 复制代码
export const DatePickerField: React.FC<{
  value: Date | null;
  onChange: (value: Date | null) => void;
  disabled?: boolean;
}> = ({ value, onChange, disabled }) => {
  return (
    <DatePicker
      selected={value}
      onSelect={onChange}
      disabled={disabled}
    />
  );
};

2. 更新类型定义

typescript 复制代码
export type FieldType =
  | 'select'
  | 'input'
  | 'textarea'
  | 'number'
  | 'datePicker' // 新增
  | 'filePicker';

3. 在配置中使用

typescript 复制代码
{
  name: 'publishDate',
  label: '发布日期',
  type: 'datePicker',
  required: true,
  component: DatePickerField,
}

🎯 最佳实践

1. 配置文件组织

  • 按业务模块划分配置文件
  • 使用 TypeScript 严格类型
  • 提供完整的默认值

2. 字段组件设计

  • 保持接口一致性
  • 支持所有必要的属性
  • 处理加载和错误状态

3. 验证规则

  • 前端验证配合后端验证
  • 提供清晰的错误提示
  • 支持异步验证

📝 总结

这套基于 shadcn 的可配置表单系统通过以下方式大幅提升了开发效率:

  1. 配置驱动:通过配置文件定义表单,减少重复代码
  2. 类型安全:完整的 TypeScript 支持,减少运行时错误
  3. 组件复用:统一的字段组件库,保证一致性
  4. 易于扩展:清晰的架构设计,便于新增功能
  5. 维护友好:分层设计,职责明确

对于中大型项目中的表单开发,这套方案能够显著提升开发效率,降低维护成本,是一个值得借鉴的优秀实践。

相关推荐
zhangxingchao21 分钟前
Flutter中的页面跳转
前端
前端风云志42 分钟前
TypeScript实用类型之Omit
javascript
烛阴1 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝2 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇2 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军2 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加3 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript
Carlos_sam4 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖4 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby5 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js