You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Ant Design动态表单字段的上下移动排序功能

Ant Design动态表单实现行上下排序功能

问题描述

我开发了一个支持多字段添加的Ant Design动态表单,已经在表单行中添加了Up和Down按钮,但尚未实现changeOrder函数。需求是通过点击按钮实现对应表单行的上下移动(例如将sight 3移至sight 1和sight 2之间),从而调整表单字段的排序顺序,请问该如何实现这一功能?

原始代码

import React from "react";
import "./index.css";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { Button, Form, Input, Select, Space } from "antd";

const { Option } = Select;

const areas = [
  { label: "Beijing", value: "Beijing" },
  { label: "Shanghai", value: "Shanghai" }
];

const sights = {
  Beijing: ["Tiananmen", "Great Wall"],
  Shanghai: ["Oriental Pearl", "The Bund"]
};

type SightsKeys = keyof typeof sights;

const App: React.FC = () => {
  const [form] = Form.useForm();

  const onFinish = (values: any) => {
    console.log("Received values of form:", values);
  };

  const handleChange = () => {
    form.setFieldsValue({ sights: [] });
  };

  return (
    <Form
      form={form}
      name="dynamic_form_complex"
      onFinish={onFinish}
      style={{ maxWidth: 600 }}
      autoComplete="off"
    >
      <Form.Item
        name="area"
        label="Area"
        rules={[{ required: true, message: "Missing area" }]}
      >
        <Select options={areas} onChange={handleChange} />
      </Form.Item>
      <Form.List name="sights">
        {(fields, { add, remove }) => (
          <>
            {fields.map((field, index) => (
              <Space key={field.key} align="baseline">
                <Form.Item
                  noStyle
                  shouldUpdate={(prevValues, curValues) =>
                    prevValues.area !== curValues.area ||
                    prevValues.sights !== curValues.sights
                  }
                >
                  {() => (
                    <Form.Item
                      {...field}
                      label={`Sight ${field.name + 1}`}
                      name={[field.name, "sight"]}
                      rules={[{ required: true, message: "Missing sight" }]}
                    >
                      <Select
                        disabled={!form.getFieldValue("area")}
                        style={{ width: 130 }}
                      >
                        {( 
                          sights[form.getFieldValue("area") as SightsKeys] || []
                        ).map((item) => (
                          <Option key={item} value={item}>
                            {item}
                          </Option>
                        ))}
                      </Select>
                    </Form.Item>
                  )}
                </Form.Item>
                <Form.Item
                  noStyle
                  shouldUpdate={(prev, cur) =>
                    prev?.sights[field?.name]?.sight !==
                    cur?.sights[field?.name]?.sight
                  }
                >
                  {() => {
                    const disabled = !form.getFieldValue([
                      "sights",
                      field.name,
                      "sight"
                    ]);

                    return (
                      <Form.Item
                        {...field}
                        label="Price"
                        name={[field.name, "price"]}
                        rules={[{ required: true, message: "Missing price" }]}
                      >
                        <Input disabled={disabled} />
                      </Form.Item>
                    );
                  }}
                </Form.Item>

                <MinusCircleOutlined onClick={() => remove(field.name)} />
                <Button
                  style={{ marginTop: "30px" }}
                  onClick={() => changeOrder(index, "Up")}
                  block
                >
                  Up
                </Button>
                <Button
                  style={{ marginTop: "30px" }}
                  onClick={() => changeOrder(index, "Down")}
                  block
                >
                  Down
                </Button>
              </Space>
            ))}

            <Form.Item>
              <Button
                type="dashed"
                onClick={() => add()}
                block
                icon={<PlusOutlined />}
              >
                Add sights
              </Button>
            </Form.Item>
          </>
        )}
      </Form.List>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form.Item>
    </Form>
  );
};

export default App;

解决方案

实现思路

核心是操作Form.List绑定的sights数组数据:获取当前数组副本,交换目标行与相邻行的位置,再通过表单API更新数据,Ant Design Form会自动重新渲染列表完成排序。同时需要处理边界情况,避免无效操作。

修改后的完整代码

import React from "react";
import "./index.css";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { Button, Form, Input, Select, Space } from "antd";

const { Option } = Select;

const areas = [
  { label: "Beijing", value: "Beijing" },
  { label: "Shanghai", value: "Shanghai" }
];

const sights = {
  Beijing: ["Tiananmen", "Great Wall"],
  Shanghai: ["Oriental Pearl", "The Bund"]
};

type SightsKeys = keyof typeof sights;

const App: React.FC = () => {
  const [form] = Form.useForm();

  const onFinish = (values: any) => {
    console.log("Received values of form:", values);
  };

  const handleChange = () => {
    form.setFieldsValue({ sights: [] });
  };

  // 实现changeOrder函数
  const changeOrder = (index: number, direction: "Up" | "Down") => {
    const currentSights = form.getFieldValue("sights") || [];
    const newSights = [...currentSights];
    let targetIndex;

    if (direction === "Up") {
      // 第一行不能上移
      if (index === 0) return;
      targetIndex = index - 1;
    } else {
      // 最后一行不能下移
      if (index === newSights.length - 1) return;
      targetIndex = index + 1;
    }

    // 交换位置
    [newSights[index], newSights[targetIndex]] = [newSights[targetIndex], newSights[index]];
    // 更新表单值
    form.setFieldsValue({ sights: newSights });
  };

  return (
    <Form
      form={form}
      name="dynamic_form_complex"
      onFinish={onFinish}
      style={{ maxWidth: 600 }}
      autoComplete="off"
    >
      <Form.Item
        name="area"
        label="Area"
        rules={[{ required: true, message: "Missing area" }]}
      >
        <Select options={areas} onChange={handleChange} />
      </Form.Item>
      <Form.List name="sights">
        {(fields, { add, remove }) => (
          <>
            {fields.map((field, index) => (
              <Space key={field.key} align="baseline">
                <Form.Item
                  noStyle
                  shouldUpdate={(prevValues, curValues) =>
                    prevValues.area !== curValues.area ||
                    prevValues.sights !== curValues.sights
                  }
                >
                  {() => (
                    <Form.Item
                      {...field}
                      label={`Sight ${field.name + 1}`}
                      name={[field.name, "sight"]}
                      rules={[{ required: true, message: "Missing sight" }]}
                    >
                      <Select
                        disabled={!form.getFieldValue("area")}
                        style={{ width: 130 }}
                      >
                        {( 
                          sights[form.getFieldValue("area") as SightsKeys] || []
                        ).map((item) => (
                          <Option key={item} value={item}>
                            {item}
                          </Option>
                        ))}
                      </Select>
                    </Form.Item>
                  )}
                </Form.Item>
                <Form.Item
                  noStyle
                  shouldUpdate={(prev, cur) =>
                    prev?.sights[field?.name]?.sight !==
                    cur?.sights[field?.name]?.sight
                  }
                >
                  {() => {
                    const disabled = !form.getFieldValue([
                      "sights",
                      field.name,
                      "sight"
                    ]);

                    return (
                      <Form.Item
                        {...field}
                        label="Price"
                        name={[field.name, "price"]}
                        rules={[{ required: true, message: "Missing price" }]}
                      >
                        <Input disabled={disabled} />
                      </Form.Item>
                    );
                  }}
                </Form.Item>

                <MinusCircleOutlined onClick={() => remove(field.name)} />
                <Button
                  style={{ marginTop: "30px" }}
                  onClick={() => changeOrder(index, "Up")}
                  disabled={index === 0}
                  block
                >
                  Up
                </Button>
                <Button
                  style={{ marginTop: "30px" }}
                  onClick={() => changeOrder(index, "Down")}
                  disabled={index === fields.length - 1}
                  block
                >
                  Down
                </Button>
              </Space>
            ))}

            <Form.Item>
              <Button
                type="dashed"
                onClick={() => add()}
                block
                icon={<PlusOutlined />}
              >
                Add sights
              </Button>
            </Form.Item>
          </>
        )}
      </Form.List>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
      </Form.Item>
    </Form>
  );
};

export default App;

关键说明

  • changeOrder函数逻辑:
    1. 获取当前sights数组的副本(避免直接修改原数据)
    2. 根据方向判断操作是否合法(比如Up时索引不能为0,Down时不能是最后一个元素)
    3. 交换目标索引和当前索引的元素
    4. 通过form.setFieldsValue更新表单数据,触发列表重新渲染
  • 按钮禁用处理:给Up按钮添加disabled={index === 0},Down按钮添加disabled={index === fields.length - 1},防止无效点击。

内容的提问来源于stack exchange,提问作者Ranjan Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 10:04:53