React中使用react-sortablejs实现自定义表单设计器,实现拖拽,排序功能

React中使用react-sortablejs实现自定义表单设计器,实现拖拽,排序功能

1.前言

最近做了一个自定义配置布局的需求,项目的最终实现效果如下:
在这里插入图片描述
需求使用的就是react-sortablejs,于是就打算简单再写个自定义表单设计器的demo,我会在下面的例子里详细的提供,各个文件的代码,让大家直接复制粘贴就能使用,以供大家参考。

2.简单介绍react-sortablejs

React-Sortablejs是一个基于React的可排序列表组件,它使用Sortable.js来实现排序功能。

使用React-Sortablejs,您可以轻松地创建可拖动和可排序的列表,并可以在React应用程序中进行自定义。它支持各种功能,例如拖动和放置,延迟拖动,动画和触摸控制。

官方api文档如下:

http://www.sortablejs.com/options.html

gitHub地址如下:

https://github.com/SortableJS/react-sortablejs

这里就不细聊各个api的作用了。有需要了解的可以自行查看。

3.自定义表单设计开发

1.组件目录

在这里插入图片描述

2.各个文件的代码

安装react-sortablejs
npm install react-sortablejs
1.index.js
import React, { useState } from "react";

import { Row, Col, Button } from "antd";
import { FormSetDiv } from "./style.js";
import FieldSetLeft from "./FieldSetLeft";
import FieldSetCenter from "./FieldSetCenter";
import FieldSetRight from "./FieldSetRight";

const FormFieldSet = () => {
  const [currentField, setCurrentField] = useState({});
  const [fieldList, setFieldList] = useState([]);

  return (
    <FormSetDiv>
      <div className="fieldSet-header">
        <Button type="primary">保存</Button>
      </div>
      <div className="fieldSet-main">
        <Row className="fieldSet-main-content">
          <Col span={3} className="fieldSet-main-content-left">
            <FieldSetLeft />
          </Col>
          <Col span={15} className="fieldSet-main-content-center">
            <FieldSetCenter
              fieldList={fieldList}
              currentField={currentField}
              onFieldChange={(list) => setFieldList(list)}
              handleChooseField={(field) => setCurrentField(field)}
            />
          </Col>
          <Col span={6} className="fieldSet-main-content-right">
            <FieldSetRight
              fieldList={fieldList}
              currentField={currentField}
              onFieldChange={(list) => setFieldList(list)}
              handleChooseField={(field) => setCurrentField(field)}
            />
          </Col>
        </Row>
      </div>
    </FormSetDiv>
  );
};

export default FormFieldSet;
2.style.js
import styled from "styled-components";

export const FormSetDiv = styled.div`
  height: 100vh;
  .fieldSet-header {
    padding: 16px;
    text-align: right;
    border-bottom: 1px solid #eee;
  }
  .fieldSet-main {
    padding: 16px 0px;
    height: calc(100% - 65px);
    box-sizing: border-box;
    &-content {
      height: 100%;
      &-left {
        height: 100%;
        overflow: auto;
        padding: 0px 16px 16px;
        .title {
          font-size: 16px;
          margin-bottom: 12px;
        }
        .sort-item {
          padding: 10px;
          border: 1px solid #eee;
          text-align: center;
          margin-bottom: 12px;
          border: 0.5px solid rgba(206, 210, 216, 1);
          box-shadow: 0px 2px 4px 0px rgba(49, 74, 111, 0.07);
          border-radius: 8px;
          font-size: 14px;
          color: #191c1f;
          cursor: move;
          &:hover {
            background: #e6f1fe;
            border: 0.5px solid rgba(27, 132, 251, 1);
            box-shadow: 0px 2px 4px 0px rgba(49, 74, 111, 0.08);
            color: #0077fa;
          }
        }
      }
      &-center {
        padding: 8px 8px 15px;
        background-color: #f3f6fc;
        height: 100%;
        overflow: auto;
        box-sizing: border-box;
        .field-center {
          height: 100%;
          overflow: auto;
          background-color: #fff;
          padding: 15px 12px;
          box-sizing: border-box;
          .main-tooltip {
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 14px;
            color: #989898;
          }
          .sortable-chosen,
          .sortable-ghost,
          .sortable-drag {
            cursor: move;
          }
          .main-field {
            cursor: move;
            position: relative;
            padding: 16px 20px;
            box-sizing: border-box;
            border: 1px solid transparent;
            border-radius: 8px;
            margin-bottom: 8px;
            &:hover {
              background: rgba(255, 255, 255, 0.44);
              border: 1px dashed rgba(10, 124, 251, 1);
              .main-field-delete {
                display: block;
              }
            }
            &-delete {
              font-size: 18px;
              position: absolute;
              top: 2px;
              right: 2px;
              color: #448aff;
              cursor: pointer;
              display: none;
            }
            .field-item {
              display: flex;
              align-items: center;
              .field-label {
                margin-right: 10px;
                width: 160px;
                text-align: right;
              }
              .field-form {
                flex: 1;
              }
            }
          }
        }
      }
      &-right {
        padding: 0px 16px;
        .title {
          font-size: 16px;
          margin-bottom: 12px;
        }
        .content {
          .label {
            margin-bottom: 10px;
          }
        }
      }
    }
  }
`;
3.FieldSetLeft.js
import React from "react";

import { ReactSortable } from "react-sortablejs";

const fieldList = [
  {
    fieldKey: "Input",
    fieldLabel: "单行文本框",
    fieldConfig: {},
  },
  {
    fieldKey: "TextArea",
    fieldLabel: "多行文本框",
    fieldConfig: {},
  },
  {
    fieldKey: "DatePicker",
    fieldLabel: "日期",
    fieldConfig: {},
  },
  {
    fieldKey: "Radio",
    fieldLabel: "单选框",
    fieldConfig: {},
  },
  {
    fieldKey: "Checkbox",
    fieldLabel: "多选框",
    fieldConfig: {},
  },
];

const FieldSetLeft = () => {
  return (
    <>
      <div className="title">基础组件</div>
      <ReactSortable
        list={fieldList}
        animation={200}
        group={{
          name: "sort-field",
          pull: "clone",
          put: false,
        }}
        setList={() => {}}
        sort={false}
        style={{ overflow: "auto" }}
        forceFallback={true}
      >
        {fieldList.map((item) => {
          return (
            <div className="sort-item" key={item?.fieldKey}>
              {item?.fieldLabel}
            </div>
          );
        })}
      </ReactSortable>
    </>
  );
};

export default FieldSetLeft;
4.FieldSetCenter.js
import React from "react";

import { ReactSortable } from "react-sortablejs";

import { CloseCircleFilled } from "@ant-design/icons";
import Layout from "./Layout";

const FieldSetCenter = ({
  fieldList = [],
  currentField = {},
  onFieldChange = () => {},
  handleChooseField = () => {},
}) => {
  //删除添加的布局字段
  const handleDelete = (e, id) => {
    e.stopPropagation();
    const newFieldList = [...fieldList];
    const curIndex = newFieldList.findIndex((item) => item.id === id);
    if (curIndex !== -1) {
      let deleteItem = newFieldList.splice(curIndex, 1);
      if (deleteItem?.[0]?.id === currentField?.id) {
        handleChooseField({});
      }
      onFieldChange(newFieldList);
    }
  };

  return (
    <ReactSortable
      list={fieldList || []}
      animation={200}
      group={{ name: "sort-field" }}
      setList={(list) =>
        onFieldChange(
          list.map((item) => ({
            ...item,
            id: item?.id ? item?.id : new Date().getTime(),
          }))
        )
      }
      sort={true}
      forceFallback={true}
      className="field-center"
    >
      {!!fieldList?.length === 0 ? (
        <div className="main-tooltip">请选择左侧的组件</div>
      ) : (
        fieldList.map((item, index) => {
          return (
            <div
              className="main-field"
              style={{
                border:
                  currentField?.id === item?.id &&
                  "1px solid rgba(28,133,252,1)",
                background: currentField?.id === item?.id && "#F8FBFF",
              }}
              key={index}
              onClick={() => handleChooseField(item)}
            >
              <Layout field={item} />
              <CloseCircleFilled
                className="main-field-delete"
                style={{
                  display: currentField?.id === item?.id && "block",
                }}
                onClick={(e) => handleDelete(e, item?.id)}
              />
            </div>
          );
        })
      )}
    </ReactSortable>
  );
};

export default FieldSetCenter;
5.FieldSetRight.js
import React from "react";

import { isEmpty } from "lodash";

import { Input } from "antd";

const FieldSetRight = ({
  fieldList = [],
  currentField = {},
  onFieldChange = () => {},
  handleChooseField = () => {},
}) => {
  const { fieldLabel } = currentField;
  const handleChange = (e) => {
    const val = e.target.value;
    let newFieldList = [...fieldList];
    const curIndex = newFieldList.findIndex(
      (item) => item?.id === currentField?.id
    );
    if (curIndex !== -1) {
      newFieldList[curIndex]["fieldLabel"] = val;
      onFieldChange(newFieldList);
      handleChooseField(newFieldList[curIndex]);
    }
  };

  return (
    <>
      <div className="title">字段设置</div>
      {!isEmpty(currentField) && (
        <>
          <div className="content">
            <div className="label">字段名称:</div>
            <Input value={fieldLabel} onChange={handleChange} />
          </div>
        </>
      )}
    </>
  );
};

export default FieldSetRight;
6.Layout/index.js
import React, { useMemo } from "react";

import Input from "./Input"; //单行文本框
import TextArea from "./TextArea"; //多行文本框
import DatePicker from "./DatePicker"; //日期
import Radio from "./Radio"; //单选框
import Checkbox from "./Checkbox"; //多选框

const FIELD_MAP = {
  Input,
  TextArea,
  DatePicker,
  Radio,
  Checkbox,
};

const Base = ({ field }) => {
  const Component = useMemo(() => {
    return FIELD_MAP[field?.fieldKey];
  }, [field?.fieldKey]);

  return Component ? <Component field={field} /> : null;
};

export default Base;
7.Input.js
import React from "react";

import { Input } from "antd";

const InputField = ({ field }) => {
  const { fieldLabel } = field;
  return (
    <div className="field-item">
      <div className="field-label">{fieldLabel}:</div>
      <div className="field-form">
        <Input readOnly placeholder="请输入" />
      </div>
    </div>
  );
};

export default InputField;

以上代码最后的Layout文件里的组件我只写了一个Input,其余表单组件,可以自行编写。

4.实现效果

在这里插入图片描述

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值