AI 驱动的表单智能化:从数据字段到最优交互布局的自动编排
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 的字段并列可能看起来"左重右轻"——左边的 select 组件和右边的 date picker 组件视觉体量不同。纯权重算法无法感知这种视觉重量差异。
-
行业惯例优先于算法:比如"身份证号"和"姓名"通常放在同一行(都是"身份信息"),但算法可能因为它们的权重组合超过 4 而拆成两行。这种"语义分组"超出了当前字段元数据的表达能力。
-
表单长度感知:一个 30 个字段的表单,算法可能排成 10 行——用户一看就头大。人类设计师会主动使用分步(Steps)、折叠面板(Collapse)来降低认知负担,但当前的布局算法只会做"平面排版"。
五、总结
AI 表单生成这件事,最合适的位置是"布局初稿生成器"。它能在 3 秒内完成设计师需要 2 小时的手动排版工作,产出 80% 正确率的布局。剩下的 20%——视觉平衡、行业惯例、认知负担管理——仍然需要人类设计师的介入。关键是设计好这个接口:AI 出初稿,人做微调,而不是让 AI 端出一个黑盒结果。
作者:李慕杰(Leo / 8limujie)
一个写了十年表单、终于把排版规则写成算法的前端匠人