在前两篇博客 动态表单(一)、动态表单(二) 中,我们深入探讨了动态表单的基础实现和表单项的动态控制。本文将在此基础上,为动态表单增加分栏布局能力,让表单呈现更加专业的视觉效果,提升用户体验。

功能目标

  • 支持双栏布局展示表单项,优化空间利用率
  • 允许表单项进行跨栏合并,适应不同信息密度需求
  • 保持代码的简洁性和可维护性,便于后续扩展

设计思路

布局转换原理

实现分栏布局的核心在于数据结构的巧妙转换——将表单项的一维数组转换为二维数组。这个二维数组的每个子数组代表表单的一行,子数组内的元素则代表该行中的各个表单项。这种转换使得我们可以利用Element Plus的栅格系统来实现灵活布局。

属性设计

我们在表单项配置中引入两个新属性:

  • group - 定义表单项所属的分栏(默认值为1),1表示左栏,2表示右栏
  • span - 定义表单项需要合并的列数(默认值为1),大于1时表示需要跨栏

算法选择:双指针策略

考虑到只需处理单栏和双栏情况,我们采用双指针算法来高效构建布局二维数组。这种方法的时间复杂度为O(n),在处理大规模表单项时仍能保持高效性能。

算法执行步骤如下:

  1. 数据分组:将表单项按group属性分为左栏(group=1)和右栏(group=2)
  2. 简单情况处理:如果没有右栏项,直接将左栏项转换为二维数组
  3. 双指针处理:使用两个指针同步遍历左右栏数组,根据span属性决定如何组合
  4. 剩余项处理:将未遍历完的栏项单独成行

双指针处理机制详解

双指针处理是整个算法的核心环节,其执行逻辑可以通过以下步骤详细说明:

指针初始化与遍历 我们使用两个指针i和j分别指向左栏数组和右栏数组的当前位置。这两个指针同步移动,但移动规则取决于当前处理项的特性。

处理逻辑决策树

  1. 左栏项需要跨栏合并(span ≥ 2)

    • 当前左栏项需要独占整行空间
    • 将[left[i]]单独作为一行添加到结果数组中
    • i指针右移1位,j指针保持不动
    • 原因:在双栏布局中,跨栏项会"覆盖"掉原本应该出现在同一行的右栏项
  2. 左栏项不需要合并(span = 1或未设置)

    • 检查右栏对应位置是否有项
    • 如果存在右栏项:将[left[i], right[j]]组合为一行
    • 如果不存在右栏项:将[left[i]]单独作为一行
    • i和j指针同时右移1位

特殊情况处理

  • 当右栏项也需要合并时,由于双栏布局的限制,这种配置被视为无效,算法会忽略右栏项的合并需求,仅按普通项处理
  • 这种设计确保了布局的合理性和一致性,避免了复杂的嵌套逻辑

可视化执行过程 可以将双指针的处理过程想象成两个并行的传送带,每个表单项就像传送带上的物品。算法负责决定哪些物品应该放在同一个包装盒(行)中:

  • 普通物品:左右传送带各取一个,放在同一个盒子中
  • 大件物品:从左传送带取一个,独占一个盒子,右传送带的物品保留到下一轮

这种机制确保了:

  • 表单项的原始顺序得到保持
  • 跨栏项能够获得足够的显示空间
  • 布局结果在视觉上合理且符合预期

通过这种精细的控制逻辑,双指针算法能够高效地将一维表单项数组转换为适合分栏显示的二维布局数组,为后续的界面渲染提供结构化数据。

代码实现

数据分组

const left = currentDisplayFormItems.value.filter(
    (item) => !item.group || item.group === 1
  );
const right = currentDisplayFormItems.value.filter((item) => item.group >= 2);

处理无双栏情况

if (!right.length) {
  return left.map((item) => [item]);
}

双指针算法核心

const array = [];
let i = 0,
    j = 0;
for (; i < left.length && j < right.length;) {
  if (!left[i].span || left[i].span === 1) {
    if (right[j]) {
      array.push([left[i], right[j]]);
      i++;
      j++;
    } else {
      array.push([left[i]]);
      i++;
    }
  } else if (left[i].span >= 2) {
    array.push([left[i]]);
    i++;
  }
}
if (i === left.length && j < right.length) {
  for (; j < right.length; j++) {
    array.push([right[j]]);
  }
} else if (j === right.length && i < left.length) {
  for (; i < left.length; i++) {
    array.push([left[i]]);
  }
}
return array;

布局渲染

<el-form class="dynamic-form" :readonly="!极狐isEdit" :model="currentModel" :key="formKey">
  <el-row :gutter="24" v-for="(item, index) of displayedFormItemMatrix" :key="index">
    <el-col :span="24 / (item.length ?? 1)" v-for="col of item" :key="col.prop">
      <el-form-item :prop="col.prop" :label="col.label">
        <DynamicFormItem :is-edit="isEdit" :data="data" v-model:model="currentModel" :item="col">
        </DynamicFormItem>
      </el-form-item>
    </el-col>
  </el-row>
</el-form>

算法执行过程实例解析

示例场景

假设我们有以下表单项配置:

const formItems = [
  {prop: "name", label: "姓名", span: 2},      // 需要合并栏
  {prop: "age", label: "年龄"},                // 普通项
  {prop: "street", label: "街道", group: 2},   // 右栏项
  {prop: "city", label: "城市", group: 2}      // 右栏项
];

执行步骤分解

步骤1:初始化

  • 左栏(left): [{姓名}, {年龄}]
  • 右栏(right): [{街道}, {城市}]
  • 指针位置: i=0, j=0
  • 结果数组: []

步骤2:第一轮处理(i=0, j=0)

  • 检查left[0](姓名): span=2,需要合并栏
  • 操作: 将{姓名}单独作为一行
  • 结果: [[{姓名}]]
  • 指针移动: i=1, j=0

步骤3:第二轮处理(i=1, j=0)

  • 检查left[1](年龄): 不需要合并栏
  • 检查right[0](街道): 存在
  • 操作: 将{年龄}和{街道}组合为一行
  • 结果: [[{姓名}], [{年龄}, {街道}]]
  • 指针移动: i=2, j=1

步骤4:处理剩余项

  • i=2(已到left末尾), j=1(未到right末尾)
  • 操作: 将right剩余项{城市}单独作为一行
  • 最终结果: [[{姓名}], [{年龄}, {街道}], [{城市}]]

可视化表示

初始状态:
左栏: [姓名(span=2), 年龄]
右栏: [街道, 城市]
指针: i=0, j=0
结果: []

处理过程:
1. 处理左栏[0]: 姓名需要合并栏 → 单独成行
   结果: [[姓名]]
   指针: i=1, j=0

2. 处理左栏[1]: 年龄不需要合并栏 → 与右栏[0]组合
   结果: [[姓名], [年龄, 街道]]
   指针: i=2, j=1

3. 左栏处理完毕,处理右栏剩余项
   结果: [[姓名], [年龄, 街道], [城市]]

功能验证

html代码

<my-dynamic-form :is-edit="true" :data="newModel" v-model:model="newModel" :form-items="newFormItems"
        @update:model="changeFn"></my-dynamic-form>

我们通过三种典型场景验证分栏功能的正确性:

1. 单栏布局(默认效果)

当所有表单项均未设置分组时,表单保持原有的单栏布局,所有表单项垂直排列,占满整个宽度。这种布局适合内容较少或需要突出单个表单项的场景。

测试代码

const newFormItems = reactive([
  {
    prop: "name",
    label: "姓名",
    // span: 2,
    edit: {
      type: "text",

    },
  },
  {
    prop: "partition",
    label: "分区",
    edit: {
      type: "select",
      options: [
        { value: "one", label: "分区一" },
        { value: "two", label: "分区二" },
      ],
    },
  },
  {
    prop: "street",
    label: "街道",
    edit: {
      type: "select",
      options: [
        { value: "one", label: "街道一" },
        { value: "two", label: "街道二" },
      ],
    },
  },
  {
    prop: "age",
    label: "年龄",
    edit: {
      type: "number",

    },
  },
  {
    prop: "address",
    label: "详细地址",
    edit: {
      type: "textarea",
    },
  },
]);

效果

image.png

2. 双栏布局(无合并)

通过为部分表单项设置group: 2,实现标准的双栏布局,表单项按左右两栏排列。这种布局有效利用水平空间,适合中等复杂度的表单。

测试代码

const newFormItems = reactive([
  {
    prop: "name",
    label: "姓名",
    edit: {
      type: "text",

    },
  },
  {
    prop: "partition",
    label: "分区",
    edit: {
      type: "select",
      options: [
        { value: "one", label: "分区一" },
        { value: "two", label: "分区二" },
      ],
    },
  },
  {
    prop: "street",
    label: "街道",
    group: 2,
    edit: {
      type: "select",
      options: [
        { value: "one", label: "街道一" },
        { value: "two", label: "街道二" },
      ],
    },
  },
  {
    prop: "age",
    label: "年龄",
    edit: {
      type: "number",

    },
  },
  {
    prop: "address",
    label: "详细地址",
    edit: {
      type: "textarea",
    },
  },
]);

效果

image-1.png

3. 栏合并功能

通过span属性实现表单项的跨栏合并,特定表单项可以横跨两栏显示。这种布局适合需要突出重要信息或处理长文本输入的场景。

测试代码

const newFormItems = reactive([
  {
    prop: "name",
    label: "姓名",
    span: 2,
    edit: {
      type: "text",

    },
  },
  {
    prop: "partition",
    label: "分区",
    edit: {
      type: "select",
      options: [
        { value: "one", label: "分区一" },
        { value: "two", label: "分区二" },
      ],
    },
  },
  {
    prop: "street",
    label: "街道",
    group: 2,
    edit: {
      type: "select",
      options: [
        { value: "one", label: "街道一" },
        { value: "two", label: "街道二" },
      ],
    },
  },
  {
    prop: "age",
    label: "年龄",
    edit: {
      type: "number",
    },
  },
  {
    prop: "address",
    label: "详细地址",
    span: 2,
    edit: {
      type: "textarea",
    },
  },
]);

效果

image-2.png

总结

通过本文介绍的双指针算法,我们成功为动态表单添加了灵活的分栏布局能力。这种实现方式具有以下优势:

  1. 配置灵活:只需简单配置group和span属性即可实现复杂布局
  2. 性能高效:算法时间复杂度为O(n),处理大规模数据时仍保持高效
  3. 兼容性好:完全向下兼容,不影响现有表单功能和使用方式
  4. 扩展性强:算法设计为未来支持更多分栏场景留出了扩展空间

这种分栏设计方案不仅提升了表单的视觉效果,也为用户提供了更加清晰的信息组织结构,极大提升了表单填写体验。双指针算法的应用展示了如何通过简洁的代码实现复杂的布局逻辑,是前端开发中算法思维的典型应用。

希望本文对您实现动态表单的分栏布局有所帮助。如有任何问题或建议,欢迎在评论区交流讨论。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]