拖拽实现基础可见:https://blog.csdn.net/qq_40761136/article/details/125021437
先上效果图和需求说明:
1、初始效果图
开始节点和结束节点为固定的。中间的环节由后台返回
中间的环节可拖拽修改顺序,开始和结束节点不允许拖拽
——左键键选中环节不放,拖拽到步骤图上其他环节之间的【+】处放开,即可改变环节在流程图上的顺序
——拖拽到当前环节前后【+】上,则拖拽不生效
2、单击某个环节后,选中环节样式如下
鼠标移入环节处,会展示允许编辑和删除的按钮
开始和结束节点不会出现该按钮提示
3、拖拽某个环节
4、拖拽后的效果。环节的顺序已发生了变化
代码如下:
/* eslint-disable eqeqeq */
import React from 'react';
import { Icon, Dropdown, Menu, Modal } from '@whalecloud/fdx';
import styles from './index.less';
const fixedStepData = [
{ tacheId: '开始', tacheName: '开始' },
{ tacheId: '结束', tacheName: '结束' },
];
const { confirm } = Modal;
class TacheStepConfig extends React.PureComponent {
state = {
tacheList: [], // 环节列表
stepCurrent: 0, // 当前选中的环节index
selectTache: {}, // 当前选中的环节数据
type: '', // add新增 edit编辑
step: '1', // 新增、编辑时选中的顺序
stepData: {}, // 编辑时选中的环节数据
};
srcTache = null; // 拖拽开始时选中的环节
componentDidMount() {
// 假装这里后台返回了数据。。
this.setState({
tacheList: [
{ tacheId: '环节1', tacheName: '环节1', test: '1' },
{ tacheId: '环节222', tacheName: '环节222', test: '2' },
{ tacheId: '环节3', tacheName: '环节3', test: '2' },
{ tacheId: '环节4', tacheName: '他的文字很多啊超多啊', test: '2' },
],
});
// 默认开始节点为初始选中的环节
this.onChangeStep(this.state.stepCurrent);
}
// 单击环节
onChangeStep = step => {
// 确保点结束时step数据会展示结束节点数据
this.setState({
stepCurrent: step,
selectTache: {
stepCurrent: step,
...this.finalStepData[step],
},
});
};
// 点击加号新增环节;index数字是几就是当前加号前有几个环节
addTache = index => {
this.setState({ step: index + 1, type: 'add' });
// 点新增之后的事件由自己添加
}
// 删除环节
deleteTache = item => {
confirm({
title: '提示',
content: '确认删除该环节?',
onOk() {
// 删除的请求
console.log('item', item);
},
onCancel() {},
});
}
// 当拖动时触发
drag = (ev, tache) => {
this.srcTache = tache;
};
allowDrop = ev => {
ev.preventDefault();
};
// 当放下后触发;newTache:拖拽后+加号前方的环节;newIndex拖拽后前方环节的数量
drop = (ev, newTache, newIndex) => {
ev.preventDefault();
// 拖拽至环节前一个加号,不处理
if (this.finalStepData[newIndex + 1]?.tacheId === this.srcTache?.tacheId) {
return;
}
// 拖拽至环节后一个加号,不处理
if (this.finalStepData[newIndex]?.tacheId === this.srcTache?.tacheId) {
return;
}
const newTacheList = [];
if (newIndex == 0) {
newTacheList.push(this.srcTache);
}
// 拖拽后,先改变环节顺序数组
this.finalStepData.forEach((item, index) => {
if (item.tacheId !== '开始' && item.tacheId !== '结束') {
if (newIndex == index) {
newTacheList.push(item);
newTacheList.push(this.srcTache);
} else if (item.tacheId != this.srcTache?.tacheId) {
newTacheList.push(item);
}
}
});
// 把当前选中环节设为拖拽的元素
this.setState({ tacheList: newTacheList });
this.onChangeStep(0);
// 此处直接改了state,如需请求后台,请自行添加代码
};
render() {
const { stepCurrent, tacheList } = this.state;
const finalStepData = [fixedStepData[0], ...tacheList, fixedStepData[1]];
this.finalStepData = finalStepData;
const menu = (item, index) => (
<Menu>
<Menu.Item>
<a onClick={() => this.setState({ step: index, stepData: item, type: 'edit' })}><Icon type="edit" style={{ marginRight: '10px' }} />编辑</a>
</Menu.Item>
<Menu.Item>
<a onClick={() => this.deleteTache(item)}><Icon type="delete" style={{ marginRight: '10px' }} />删除</a>
</Menu.Item>
</Menu>
);
// 获取图标
const getIcon = index => {
if (index === 0) return 'play-circle';
if (index >= finalStepData.length - 1) return 'minus-circle';
return 'check-circle';
};
// 获取图标颜色
const getImgBgClass = index => {
if (index === 0 || index >= finalStepData.length - 1) return styles.processImg;
if (index === stepCurrent) return styles.processImgCurrent; // 当前选中环节,橙色
if (index < stepCurrent) return styles.processImgDone; // 当前选中环节前的环节,绿色
return styles.processImgNoDone;
};
const getCustomDot = index => (
<div className={getImgBgClass(index)}>
<span>
<Icon type={getIcon(index)} theme="filled" />
</span>
</div>
);
return (
<div className={styles.Nav}>
{
finalStepData && finalStepData.map((item, index) => (
<div style={{ display: 'flex', position: 'relative', height: '53px' }}>
<Dropdown overlay={(index !== 0 && index < finalStepData.length - 1) ? menu(item, index) : <div />}>
<div
className={styles.NavContent}
onClick={() => this.onChangeStep(index)}
draggable={index !== 0 && index < finalStepData.length - 1} // 判断:不是开始节点和结束节点才允许拖拽
onDragStart={event => this.drag(event, item)} // 拖拽开始触发
>
{getCustomDot(index)}
<div style={{ marginTop: 7 }}>{item.tacheName}</div>
</div>
</Dropdown>
{/* 箭头 */}
{
index < finalStepData.length - 1 ?
<div className={stepCurrent > index ? styles.NavArrowActive : styles.NavArrow} />
: null
}
{/* 箭头中的加号 */}
{
index < finalStepData.length - 1 ?
<div
className={stepCurrent > index ? styles.NavAddActive : styles.NavAdd}
onClick={() => this.addTache(index)}
onDrop={event => this.drop(event, item, index)} // 环节拖拽至此处后触发
onDragOver={event => this.allowDrop(event)}
>
<Icon type="plus-circle" theme="filled" />
</div>
: null
}
</div>
))
}
</div>
);
}
}
export default TacheStepConfig;
样式代码如下:
@imgWidth: 26px;
@imgPadding: 2px;
@imgDivWidth: @imgWidth + @imgPadding * 2 + 1px;
@imgShadowWidth: 8px;
@imgHoverShadowWidth: 10px;
@arrowWidth: 100px;
@arrowHeight: 2px;
@arrowSize: 4px;
@normalColor: #5A91FF;
@doneColor: #1ABE61;
@noDoneColor: #c9c9c9;
@activeColor: #FF7F15;
.Nav {
width: 100%;
padding: 12px 20px;
// background-image: url("../../../../../../img/bgImg.png");
overflow-x: auto;
height: 85px;
display: flex;
white-space: nowrap;
.processImg {
border-radius: 50%;
border: 1px solid #dedede;
background: #fff;
box-shadow: 0 2px @imgShadowWidth @normalColor;
width: @imgDivWidth;
height: @imgDivWidth;
padding: @imgPadding;
transition: box-shadow 0.3s;
margin: 0 auto;
&:hover {
box-shadow: 0 0 @imgShadowWidth @normalColor;
}
> span {
display: flex;
font-size: 12px;
width: 100%;
height: 100%;
border: none;
margin: 0;
border-radius: 50%;
background: @normalColor;
color: #fff;
align-items: center;
justify-content: center;
}
}
.processImgCurrent {
.processImg;
box-shadow: 0 2px @imgShadowWidth @activeColor;
&:hover {
box-shadow: 0 0 @imgShadowWidth @activeColor;
}
> span {
background: @activeColor;
font-size: 14px;
}
}
.processImgDone {
.processImg;
box-shadow: 0 2px @imgShadowWidth @doneColor;
&:hover {
box-shadow: 0 0 @imgShadowWidth @doneColor;
}
> span {
background: @doneColor;
font-size: 14px;
}
}
.processImgNoDone {
.processImg;
box-shadow: 0 2px @imgShadowWidth @noDoneColor;
&:hover {
box-shadow: 0 0 @imgShadowWidth @noDoneColor;
}
> span {
background: @noDoneColor;
font-size: 14px;
}
}
.NavContent {
text-align: center;
cursor: pointer;
&:hover {
color: @doneColor;
}
}
.NavArrow {
height: 2px;
width: 64px;
background: @noDoneColor;
align-self: center;
margin: 0px 15px 26px 15px;
position: relative;
&::before {
content: '';
width: 5px;
height: 4px;
border-width: 4px;
border-color: transparent transparent transparent @noDoneColor;
border-style: solid;
position: absolute;
top: -3px;
right: -7px;
transition-delay: 0.3s;
transition: border-left-color 0.2s;
}
}
.NavArrowActive {
.NavArrow;
background: #9ee7be;
&::before {
border-color: transparent transparent transparent #9ee7be;
}
}
.NavAdd {
color: #8693A7;
margin-bottom: 26px;
position: absolute;
right: 39px;
top: 2px;
cursor: pointer;
font-size: 16px;
}
.NavAddActive {
.NavAdd;
color: @doneColor;
}
}
最后就完美实现效果啦~
不过个人认为有个缺陷是,使用这样的H5拖拽写法,虽然简单。就是在当前页面中,只允许存在一个拖拽事件。如果有多个类似的拖拽事件,则一个事件的物品拖拽至另一个拖拽事件的物品中时,不会有效果(甚至可能看放入事件出现问题)