基于 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. 维护友好:分层设计,职责明确

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

相关推荐
liliangcsdn8 分钟前
mac mlx大模型框架的安装和使用
java·前端·人工智能·python·macos
CssHero12 分钟前
基于vue3完成领域模型架构建设
前端
PanZonghui15 分钟前
用项目说话:我的React博客构建成果与经验复盘
前端·react.js·typescript
挽淚17 分钟前
JavaScript 数组详解:从入门到精通
javascript
言兴18 分钟前
教你如何理解useContext加上useReducer
前端·javascript·面试
sunbyte21 分钟前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | GoodCheapFast(Good - Cheap - Fast三选二开关)
前端·javascript·css·vue.js·tailwindcss
7ayl22 分钟前
TCP 连接终止:四次挥手
面试
前端的日常23 分钟前
网页视频录制新技巧,代码实现超简单!
前端
前端的日常24 分钟前
什么是 TypeScript 中的泛型?请给出一个使用泛型的示例。
前端
今禾25 分钟前
一行代码引发的血案:new Array(5) 到底发生了什么?
前端·javascript·算法