AI 驱动的表单智能化:从数据字段到最优交互布局的自动编排

AI2周前发布 beixibaobao
14 0 0

AI 驱动的表单智能化:从数据字段到最优交互布局的自动编排

一、表单是 AI 最容易介入、但又最容易被忽视的 UI 场景

做过后台系统的前端都懂——表单是工作量黑洞。一个"用户信息编辑"页面,15 个字段,不同字段之间有不同的依赖关系、校验规则、显示条件,外加适配移动端的响应式布局。从产品出 PRD 到前端交付,保守估计 3 个工作日。

但如果你仔细观察:表单的布局决策其实有很强的规律性。哪些字段应该横排?哪些应该全宽?哪些可以合并到一个折叠区域?哪些字段之间有联动关系?这些决策规则是可以用算法描述的——而这正是 AI UI 生成的理想场景。

这篇文章,我会从字段分析、布局编排、响应式适配三个层面,拆解一个 AI 驱动的表单自动生成系统。

二、字段语义分析与布局决策树

字段的布局权重计算

每个字段根据它的类型和预期输入长度,被分配一个"布局权重":

字段类型 布局权重 默认列宽策略 示例
text (short) 1 半宽(可与另一个1权重字段并列) 姓名、手机号
text (medium) 2 全宽 详细地址、公司名称
textarea 3 全宽 + 较高行数 备注、简介
select 1 半宽 性别、状态
date 1 半宽或 1/3 宽 生日、开始日期
file/image 2 全宽 上传区域
radio/checkbox 1-2(按选项数量) 按内容宽度自适应 偏好设置
number 1 半宽或 1/3 宽 金额、数量
richtext 4 全宽 + 最大高度 文章正文

三、表单布局编排引擎的实现

/**
 * AI 表单布局编排引擎
 */
interface FormField {
  key: string;
  type: 'text' | 'textarea' | 'select' | 'date' | 'number' | 'file' | 'radio' | 'checkbox' | 'richtext';
  label: string;
  required: boolean;
  placeholder?: string;
  /** 预估的输入长度 */
  estimatedLength: 'short' | 'medium' | 'long';
  /** 依赖的字段(联动关系) */
  dependsOn?: string[];
  /** 显示条件 */
  showWhen?: (values: Record<string, any>) => boolean;
}
interface LayoutRow {
  fields: FormField[];
  /** 每列占比 */
  columns: number[];
}
class FormLayoutEngine {
  /**
   * 核心布局算法:贪心装箱
   *
   * 将字段依次放入行中,每行总权重不超过 4
   * 权重 1 = 1/4 行宽, 权重 2 = 半宽, 权重 3 = 3/4, 权重 4 = 全宽
   */
  generateLayout(fields: FormField[]): LayoutRow[] {
    // 第一步:计算每个字段的布局权重
    const weighted = fields.map(f => ({
      field: f,
      weight: this.calculateWeight(f),
    }));
    // 第二步:按关联关系分组
    const grouped = this.groupByDependencies(weighted);
    // 第三步:贪心装箱
    const rows: LayoutRow[] = [];
    const MAX_ROW_WEIGHT = 4;
    for (const group of grouped) {
      // 每个依赖组保持在同一行
      const totalWeight = group.reduce((sum, item) => sum + item.weight, 0);
      if (totalWeight <= MAX_ROW_WEIGHT) {
        rows.push({
          fields: group.map(g => g.field),
          columns: group.map(g => g.weight / totalWeight),
        });
      } else {
        // 超过行容量:拆分
        let currentRow: typeof group = [];
        let currentWeight = 0;
        for (const item of group) {
          if (currentWeight + item.weight <= MAX_ROW_WEIGHT) {
            currentRow.push(item);
            currentWeight += item.weight;
          } else {
            // 当前行满,提交并开始新行
            rows.push({
              fields: currentRow.map(g => g.field),
              columns: currentRow.map(g => g.weight / currentWeight),
            });
            currentRow = [item];
            currentWeight = item.weight;
          }
        }
        // 提交最后一行
        if (currentRow.length > 0) {
          rows.push({
            fields: currentRow.map(g => g.field),
            columns: currentRow.map(g => g.weight / currentWeight),
          });
        }
      }
    }
    return rows;
  }
  /** 计算字段布局权重 */
  private calculateWeight(field: FormField): number {
    const typeWeights: Record<string, number> = {
      text: field.estimatedLength === 'short' ? 1 : 2,
      textarea: 3,
      select: 1,
      date: 1,
      number: 1,
      file: 2,
      radio: 1,
      checkbox: 1,
      richtext: 4,
    };
    let weight = typeWeights[field.type] ?? 1;
    // 必填字段:权重不变,但标记星号
    // 长的 placeholder:可能在移动端需要更多空间
    if (field.placeholder && field.placeholder.length > 30) {
      weight = Math.max(weight, 2);
    }
    return weight;
  }
  /**
   * 按依赖关系分组
   *
   * 规则:
   * 1. 有 dependsOn 关系的字段与它的依赖项分到同一组
   * 2. 同一组内的字段布局保持连续
   */
  private groupByDependencies(items: Array<{ field: FormField; weight: number }>) {
    const groups: Array<Array<{ field: FormField; weight: number }>> = [];
    const visited = new Set<string>();
    // 先收集所有依赖关系
    const dependencyMap = new Map<string, string[]>();
    for (const item of items) {
      if (item.field.dependsOn && item.field.dependsOn.length > 0) {
        for (const dep of item.field.dependsOn) {
          if (!dependencyMap.has(dep)) {
            dependencyMap.set(dep, []);
          }
          dependencyMap.get(dep)!.push(item.field.key);
        }
      }
    }
    for (const item of items) {
      if (visited.has(item.field.key)) continue;
      const group: typeof items = [];
      // BFS 收集关联字段
      const queue = [item.field.key];
      while (queue.length > 0) {
        const key = queue.shift()!;
        if (visited.has(key)) continue;
        visited.add(key);
        const found = items.find(i => i.field.key === key);
        if (found) group.push(found);
        // 添加被依赖的字段
        const dependents = dependencyMap.get(key) || [];
        queue.push(...dependents.filter(d => !visited.has(d)));
      }
      groups.push(group);
    }
    return groups;
  }
  /**
   * 响应式断点调整
   *
   * 在移动端(<768px),所有行都变成单列
   */
  generateResponsiveLayout(rows: LayoutRow[], breakpoint: number) {
    return rows.map(row => {
      if (breakpoint < 768) {
        return {
          fields: row.fields,
          columns: row.fields.map(() => 1), // 全部单列
        };
      }
      return row;
    });
  }
}
/**
 * 表单校验规则自动生成
 */
class ValidationRuleGenerator {
  generate(field: FormField) {
    const rules: any[] = [];
    if (field.required) {
      rules.push({
        required: true,
        message: `${field.label}不能为空`,
      });
    }
    switch (field.type) {
      case 'text':
        if (field.key.includes('phone') || field.key.includes('mobile')) {
          rules.push({
            pattern: /^1[3-9]d{9}$/,
            message: '请输入正确的手机号',
          });
        }
        if (field.key.includes('email')) {
          rules.push({
            type: 'email',
            message: '请输入正确的邮箱地址',
          });
        }
        break;
      case 'number':
        if (field.key.includes('age')) {
          rules.push({
            type: 'number',
            min: 1,
            max: 150,
            message: '请输入合理的年龄',
          });
        }
        break;
    }
    return rules;
  }
}

四、局限性:什么时候 AI 排的表单不如人

三个关键边界:

  1. 字段权重算法无法处理"视觉平衡":两个权重为 1 的字段并列可能看起来"左重右轻"——左边的 select 组件和右边的 date picker 组件视觉体量不同。纯权重算法无法感知这种视觉重量差异。

  2. 行业惯例优先于算法:比如"身份证号"和"姓名"通常放在同一行(都是"身份信息"),但算法可能因为它们的权重组合超过 4 而拆成两行。这种"语义分组"超出了当前字段元数据的表达能力。

  3. 表单长度感知:一个 30 个字段的表单,算法可能排成 10 行——用户一看就头大。人类设计师会主动使用分步(Steps)、折叠面板(Collapse)来降低认知负担,但当前的布局算法只会做"平面排版"。

五、总结

AI 表单生成这件事,最合适的位置是"布局初稿生成器"。它能在 3 秒内完成设计师需要 2 小时的手动排版工作,产出 80% 正确率的布局。剩下的 20%——视觉平衡、行业惯例、认知负担管理——仍然需要人类设计师的介入。关键是设计好这个接口:AI 出初稿,人做微调,而不是让 AI 端出一个黑盒结果。


作者:李慕杰(Leo / 8limujie)
一个写了十年表单、终于把排版规则写成算法的前端匠人

© 版权声明

相关文章