react-dnd 拖拽

 

 Index.js:

import React from 'react'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { Form, Button, Collapse, Col, Row } from 'antd'
import Header from './Header'
import useList from './useList'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { Icon } from '../../../../components/light'
import { getComponentArr, getAttrFields } from './config'
import List from './List'
import BtnField from './BtnField'

const { Panel } = Collapse

function Index(props) {
  const {
    applicationTitle,
    dataSource,
    form,
    formForAttr,
    initValues,
    initValuesForAttr,
    tableId,
    cardActiveId,
    moveCard,
    handleFinish,
    handleFinishFailed,
    handleAdd,
    handleSave,
    handleCardActiveId,
    handleValuesChange,
    handleDelete,
  } = useList(props)

  return (
    <div className="m-admin-content">
      <Header
        applicationTitle={applicationTitle}
        tableId={tableId}
        onSave={handleSave}
      ></Header>
      <div className="m-design-wrap">
        <div className="m-design-sidebar">
          <Collapse defaultActiveKey={['1', '2', '3']}>
            <Panel header="通用字段" key="1">
              <Row gutter={[2, 2]}>
                <DndProvider backend={HTML5Backend}>
                  {getComponentArr().map((fieldInfo, index) => (
                    <BtnField key={index} fieldInfo={fieldInfo} onAdd={handleAdd} />
                  ))}
                </DndProvider>
              </Row>
            </Panel>
            <Panel header="联系信息字段" key="2">
              <Row gutter={[2, 2]}>
                <Col span={8}>
                  <div className="m-component-item">
                    <div></div>
                    <div>敬请期待</div>
                  </div>
                </Col>
              </Row>
            </Panel>
            <Panel header="商品字段" key="3">
              <Row gutter={[2, 2]}>
                <Col span={8}>
                  <div className="m-component-item">
                    <div></div>
                    <div>敬请期待</div>
                  </div>
                </Col>
              </Row>
            </Panel>
          </Collapse>
        </div>
        <div className="m-design-content">
          <Form
            form={form}
            labelCol={{ span: 4 }}
            wrapperCol={{ span: 17 }}
            initialValues={{ ...initValues }}
            onFinish={handleFinish}
            onFinishFailed={handleFinishFailed}
          >
            <DndProvider backend={HTML5Backend}>
              <List
                dataSource={dataSource}
                cardActiveId={cardActiveId}
                moveCard={moveCard}
                handleCardActiveId={handleCardActiveId}
                handleDelete={handleDelete}
              />
            </DndProvider>
            <Form.Item
              wrapperCol={{ offset: 4, span: 17 }}
              className="m-design-footer"
            >
              <Button type="primary" htmlType="submit" className="m-space">
                <Icon name="submit" className="m-tool-btn-icon"></Icon>
                提交
              </Button>
              <Button
                className="m-space"
                onClick={() => {
                  form.resetFields()
                }}
              >
                <Icon name="reset" className="m-tool-btn-icon"></Icon>
                重置
              </Button>
            </Form.Item>
          </Form>
        </div>
        <div className="m-design-attr">
          <Form
            form={formForAttr}
            labelCol={{ span: 8 }}
            wrapperCol={{ span: 15 }}
            initialValues={{ ...initValuesForAttr }}
            scrollToFirstError={true}
            onValuesChange={handleValuesChange}
            id="m-set-application-modal-form"
            className="m-set-application-modal-form"
          >
            {getAttrFields()}
          </Form>
        </div>
      </div>
    </div>
  )
}

const mapStateToProps = (state) => {
  return {}
}

const mapDispatchToProps = (dispatch) => {
  return {
    onSetState(key, value) {
      dispatch({ type: 'SET_LIGHT_STATE', key, value })
    },
    onDispatch(action) {
      dispatch(action)
    },
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Index))

BtnField.js:

import { useDrag } from 'react-dnd'
import { ItemTypes } from './ItemTypes'
import { Icon } from '../../../../components/light'
import { Col } from 'antd'

export default function BtnField({ fieldInfo, onAdd }) {
  const [{ isDragging }, drag] = useDrag(() => ({
    type: ItemTypes.BTN_FIELD,
    item: { ...fieldInfo },
    end: (item, monitor) => {
      const dropResult = monitor.getDropResult()
      if (item && dropResult) {
        console.log(`${item.title} 加入 ${dropResult.name}`)
        onAdd({fieldInfo})
      }
    },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
      handlerId: monitor.getHandlerId(),
    }),
  }))
  const opacity = isDragging ? 0.4 : 1
  return (
    <Col span={8}>
      <div
        className="m-component-item"
        ref={drag}
        style={{ opacity }}
        data-testid={`box-${fieldInfo.title}`}
        onClick={() => onAdd({fieldInfo})}
      >
        <div>
          <Icon name={fieldInfo.icon}></Icon>
        </div>
        <div>{fieldInfo.title}</div>
      </div>
    </Col>
  )
}

config.js:

import { Form, Input, Button } from 'antd'
import { FieldRequired } from '../../../../components/light'

//表格列字段
const getColumns = (props) => {
  return [
    {
      title: 'ID',
      dataIndex: 'id',
    },
    {
      title: '字段名称',
      dataIndex: 'title',
    },
    {
      title: '英文名称',
      dataIndex: 'dataIndex',
    },
    {
      title: '表单组件名',
      dataIndex: 'formComponentName',
      render: (text) => {
        return text ? text : '无'
      },
    },
    {
      title: '渲染函数名',
      dataIndex: 'renderFunName',
      render: (text) => {
        return text ? text : '无'
      },
    },
    {
      title: '字段必填',
      dataIndex: 'rules',
      render: (text) => {
        const result = Array.isArray(text) && text.length > 0 && text[0]
        return result ? (result.required ? '是' : '否') : '否'
      },
    },
    {
      title: '表格展示',
      dataIndex: 'isColumn',
      render: (text) => {
        return text ? '是' : '否'
      },
    },
    // {
    //   title: '搜索',
    //   dataIndex: 'isSearch',
    //   render: (text) => {
    //     return text ? '是' : '否'
    //   },
    // },
    {
      title: '添加/编辑',
      dataIndex: 'isModalField',
      render: (text) => {
        return text ? '是' : '否'
      },
    },
    {
      title: '顺序号',
      dataIndex: 'orderIndex',
      render: (text) => {
        return typeof text === 'number' ? text : '无'
      },
    },
    {
      title: '操作',
       220,
      render: (record) => {
        if (record.isSystem) {
          return '系统字段'
        } else {
          return (
            <div className="m-action">
              <Button
                className="m-action-btn"
                size="small"
                danger
                onClick={() => props.onDelete(record)}
              >
                删除
              </Button>
              <Button
                className="m-action-btn"
                size="small"
                onClick={() => props.onCheck(record)}
              >
                查看
              </Button>
              <Button
                className="m-action-btn"
                size="small"
                onClick={() => props.onEdit(record)}
              >
                编辑
              </Button>
            </div>
          )
        }
      },
    },
  ]
}

//组件元素
const getComponentArr = () => {
  return [
    {
      icon: 'input',
      title: '单行文本',
      formComponentName: "Input",
      dataIndex: 'input',
      renderFunName: "renderSpan"
    },
    {
      icon: 'textarea',
      title: '多行文本',
      formComponentName: "TextArea",
      dataIndex: 'textArea',
      renderFunName: "renderSpan"
    },
    {
      icon: 'number-input',
      title: '数字',
      formComponentName: "InputNumber",
      dataIndex: 'inputNumber ',
      renderFunName: "renderSpan"
    },
  ]
}

//添加编辑查看对话框表单字段
const getAttrFields = () => {
  return (
    <>
      <Form.Item
        label="字段名称"
        name="title"
        rules={[
          {
            required: true,
            message: '请输入字段名称!',
          },
        ]}
      >
        <Input />
      </Form.Item>
      <Form.Item
        label="英文名称"
        name="dataIndex"
        rules={[
          {
            required: true,
            message: '请输入字段名称!',
          },
        ]}
      >
        <Input />
      </Form.Item>
      <Form.Item label="字段必填" name="rules">
        <FieldRequired></FieldRequired>
      </Form.Item>
    </>
  )
}

export { getColumns, getComponentArr, getAttrFields }

Header.js:

import React from 'react'
import { Button } from 'antd'
import { withRouter, Link } from 'react-router-dom'
import { Icon } from '../../../../components/light'

function Header(props) {
  const { applicationTitle, tableId, onSave } = props
  return (
    <div className="m-design-header">
      <div className="m-design-header-title">
        <Icon
          name="goback"
          title="返回"
          className="m-set-application-header-icon"
          onClick={() => props.history.go(-1)}
        ></Icon>
        <span title={applicationTitle}>{applicationTitle}</span>
      </div>
      <div className="m-design-header-middle"></div>
      <div className="m-design-header-action">
        <Button type="primary" onClick={onSave}>
          保存
        </Button>
        <Link to={`/light/formview?id=${tableId}`} target="_blank" style={{display: 'inherit'}}>
          <Button>预览</Button>
        </Link>
      </div>
    </div>
  )
}

export default withRouter(Header)

ItemTypes.js:

export const ItemTypes = {
  LIST_ITEM: 'listItem',
  BTN_FIELD: 'btnField' //'btnField',
}

List.js:

import { useDrop } from 'react-dnd'
import { ItemTypes } from './ItemTypes'
import ListItem from './ListItem'

export default function List({
  dataSource,
  cardActiveId,
  moveCard,
  handleCardActiveId,
  handleDelete,
}) {
  const [{ canDrop, isOver }, drop] = useDrop(() => ({
    accept: ItemTypes.BTN_FIELD,
    drop: () => ({ name: '容器' }),
    collect: (monitor) => ({
      isOver: monitor.isOver(),
      canDrop: monitor.canDrop(),
    }),
  }))
  const isActive = canDrop && isOver
  return (
    <div
      ref={drop}
      className={`m-center-list-wrap ${isActive ? 'active' : ''}`}
    >
      {dataSource.map((card, index) => (
        <ListItem
          key={card.id}
          index={index}
          cardActiveId={cardActiveId}
          card={card}
          moveCard={moveCard}
          onCardActiveId={handleCardActiveId}
          onDelete={handleDelete}
        />
      ))}
    </div>
  )
}

ListItem.js:

import { useRef } from 'react'
import { useDrag, useDrop } from 'react-dnd'
import { ItemTypes } from './ItemTypes'
import { Form, Input, Button } from 'antd'
import { getFormComponentArr } from '../../../../utils/tools'

export default function ListItem({
  index,
  cardActiveId,
  card,
  moveCard,
  onCardActiveId,
  onDelete,
}) {
  const ref = useRef(null)
  const [{ handlerId }, drop] = useDrop({
    accept: ItemTypes.LIST_ITEM,
    collect(monitor) {
      return {
        handlerId: monitor.getHandlerId(),
      }
    },
    hover(item, monitor) {
      if (!ref.current) {
        return
      }
      const dragIndex = item.index
      const hoverIndex = index
      // Don't replace items with themselves
      if (dragIndex === hoverIndex) {
        return
      }
      // Determine rectangle on screen
      const hoverBoundingRect = ref.current?.getBoundingClientRect()
      // Get vertical middle
      const hoverMiddleY =
        (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
      // Determine mouse position
      const clientOffset = monitor.getClientOffset()
      // Get pixels to the top
      const hoverClientY = clientOffset.y - hoverBoundingRect.top
      // Only perform the move when the mouse has crossed half of the items height
      // When dragging downwards, only move when the cursor is below 50%
      // When dragging upwards, only move when the cursor is above 50%
      // Dragging downwards
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return
      }
      // Dragging upwards
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return
      }
      // Time to actually perform the action
      moveCard(dragIndex, hoverIndex)
      // Note: we're mutating the monitor item here!
      // Generally it's better to avoid mutations,
      // but it's good here for the sake of performance
      // to avoid expensive index searches.
      item.index = hoverIndex

      console.log(hoverIndex)
    },
  })
  const [{ isDragging }, drag] = useDrag({
    type: ItemTypes.LIST_ITEM,
    item: () => {
      return { id: card.id, index }
    },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  })
  const opacity = isDragging ? 0 : 1
  drag(drop(ref))

  //console.log(card)
  const renderDom = () => {
    if (card.isModalField) {
      const result = getFormComponentArr().find(
        (componentItem) =>
          componentItem.formComponentName === card.formComponentName
      )
      return (
        <div
          ref={ref}
          style={{ opacity }}
          data-handler-id={handlerId}
          className={`m-design-card ${
            cardActiveId === card.id ? 'active' : ''
          }`}
          onClick={() => onCardActiveId({ id: card.id })}
        >
          <div className="m-design-card-info">
            <Form.Item
              key={card.id}
              label={card.title}
              name={card.dataIndex}
              rules={card.rules}
            >
              {result ? result.component : <Input></Input>}
            </Form.Item>
          </div>
          <div className="m-design-card-action">
            <Button
              className="m-action-btn"
              size="small"
              danger
              onClick={() => onDelete(card)}
            >
              删除
            </Button>
          </div>
        </div>
      )
    } else {
      return null
    }
  }

  return <>{renderDom()}</>
}

useList.js:

import { useState, useEffect, useCallback } from 'react'
import Api from '../../../../api'
import { Modal, Form, message } from 'antd'
import update from 'immutability-helper'
import { getRouterSearchObj } from '../../../../utils/tools'
import { v4 as uuidv4 } from 'uuid'

const { confirm } = Modal

let currentDataSource = []
export default function useList(props) {
  const [form] = Form.useForm()
  const [formForAttr] = Form.useForm()
  const [dataSource, setDataSource] = useState([])
  const [applicationTitle, setApplicationTitle] = useState()
  const [cardActiveId, setCardActiveId] = useState()
  const [initValuesForAttr, setInitValuesForAttr] = useState({})

  //获取路由参数
  const routerSearchObj = getRouterSearchObj(props)
  const tableId = routerSearchObj.id - 0

  const addInitValues = {}

  //搜索
  const handleSearch = () => {
    Api.light.fieldsSearch({ tableId }).then((res) => {
      if (res.code === 200) {
        let tempDataSource = res.data.fields.filter((item) => !item.isSystem)
        setDataSource(tempDataSource)
        setApplicationTitle(res.data.title)
        if (Array.isArray(tempDataSource) && tempDataSource.length > 0) {
          handleCardActiveId({
            id: tempDataSource[0].id,
            myDataSource: tempDataSource,
          })
        }
      }
    })
  }

  //拖动改变顺序
  const moveCard = useCallback(
    (dragIndex, hoverIndex) => {
      const dragCard = dataSource[dragIndex]
      setDataSource(
        update(dataSource, {
          $splice: [
            [dragIndex, 1],
            [hoverIndex, 0, dragCard],
          ],
        })
      )
    },
    [dataSource]
  )

  //添加新字段
  const handleAdd = ({ fieldInfo }) => {
    const orderIndexArr = currentDataSource.map((item) => item.orderIndex)
    const orderIndex = Math.max.apply(Math, orderIndexArr) + 1
    const id = uuidv4()
    let tempValues = {
      id,
      dataIndex: `${fieldInfo.dataIndex}-${id}`,
      isColumn: true,
      isModalField: true,
      orderIndex,
    }
    console.log({ ...fieldInfo, ...tempValues })
    console.log(currentDataSource)
    setDataSource([...currentDataSource, { ...fieldInfo, ...tempValues }])
  }

  //保存
  const handleSave = () => {
    console.log(dataSource)
    const newDataSource = dataSource.map((item, index) => {
      return { ...item, orderIndex: index + 1 }
    })
    console.log(newDataSource)
    Api.light
      .fieldsEditAll({ tableId, dataItem: newDataSource })
      .then((res) => {
        if (res.code === 200) {
          message.success(res.message)
        }
      })
  }

  //删除
  const handleDelete = (record) => {
    console.log('删除, id:', record.id)
    confirm({
      title: '确认要删除吗?',
      onOk() {
        const newDataSource = dataSource.filter(item => item.id !== record.id)
        setDataSource(newDataSource)
      },
    })
  }

  //添加或编辑
  const handleFinish = (values) => {
    console.log('Success:', values)
  }

  //校验失败
  const handleFinishFailed = (errorInfo) => {
    console.log('Failed:', errorInfo)
  }

  //设置当前card
  const handleCardActiveId = ({ id, myDataSource = dataSource }) => {
    setCardActiveId(id)
    let currentItem = myDataSource.find((item) => item.id === id)
    const rules =
      Array.isArray(currentItem.rules) && currentItem.rules.length > 0
        ? currentItem.rules[0]
        : {}
    setInitValuesForAttr({ ...currentItem, rules })
  }

  //修改表单字段属性
  const handleValuesChange = (changedValues, allValues) => {
    const cardActiveIndex = dataSource.findIndex(
      (item) => item.id === cardActiveId
    )

    let tempValues = {
      rules: [allValues.rules],
    }

    dataSource[cardActiveIndex] = {
      ...dataSource[cardActiveIndex],
      ...allValues,
      ...tempValues,
    }
    setDataSource([...dataSource])
  }

  useEffect(() => {
    formForAttr.resetFields()
    // eslint-disable-next-line
  }, [initValuesForAttr])

  //挂载完
  useEffect(() => {
    handleSearch()
    // eslint-disable-next-line
  }, [])

  //dataSource更新,同步更新currentDataSource,handleAdd函数中dataSource的值为空数组,这是一个bug
  useEffect(() => {
    currentDataSource = dataSource
  }, [dataSource])

  return {
    form,
    formForAttr,
    initValuesForAttr,
    dataSource,
    applicationTitle,
    addInitValues,
    tableId,
    cardActiveId,
    handleSearch,
    moveCard,
    handleDelete,
    handleFinish,
    handleFinishFailed,
    handleAdd,
    handleSave,
    handleCardActiveId,
    handleValuesChange,
  }
}

原文地址:https://www.cnblogs.com/xutongbao/p/15264311.html