一、前言
接到公司需求,要做一个可拖拽的甘特图来实现排期需求,官方的插件要付费还没有中文的官方文档可以看,就去找了各种开源的demo来看,功能上都不是很齐全,于是总结了很多demo,合在一起组成了一版较为完整的满足需求的甘特图。
二、主要功能
1.拖拽 拖拽功能是甘特图的主要功能,该demo实现了甘特图时间块上、下、左、右拖拽功能。
2.排序 拖拽后时间块进行排序,计算重叠区域大小确定插入位置。
3.时间选择 结合element-ui的日期时间选择器来确定时间轴。
5.新建排期任务 使用element-ui的弹框以及表单 新建成功的排期列表添加到排期任务中。
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,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('时间块位置变化后回调事件',rowNId);
},save() {
console.log(JSON.stringify(this.ganttData));
},
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;
},
// 搜索数据数组
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);
},
四、实现效果
甘特图实现
总结
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。