vue2实现可拖拽甘特图(结合element-ui的gantt图)

news2024/11/18 3:48:23

一、前言

  接到公司需求,要做一个可拖拽的甘特图来实现排期需求,官方的插件要付费还没有中文的官方文档可以看,就去找了各种开源的demo来看,功能上都不是很齐全,于是总结了很多demo,合在一起组成了一版较为完整的满足需求的甘特图。

二、主要功能

1.拖拽  拖拽功能是甘特图的主要功能,该demo实现了甘特图时间块上、下、左、右拖拽功能。

2.排序 拖拽后时间块进行排序,计算重叠区域大小确定插入位置。

3.时间选择 结合element-ui的日期时间选择器来确定时间轴。

4.搜索  搜索已存在的时间块,并定位到相应位置。

5.新建排期任务  使用element-ui的弹框以及表单 新建成功的排期列表添加到排期任务中。

6.右键菜单 右击时间块,可以进行查看、删除、修改等操作。

7.撤回 每删除或移动时间块后,增加一条操作记录,点击撤回可撤回当前操作。

8.批量操作 在批量操作后点击保存,才向后端存储数据。

三、功能实现

1.默认时间轴线今天的前三后四

	// 设置默认时间 当前日期前三后四
		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.拖拽事件实现

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. 右击设置自定义右键菜单

		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.计算时间选择器相差天数以渲染时间轴

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.搜索功能(使用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);
		},

 四、实现效果 

      由于需求是以弹框形式实现,没有做全屏显示,具体效果如下:

甘特图实现

 

         

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/409639.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【前端工程化】配置package.json中scripts命令脚本,新手必学

每日鸡汤&#xff1a;你总要努力追上那个曾经被赋予众望的自己吧 目录 前言 一、运行npm run 命令之后会干啥&#xff1f; 1. scripts里面写啥 2. node_modules/.bin 二进制可执行文件 二、运行插件配置 1. 运行某个npm包的命令 2. 多个命令一起运行 总结 前言 配置pac…

JavaScript工具库——Lodash.js介绍安装及使用

前言&#xff1a; 本文主要介绍-JavaScript工具库——Lodash.js介绍安装及使用&#xff01; 作为初入职场、或者对 JavaScript 很多原理掌握的还不是那么透彻的时候&#xff0c;Lodash 这个工具库绝对是一把“杀手锏”&#xff0c;让我们一起来看看这把 “杀手锏” 的厉害之处。…

【前端八股文】浏览器系列:性能优化——HTML、CSS、JS、渲染优化

文章目录HTMLCSSCSS加载会造成阻塞吗JavaScript渲染优化参考本系列目录&#xff1a;【前端八股文】目录总结 是以《代码随想录》八股文为主的笔记。详情参考在文末。 代码随想录的博客_CSDN博客-leecode题解,ACM题目讲解,代码随想录领域博主 性能优化&#xff0c;从以下几个方…

学前端没这些工具怎么行

目录 前端工具 1.编译软件 2.网上编辑软件 3.参考文档 4.扩展 5.学习软件 6.优秀的教育机构 7.转化工具 8.记录笔记程序 ☀️作者简介&#xff1a;大家好我是言不及行yyds &#x1f40b;个人主页&#xff1a;言不及行yyds的CSDN博客 &#x1f381;系列专栏&#xff1a;…

前端vue3项目中百度地图的使用api及实例

目录 一、使用百度地图的准备工作&#xff1f; 二、百度地图的简单Demo 三、百度地图的常用api有哪些&#xff1f; 1、百度地图的类型&#xff1f; 2、百度地图控件 一、使用百度地图的准备工作&#xff1f; 1、先注册百度账号 --> 申请成为百度开发者 --> 获取服务…

React--》状态管理工具—Mobx的讲解与使用

目录 Mobx的讲解与使用 Mobx环境配置 Mobx的基本使用 Mobx计算属性的使用 Mobx监听属性的使用 Mobx处理异步的使用 Mobx的模块化 Mobx的讲解与使用 Mobx是一个可以和React良好配合的集中状态管理工具&#xff0c;mobx和react的关系相当于vuex和vue之间的关系&#xff0…

IntelliJ IDEA【前端必备插件】

有志者事竟成&#xff0c;破釜沉舟百二秦关终属楚苦心人天不负&#xff0c;卧薪尝胆三千越甲可吞吴。 &#x1f4cc;博主介绍 &#x1f492;首页:水香木鱼 &#x1f6eb;专栏&#xff1a;工欲善其事必先利其器 ✍简介: 博主姓&#xff1a;陈&#xff0c;名&#xff1a;春波。…

前端开发神器VS Code安装教程

✅作者简介&#xff1a;CSDN一位小博主&#xff0c;正在学习前端 &#x1f4c3;个人主页&#xff1a;白月光777的CSDN博客 &#x1f4ac;个人格言&#xff1a;但行好事&#xff0c;莫问前程 安装VS CodeVS Code简介VS Code安装VS Code汉化结束语&#x1f4a1;&#x1f4a1;&…

webpack热更新原理-连阿珍都看懂了

前言 在旧开发的时代&#xff0c;前端项目在开发的过程中修改代码&#xff0c;很有可能是手动切到浏览器刷新页面来看到改动效果。操作不方便且页面之前的编辑记录也都丢失&#xff0c;体验可以说为0。想象一下一个表达你努力填满了所有输入项&#xff0c;结果因为调了一下样式…

vue3.0全新文档快速上手学习内容整理

目录 ## 1.Vue3简介 ## 2.Vue3带来了什么 ### 1.性能的提升 ### 2.源码的升级 ### 4.新的特性 1. Composition API&#xff08;组合API&#xff09; 2. 新的内置组件 3. 其他改变 # 一、创建Vue3.0工程 ## 1.使用 vue-cli 创建 ## 2.使用 vite 创建 # 二、常用 Com…

vue3导出表格excel(支持多sheet页),并自定义导出样式

前期准备 npm install file-saver npm install xlsx npm install xlsx-js-style 先说一说这里为什么选择用xlsx-js-style插件设置导出excel的样式。因项目需要&#xff0c;我在网上找了很多关于导出excel自定义样式的文章&#xff0c;用的最多最普遍的插件就是xlsx-style&#…

unipush2.0教程

解释一下名词 透传消息&#xff1a;无论手机app&#xff0c;是否在运行(打开了)&#xff0c;还是清了后台&#xff08;关闭&#xff09;&#xff0c;都可以收到消息 通知消息&#xff1a;只能app打开了&#xff0c;才能收到 1.开通unipush 2.点击上图的unipush2.0下面的配置&am…

【VSCode】调试器debugger详细使用手册

Visual Studio Code 的主要功能之一是其出色的调试支持。VS Code 的内置调试器有助于加速您的编辑、编译和调试循环。 调试器扩展 VS Code 具有对Node.js运行时的内置调试支持&#xff0c;并且可以调试 JavaScript、TypeScript 或任何其他转换为 JavaScript 的语言。 开始调…

【CSS】CSS 特性 ③ ( CSS 优先级 | 权重叠加计算公式 )

文章目录一、权重叠加计算公式1、后代选择器权重计算2、后代选择器权重计算二3、链接伪类选择器权重计算二、代码示例1、标签结构2、后代选择器选择案例 12、后代选择器选择案例 23、后代选择器选择案例 3一、权重叠加计算公式 在使用 多个类型的 基础选择器 进行 组合 时 , 如…

【JavaScript】手撕前端面试题:手写new操作符 | 手写Object.freeze

&#x1f5a5;️ NodeJS专栏&#xff1a;Node.js从入门到精通 &#x1f5a5;️ 博主的前端之路&#xff08;源创征文一等奖作品&#xff09;&#xff1a;前端之行&#xff0c;任重道远&#xff08;来自大三学长的万字自述&#xff09; &#x1f5a5;️ TypeScript知识总结&…

HttpServletRequest 获取参数

1 HttpServletRequest获取参数方法 可以使用HttpServletRequest获取客户端的请求参数&#xff0c;相关方法如下&#xff1a; String getParameter(String name)&#xff1a;通过指定名称获取参数值&#xff1b;String[] getParameterValues(String name)&#xff1a;通过指定名…

new bing 申请加入候补

最近Chatgpt非常的火爆&#xff0c; 微软也是把新版必应Chatgpt的测试版已经推出。1.下载安装新必应(new bing)需要下载 Edge新版本 Edge dev下载链接: Microsoft Edge 预览体验成员 (microsoftedgeinsider.com)安装插件在设置中找到扩展-> 获取Microsoft Edge扩展搜索获取 …

vue中的nexttick

我们了解nexttick之前&#xff0c;我们先来看一个例子。 我们先要每一次都在点击按钮的时候&#xff0c;都进行字符串的累加操作&#xff0c;并且在该函数中计算该字符串所占的高度offsetHeight&#xff0c;但是我们当进行第一次点击的时候&#xff0c;此时打印的结果不符合,因…

Javascript Object和Map之间的转换

简单的区分Map和Object Map是ES6退出的一个类型&#xff0c;特点&#xff1a;任何值都可作为属性名 Object特点&#xff1a;属性名只能是字符串(一开始我也不信&#xff0c;测试后才发现的) 代码图片创建一个map类型 new Map([[key, value],[key1, value1] ])简单的介绍下面…

vue 获取后端数据

目录 proxy 解决本地请求问题 vite Vue CLI fetch 代码演示 Post请求 ​编辑Get请求 Axios 安装 代码演示 Post请求 Get请求 TS 封装Axios 代码演示 proxy 解决本地请求问题 为什么会出现跨域问题? 浏览器的同源策略 首先给出浏览器“同源策略”的一种经典定义…