一、表单布局自动化的核心驱动力
做过后台管理系统的开发者都清楚——表单页面是前端开发中工作量最密集的环节之一。一个中等复杂度的"用户信息编辑"页面,涉及十几个字段,每个字段都关联着校验规则、依赖关系、条件显示和响应式布局适配。从产品经理输出 PRD 到前端完成交付,通常需要数个工作日,遇到字段联动复杂的场景还会更久。
但仔细拆解后会发现:表单布局的决策逻辑其实有很强的可形式化特征。哪些字段适合半宽排列?哪些必须独占一行?哪些可以收进折叠面板?哪些字段间的联动关系要求它们出现在同一屏?这些问题的答案都可以用规则来描述,而这正是 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 | 全宽+最大高度 | 文章正文 |
权重值的核心逻辑是:输入控件越复杂、占用空间越大,权重越高。权重为 1 的字段可两两并列,权重 2 的字段独占半行以上,权重 3 或 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;
if (field.placeholder && field.placeholder.length > 30) {
weight = Math.max(weight, 2);
}
return weight;
}
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 = [];
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;
}
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 的字段并排放置,左侧的下拉选择器和右侧的日期选择器在视觉体量上存在差异,但纯权重算法无法感知这种差异,排出来的布局可能"左重右轻"。
第二,行业惯例优先级高于算法逻辑。例如"身份证号"和"姓名"通常放在同一行(同属"身份信息"语义分组),但算法可能因为权重总和超过 4 而将它们拆到两行。这种语义级别的分组判断需要更丰富的元数据支持。
第三,长表单的认知负担管理。一个 30 字段的表单按权重排成 10 行,用户一眼看去就感到压力。人类设计师会主动采用分步(Steps)、折叠面板(Collapse)等模式来降低认知负荷,但当前 AI 布局引擎只做"平面排版",没有这种结构分化能力。
五、总结:AI 出初稿,人做微调
AI 表单生成最合适的定位是"布局初稿生成器"。三秒内可以完成设计师需要两小时的手动排版,产出 80% 正确率的排版。剩下的 20%——视觉平衡、行业惯例遵从、认知负担管理——仍需要人类设计师介入。关键在于设计好协作接口:AI 输出可编辑的初稿,人做针对性微调,而不是让 AI 端出一个不可干预的黑盒结果。

评论0