--- title: 'react-dnd 拖拽' date: 2021-08-23 13:57:00 --- ![](https://img-blog.csdnimg.cn/20210823135611101.gif) ![](https://img-blog.csdnimg.cn/20210823135047242.png) Index.js: ```javascript 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: ```javascript 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: ```javascript 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: '操作', width: 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: ```javascript 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: ```javascript export const ItemTypes = { LIST_ITEM: 'listItem', BTN_FIELD: 'btnField' //'btnField', } ``` List.js: ```javascript 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: ```javascript 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: ```javascript 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, } } ```