Vue2 implements draggable Gantt chart (combined with element-ui's gantt chart)

I. Introduction

  After receiving the company's request, I want to make a draggable Gantt chart to meet the scheduling requirements. The official plug-in has to be paid and there are no official documents in Chinese to read, so I looked for various open source demos. The functions are all It is not very complete, so I summarized a lot of demos and combined them together to form a relatively complete version of the Gantt chart that meets the needs.

2. Main functions

1. Drag and drop The drag and drop function is the main function of the Gantt chart. This demo implements the up, down, left, and right drag functions of the Gantt chart time block.

2. Sorting Time blocks are sorted after dragging, and the size of the overlapping area is calculated to determine the insertion position.

3. Time selection combines element-ui's date and time selector to determine the time axis.

4. Search Search the existing time block and locate the corresponding position.

5. Create a new scheduled task Use the pop-up box of element-ui and the form to add the successfully created scheduled list to the scheduled task.

6. Right-click the time block in the right-click menu, and you can perform operations such as viewing, deleting, and modifying.

7. Undo After each time block is deleted or moved, an operation record is added, and the current operation can be withdrawn by clicking Undo.

8. Batch operation Click Save after batch operation to store data to the backend.

3. Function realization

1. The default timeline is the first three and the last four of today

	// 设置默认时间 当前日期前三后四
		defaultDate() {
			const beg = new Date(new Date().getTime() - 3600 * 1000 * 24 * 3)
				.toISOString()
				.replace('T', ' ')
				.split('.')[0] //默认开始时间3天前
			const end = new Date(new Date().getTime() + 3600 * 1000 * 24 * 4)
				.toISOString()
				.replace('T', ' ')
				.split('.')[0]//默认结束时间4天后
			this.choiceTime = [beg, end] //将值设置给插件绑定的数据
			// console.log(this.value1);
		},

 2. Drag and drop event implementation

onMouseDown(e, blockId, rowIndex) {
			// 删除模式下不处理拖动事件
			if (this.isDeleteMode) {
				return;
			}
			this.moveX = 0;
			this.moveY = 0;

			// 用box 移动,不采用 Doucment
			const box = this.$refs.box;
			const dom = e.target;
			// 算出鼠标相对元素的位置
			const disX = e.clientX - dom.offsetLeft;
			const disY = e.clientY - dom.offsetTop;
			console.log('鼠标正在拖动',e.clientX,dom.offsetLeft);

			// 当点击下来的时候 nowSuck 其实等于的就是当前index
			this.nowSuck = rowIndex;

			// 让本来拥有手掌样式的class取消
			dom.classList.remove('gantt-grab');
			// 让整个box 鼠标都是抓住
			box.classList.add('gantt-grabbing');
			// 如果事件绑定在 dom上,那么一旦鼠标移动过快就会脱离掌控
			box.onmousemove = ee => {
				// 获得当前受到拖拽的div 用于计算suck 所谓拖引的行数
				const top = parseInt(dom.style.top);
				// 四舍五入 获得磁性吸附激活的值 (索引值)  60是block的height 10是时间块距离block的top  suck 可以当作row的索引
				let suck = Math.round((top - 10) / 60) + rowIndex;

				// suck的边界值设置
				if (suck < 0) {
					suck = 0;
				} else if (suck > this.ganttData.length - 1) {
					suck = this.ganttData.length - 1;
				}

				// suck 行样式变化
				this.nowSuck = suck;

				// 移动事件
				this.onMouseMove(ee, disX, disY, dom);
				// dom.style.left=this.moveX;
			};
			// 不管在哪里,鼠标松开的时候,清空事件 所以对于这个 “不管在哪里,使用了window”
			window.onmouseup = () => {
				// 鼠标松开了,让时间块恢复手掌样式
				dom.classList.add('gantt-grab');
				// 整个box 不在抓住了,变成箭头鼠标
				box.classList.remove('gantt-grabbing');
				// 当移动距离小于5时,不做移动处理
				//console.log('移动距离:', this.moveX, this.moveY);
				if (this.moveX < 1 && this.moveY < 1 && this.moveX > -1 && this.moveY > -1) {
					console.log('无效移动');
					box.onmousemove = null;
					window.onmouseup = null;
					this.nowSuck = -1;
					return;
				}
				// 保存操作日志
				this._addHisList(this.ganttData);

				const index = this.ganttData[rowIndex][this.listKey].findIndex(item => {
					return item.id === blockId;
				});
				const oldTimeBlock = this.ganttData[rowIndex][this.listKey][index];
				// let timeId = oldTimeBlock.id;

				// startTime 与 endTime 用于数据重新替换  上面css已经经过计算 15px为1小时
				const startTime = new Date(Date.parse(this.choiceTime[0]) + (parseInt(dom.style.left) * 3600000) / 15);
				const endTime = new Date(this._getTime(startTime) + (parseInt(dom.style.width) * 3600000) / 15);
				// console.log(startTime, endTime, dom.style.width, parseInt(dom.style.left) * 60 * 1000);
				const suck = this.nowSuck;

				// 加入新数据
				const data = oldTimeBlock;
				// 更新开始时间和结束时间
				this.$set(data, 'startTime', startTime);
				this.$set(data, 'endTime', endTime);
				// 修改时间块的equipmentId
				this.$set(data, 'equipmentId', this.ganttData[suck].id);
				/**
				 * 本来dom元素磁性吸附是打算用document.appendChild() 方式来做的,但是对于vue来说 for出来的子元素就算变了位置,其index也不属于新的row
				 */
				// 老数据 删除
				this.ganttData[rowIndex][this.listKey].splice(index, 1);

				// 新数据加入
				this.ganttData[suck][this.listKey].push(data);

				// 坐标定位 磁性吸附 永远的10px   不知道为啥动态绑定的元素也会受到以前元素的影响,可能是 for 中 index带来的影响
				dom.style.top = this.defaultTop + 'px';

				// 松开鼠标的时候 清空
				box.onmousemove = null;
				window.onmouseup = null;

				// 需要重新计算吸附位置,以及整行重新排列
				this.$nextTick(() => {
					this._recalcRowTimes(data, this.ganttData[suck][this.listKey]);
				});

				// 将当前row 清空
				this.nowSuck = -1;

				// 改变位置后回调事件
				this.afterChangePosition(data, this.ganttData[rowIndex].id, this.ganttData[suck].id);
			};
		},
	/**
		 * 鼠标移动的时候,前置条件鼠标按下
		 * @param e 时间块的 event 事件
		 * @param disX x轴
		 * @param disY y轴
		 * @param targetDom 时间块的dom 其实可以直接 e.target 获取
		 */
		onMouseMove(e, disX, disY, targetDom) {
			// 用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
			let left = e.clientX - disX;
			const top = e.clientY - disY;
			console.log('x轴移动距离',left);
			this.moveX = left;
			this.moveY = top;

			// 移动位置不能小于零(开始时间)
			if (left < 0) {
				left = 0;
			}

			//拖动时间块关闭右键菜单
			this.menuVisible = false;

			targetDom.style.left = left + 'px';
			targetDom.style.top = top + 'px';
		},
		/**
		 * 时间块位置变化后回调事件
		 * @param data 时间块的值 包括时间块id startTime endTime
		 * @param rowOId 时间块原来所在的那个飞机id (一条横线)
		 * @param rowNId 时间块新的所在的那个飞机id
		 */
		afterChangePosition(data, rowOId, rowNId) {
			console.log('时间块位置变化后回调事件', rowOId, rowNId);
		},

		save() {
			console.log(JSON.stringify(this.ganttData));
		},

3. Right-click to set a custom right-click menu

		onRightClick(MouseEvent, row, block) {
			//编辑需要把时间长度先计算好
			MouseEvent.preventDefault(); //关闭浏览器右键默认事件
			block.timeDiff = (block.endTime - block.startTime) / 3600000;
			this.editRow = row;
			this.editBlock = block;

			// this.menuVisible = false; // 先把模态框关死,目的是 第二次或者第n次右键鼠标的时候 它默认的是true
			this.menuVisible = true; // 显示模态窗口,跳出自定义菜单栏
			console.log('唤醒点击事件', this.menuVisible, this.editBlock, MouseEvent.clientX);
			this.CurrentRow = row;
			var menu = document.querySelector('.menu');
			if (MouseEvent.clientX > 1800) {
				menu.style.left = MouseEvent.clientX - 100 + 'px';
			} else {
				menu.style.left = MouseEvent.clientX + 1 + 'px';
			}
			document.addEventListener('click', this.cancelMouse); // 给整个document新增监听鼠标事件,点击任何位置执行foo方法
			if (MouseEvent.clientY > 700) {
				menu.style.top = MouseEvent.clientY - 30 + 'px';
			} else {
				menu.style.top = MouseEvent.clientY - 10 + 'px';
			}
			console.log('位置問題', MouseEvent.clientY - 30 + 'px', MouseEvent.clientY - 10 + 'px');
			// this.styleMenu(menu);
		},

		cancelMouse() {
			// document.oncontextmenu=false;
			// 取消鼠标监听事件 菜单栏
			this.menuVisible = false;
			document.removeEventListener('click', this.foo); // 关掉监听,
		},

4. Calculate the difference in days of the time picker to render the timeline

choiceTimeArr() {
			const timeArr = [];
			// 时间戳毫秒为单位
			// 尾时间-首时间 算出头尾的时间戳差  再换算成天单位                                   毫秒->分->时->天
			// const diffDays = (this.choiceTime[1].getTime() - this.choiceTime[0].getTime()) / 1000 / 60 / 60 / 24;
			const diffDays = Math.abs(Date.parse(this.choiceTime[1])- Date.parse(this.choiceTime[0])) / 1000 / 60 / 60 / 24
			console.log('我是时间差啊', diffDays);
			// 一天的时间戳)
			const oneDayMs = 24 * 60 * 60 * 1000;
			// 差了多少天就便利多少天 首时间+当前便利的天数的毫秒数
			for (let i = 0; i < diffDays + 1; i++) {
				// 时间戳来一个相加,得到的就是时间数组
				timeArr.push(new Date(Date.parse(this.choiceTime[0]) + i * oneDayMs));
			}

			// console.log(diffDays, oneDayMs, timeArr);
			return timeArr;
		},

 5. Search function (example using element-ui)

		// 搜索数据数组
		loadAll() {
			return [
				{ "value": "三全鲜食(北新泾店)", "address": "长宁区新渔路144号" },
				{ "value": "Hot honey 首尔炸鸡(仙霞路)", "address": "上海市长宁区淞虹路661号" },
				{ "value": "新旺角茶餐厅", "address": "上海市普陀区真北路988号创邑金沙谷6号楼113" },
				{ "value": "泷千家(天山西路店)", "address": "天山西路438号" },
				{ "value": "胖仙女纸杯蛋糕(上海凌空店)", "address": "上海市长宁区金钟路968号1幢18号楼一层商铺18-101" },
				{ "value": "贡茶", "address": "上海市长宁区金钟路633号" },
				{ "value": "豪大大香鸡排超级奶爸", "address": "上海市嘉定区曹安公路曹安路1685号" },
				{ "value": "茶芝兰(奶茶,手抓饼)", "address": "上海市普陀区同普路1435号" },
				{ "value": "十二泷町", "address": "上海市北翟路1444弄81号B幢-107" },
				{ "value": "星移浓缩咖啡", "address": "上海市嘉定区新郁路817号" },
				{ "value": "阿姨奶茶/豪大大", "address": "嘉定区曹安路1611号" },
				{ "value": "新麦甜四季甜品炸鸡", "address": "嘉定区曹安公路2383弄55号" }
			];
		},
		querySearchAsync(queryString, cb) {
			var restaurants = this.restaurants;
			var results = queryString ? restaurants.filter(this.createStateFilter(queryString)) : restaurants;

			clearTimeout(this.timeout);
			this.timeout = setTimeout(() => {
				cb(results);
			}, 3000 * Math.random());
		},
		createStateFilter(queryString) {
			return (state) => {
				return (state.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
			};
		},
		handleSelect(item) {
			console.log(item);
		},

 4. Realize the effect 

      Since the requirement is realized in the form of a pop-up frame, there is no full-screen display. The specific effect is as follows:

Gantt chart implementation

 

         

Guess you like

Origin blog.csdn.net/qq_45870314/article/details/127730619