Vue结合Element UI的el-table打造加工工序甘特图可视化解决方案_elementui 甘特图
引言
在玻璃加工行业,高效管理切割、磨边、洗、钢化、丝印等复杂工序对于提升生产效率至关重要。本文将介绍如何利用Vue.js框架结合Element UI组件库,自定义实现一个工序甘特图,以可视化展示各道工序的时间线与进度,为生产调度带来便利。
目录
-
- 引言
- 渲染效果
- 拖拽滚动,同产品高亮
-
- 小时高度,宽度改变动态效果
- 全部代码:
- 优化
-
- 优化内容:减少timeInterval的调用,拖动滚动效果,同产品高亮效果。
渲染效果
同产品高亮
拖拽滚动,同产品高亮
小时高度,宽度改变动态效果
全部代码:
优化
优化内容:减少timeInterval的调用,拖动滚动效果,同产品高亮效果。
<template> <div class=\"home group\"> <el-card :span=\"24\" :xs=\"24\" class=\"box-card\" id=\"boxCard\" ref=\"tableBox\" append-to-body > <el-row> <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain icon=\"el-icon-view\" size=\"mini\" @click.native=\"switchView\" >切换视图</el-button > </el-col> <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain icon=\"el-icon-s-tools\" size=\"mini\" @click.native=\"startSchedulingProductionFun\" >开始排产</el-button > </el-col> <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain icon=\"el-icon-s-tools\" size=\"mini\" @click.native=\"proposalFun\" >负荷调整建议</el-button > </el-col> <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain size=\"mini\" icon=\"el-icon-document\" >查看物料到货计划</el-button > </el-col> <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain size=\"mini\" icon=\"el-icon-upload2\" >导出</el-button > </el-col> <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain icon=\"el-icon-printer\" size=\"mini\" >打印</el-button > </el-col> <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain icon=\"el-icon-lock\" size=\"mini\" >锁定排产</el-button > </el-col> <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain icon=\"el-icon-unlock\" size=\"mini\" >解锁排产</el-button ></el-col > <!-- <el-col :span=\"1.5\" style=\"margin-right: 10px\"> <el-button type=\"primary\" plain icon=\"el-icon-rank\" size=\"mini\" >拖拽排产</el-button ></el-col > --> </el-row> <el-table @mousedown.native=\"startDrag\" @mousemove.native=\"handleDrag\" @mouseup.native=\"endDrag\" @mouseleave.native=\"endDrag\" v-loading=\"loading\" element-loading-text=\"正在加载处理数据....\" v-if=\"isView\" :virtual-scroll=\"true\" class=\"ganteTable\" :class=\"isFillScreen ? \'fullscreen-table ganteTable\' : \'ganteTable\'\" ref=\"ganTeTable\" :height=\"tableHeight\" :style=\" timeArr.length > 0 ? \'width:\' + tableWidth + \'px;\' : \'width:239px;height:100%\' \" :key=\"tableKey\" :cell-style=\"iCellStyle\" :fit=\"false\" :data=\"tableData\" border align=\"center\" size=\"mini\" :span-method=\"tableSpanMethod\" > <el-table-column fixed align=\"center\" prop=\"index1\" label=\"工作中心\" class=\"index1\" > <template slot-scope=\"scope\"> <el-popover placement=\"top-start\" title=\"工作中心\" trigger=\"hover\" :content=\"scope.row.index1.label\" > <div slot=\"reference\" :class=\"rowHeight < 36 ? \'oneLineCls\' : \'twoLineCls\'\" > {{ scope.row.index1.label }} </div> </el-popover> </template> </el-table-column> <el-table-column fixed align=\"center\" prop=\"index2\" label=\"产线名称\" class=\"index1\" > <template slot-scope=\"scope\"> <!-- {{ scope.row.index2.label }} --> <el-popover placement=\"top-start\" title=\"产线名称\" width=\"200\" trigger=\"hover\" :content=\"scope.row.index3.label\" > <div slot=\"reference\" :class=\"rowHeight < 36 ? \'oneLineCls\' : \'twoLineCls\'\" > {{ scope.row.index2.label }} </div> </el-popover> </template> </el-table-column> <el-table-column fixed height=\"47px\" align=\"center\" prop=\"index3\" label=\"设备名称\" class=\"index1\" > <template slot-scope=\"scope\"> <!-- {{ scope.row.index3.label }} --> <el-popover placement=\"top-start\" title=\"设备名称\" width=\"200\" trigger=\"hover\" :content=\"scope.row.index3.label\" > <div slot=\"reference\" :class=\"rowHeight < 36 ? \'oneLineCls\' : \'twoLineCls\'\" > {{ scope.row.index3.label }} </div> </el-popover> </template> </el-table-column> <!-- 表头遍历日期 --> <div v-for=\"(timeArrItem, index1) in timeArr\" :key=\"timeArrItem + index1 + \'\'\" > <el-table-column height=\"47px\" align=\"center\" :label=\"timeArrItem.substr(0, 10)\" > <!-- 表头遍历时间 --> <template v-for=\"(hourArrItem, index2) in hourArr\"> <el-table-column height=\"47px\" align=\"center\" class=\"pc-box\" :label=\"hourArrItem + \'\'\" :key=\"index1 + \'-\' + index2 + 5 + timeArrItem + hourArrItem\" :width=\"latticeWidth + \'px;\'\" > <template slot=\"header\"> <div class=\"hour-item\" @mousemove=\"updateXY\"> <span>{{ hourArrItem }}</span> <div class=\"hour-ten-scale-box\"><div class=\"hour-ten-scale\" v-for=\"x in 5\" :key=\"x\"></div> </div> <div class=\"hour-ten-side-box\"><div class=\"hour-ten-side-box-left\"></div><div class=\"hour-ten-side-box-right\"></div> </div> </div> <div class=\"minute-scale\" v-if=\"index2 === 0 && index1 === 0\" :style=\"{ width: tableWidth + \'px\' }\" ></div> <div ref=\"pointBox\" id=\"pointBox\" v-if=\"index2 === 0 && index1 === 0\" ></div> </template> <template slot-scope=\"scope\" v-if=\"index2 === 0 && index1 === 0\" > <div id=\"content-box\" @mousemove=\"updateXY\" class=\"content-box\" :ref=\"index1 + \'-\' + index2 + 5\" :style=\"\'width:\' +timeArr.length * 24 * latticeWidth +\'px;overflow:hidden;\' \" > <el-tooltip:draggable=\"true\"v-for=\"workItem in scope.row.workPlanList.data\":key=\"workItem.onlyId\"class=\"item\"style=\"z-index: 99999\"effect=\"dark\"content=\"Bottom Right 提示文字\"placement=\"bottom-end\" ><div slot=\"content\"> <p>信息</p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 工序:{{ workItem.workOrder }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 产品名称:{{ workItem.materialName }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 产品编码:{{ workItem.materialNo }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 产线名称:{{ workItem.productLineName }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 产线编码:{{ workItem.productLineSn }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 设备名称:{{ scope.row.index3.label }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 开始时间:{{ workItem.startTime }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 结束时间:{{ workItem.endTime }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 交期时间:{{ workItem.deliverTime }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 工作中心描述:{{ workItem.workCenterDesc }} </p> <p v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" > 工作中心编码:{{ workItem.workCenterSn }} </p> <p v-if=\"workItem.lineChangeFlag === true\"> 状态:换线 </p> <p v-if=\"workItem.restFlag === true\">状态:休息</p></div><div v-if=\" workItem.lineChangeFlag === null && workItem.restFlag === null \" @click=\" highlightSimilarElements( \'A\' + workItem.materialNo + workItem.deliverTime ) \" :draggable=\"true\" :class=\" \'AAA A\' + workItem.materialNo + workItem.deliverTime + \'\' \" :style=\" \'background-color:\' + workItem.color + \';\' + \' position: absolute;\' + timeInterval( workItem.startTime, workItem.endTime, workItem.onlyId ) \"></div><div v-if=\"workItem.lineChangeFlag === true\" @click=\" highlightSimilarElements( \'A\' + workItem.materialNo + workItem.deliverTime ) \" :draggable=\"true\" :class=\" \'AAA A\' + workItem.materialNo + workItem.deliverTime + \'\' \" :style=\" \' position: relative; top: 50%; bottom: 50%;height: 3px\' + \';\' + \'background-color:\' + \'#FF0000\' + \';\' + \' position: absolute;\' + timeInterval( workItem.startTime, workItem.endTime, workItem.onlyId ) \"></div><div v-if=\"workItem.restFlag === true\" @click=\" highlightSimilarElements( \'A\' + workItem.materialNo + workItem.deliverTime ) \" :draggable=\"true\" :class=\" \'AAA A\' + workItem.materialNo + workItem.deliverTime + \'\' \" :style=\" \'background-color:\' + \'#cedcf0;\' + \' position: absolute;\' + timeInterval( workItem.startTime, workItem.endTime, workItem.onlyId ) \"></div> </el-tooltip> </div> </template> </el-table-column> </template> </el-table-column> </div> </el-table> <day-table-view v-if=\"!isView\"></day-table-view> <scheduling-window ref=\"schedulingWindow\" @openSchedulingResultsWindow=\"openSchedulingResultsWindow\" ></scheduling-window> <scheduling-results-window ref=\"schedulingResultsWindow\" @switchView=\"switchView\" ></scheduling-results-window> <suggestions-load-adjustment ref=\"suggestionsLoadAdjustment\" @reScheduleProduction=\"getGanttChartData\" ></suggestions-load-adjustment> <div class=\"slider-block\" v-if=\"isView\"> <div class=\"slider-block-text\">小时高度:</div> <el-slider @change=\"sliderHeightChange\" style=\"width: 200px\" v-model=\"rowHeight\" :min=\"24\" :max=\"72\" :step=\"12\" show-stops > </el-slider> </div> <div class=\"slider-block1\" v-if=\"isView\"> <div v-if=\"isSlider\" class=\"slider-block-text\">小时宽度:</div> <el-slider v-if=\"isSlider\" @change=\"sliderWidthChange\" style=\"width: 200px\" v-model=\"latticeWidth\" :min=\"minWidth\" :max=\"maxWidth\" :step=\"12\" show-stops > </el-slider> </div> <!-- <div class=\"slider-block1-btn\" v-show=\"isView\"> <el-button @click=\"fullScreenFunc\" size=\"mini\"> {{ isFillScreen ? \"退出全屏\" : \"全屏\" }} </el-button> </div> --> </el-card> </div></template><script>import { getGanttChart } from \"@/api/ganttChart/ganttChart\";import dayTableView from \"./gentterTable.vue\";import schedulingWindow from \"./startSchedulingWindow.vue\";import schedulingResultsWindow from \"./schedulingResultsWindow.vue\";import suggestionsLoadAdjustment from \"./suggestionsLoadAdjustment.vue\";export default { name: \"dailyScheduling\", components: { dayTableView, schedulingWindow, schedulingResultsWindow, suggestionsLoadAdjustment, }, data() { return { styleCache: {}, //存放缓存结果 highlightedClass: null, dragging: false, startX: 0, startY: 0, scrollLeft: 0, scrollTop: 0, ganTeTable: null, // 初始化表格引用 isFillScreen: false, // 是否全屏 loading: false, //表格数据处理 isView: true, content: \"内容\", isSlider: true, //是否显示缩放和拉长 minWidth: 24, //最小缩放 maxWidth: 72, //最大缩放 rowHeight: 24, //每一行的高度 tableWidth: 0, pointObj: { pointX: 340, pointBoxLeft: 0, //指针盒子距离左侧的偏移量 }, eleData: { dayList: [], startTime: \"\", // 开始时间 endTime: \"\", // 结束时间 workcenterList: [], }, tableHeight: 0, //table的高度 oneHourPx: 24, //一小时间隔15px 一分钟间隔0.25px oneMinutePx: 0.4, //一分钟0.4px tableData: null, //表格数据 latticeWidth: 30, //一个单元格的宽度最小24px timeArr: [], //天数集合 hourArr: [ \"00\", \"01\", \"02\", \"03\", \"04\", \"05\", \"06\", \"07\", \"08\", \"09\", \"10\", \"11\", \"12\", \"13\", \"14\", \"15\", \"16\", \"17\", \"18\", \"19\", \"20\", \"21\", \"22\", \"23\", ], tableKey: 0, //值改变更新table earliestTime: \"\", //最早时间 latestTime: \"\", //最晚时间 conWidth: 0, usedKeys: new Set(), }; }, created() { window.onload = function () { document.addEventListener(\"touchstart\", function (event) { if (event.touches.length > 1) { event.preventDefault(); } }); document.addEventListener(\"gesturestart\", function (event) { event.preventDefault(); }); }; //初始化表格高度,和初始化指针数据 this.$set(this.pointObj, \"pointX\", 0); this.$nextTick(() => { this.tableHeight = this.$refs.tableBox.offsetHeight - 110; }); }, mounted() { this.getGanttChartData(); //禁止ctrl+滚轮缩放 let scrollFunc = function (e) { e = e || window.event; if (e.wheelDelta && event.ctrlKey) { //IE/Opera/Chrome event.returnValue = false; } else if (e.detail) { //Firefox event.returnValue = false; } }; /*注册事件*/ if (document.addEventListener) { document.addEventListener(\"DOMMouseScroll\", scrollFunc, false); } //W3C window.onmousewheel = document.onmousewheel = scrollFunc; //IE/Opera/Chrome/Safari //设置表格最大高度沾满全屏 this.$nextTick(() => { this.tableHeight = document.getElementById(\"boxCard\").offsetHeight - 110; window.addEventListener(\"scroll\", this.handleScroll, true); //获取标针盒子距离浏览器左侧的距离 // this.pointObj.pointBoxLeft = document.getElementById(\"pointBox\").getBoundingClientRect().left //监听浏览器窗口变化 const that = this; window.onresize = () => { return (() => { //计算装有指针的盒子距离浏览器左侧的距离,指针减去这个盒子距离浏览器左侧的偏移量得到正确时间指针 this.pointObj.pointBoxLeft = 0; console.log(\"窗口改变了\"); })(); }; //如果日期小于2天 则官渡为39 if (this.timeArr.length <= 2) { this.latticeWidth = 24; this.minWidth = 24; this.maxWidth = 72; } // console.log(\"我被执行了\", this.timeArr.length); if (this.timeArr.length >= 2) { this.latticeWidth = 24; this.minWidth = 24; this.maxWidth = 72; } if (this.timeArr.length == 1) { this.latticeWidth = 66; this.isSlider = false; } this.widthAA = this.timeArr.length * 24 * this.latticeWidth; this.tableWidth = this.widthAA + 240; }); this.ganTeTable = this.$refs.ganTeTable.$el.querySelector( \".el-table__body-wrapper\" ); }, computed: { // timeInterval() { // return (startTime, endTime,id) => { // console.log(\"我被执行了\", startTime, endTime,id); // let time = new Date(endTime) - new Date(startTime); // 获取任务开始时间和任务结束时间的相差时间戳 // let minuteDiff = Math.floor(time / (60 * 1000)); // 相差时间间隔 // let initialTime = new Date(startTime) - new Date(this.timeArr[0]); // 获取距离最开始的距离 // let inittiDiff = Math.floor(initialTime / (60 * 1000)); // let obj = { // widthPx: minuteDiff * (this.latticeWidth / 60), // startPx: inittiDiff * (this.latticeWidth / 60), // }; // return \"width:\" + obj.widthPx + \"px;left:\" + obj.startPx + \"px;\"; // }; // }, }, methods: { timeInterval(startTime, endTime, id) { // Check cache first const cacheKey = `${startTime}_${endTime}_${id}`; if (this.styleCache[cacheKey]) { return this.styleCache[cacheKey]; } console.log(\"我被执行了\", startTime, endTime, id); let time = new Date(endTime) - new Date(startTime); // 获取任务开始时间和任务结束时间的相差时间戳 let minuteDiff = Math.floor(time / (60 * 1000)); // 相差时间间隔 let initialTime = new Date(startTime) - new Date(this.timeArr[0]); // 获取距离最开始的距离 let inittiDiff = Math.floor(initialTime / (60 * 1000)); let obj = { widthPx: minuteDiff * (this.latticeWidth / 60), startPx: inittiDiff * (this.latticeWidth / 60), }; const style = `width:${obj.widthPx}px;left:${obj.startPx}px;`; // Store in cache this.styleCache[cacheKey] = style; return style; }, generateRandomKey() { let key; do { key = Math.random().toString(36).substr(2, 9); // Generate a random string key } while (this.usedKeys.has(key)); // Ensure key is unique this.usedKeys.add(key); // Add key to usedKeys set return key; }, // 负荷调整建议 proposalFun() { this.$refs.suggestionsLoadAdjustment.open(); }, // 开始排产弹窗 openSchedulingResultsWindow() { this.showDialogVisible = false; this.$refs.schedulingResultsWindow.open(); }, highlightSimilarElements(targetClass) { // 先移除所有元素的高亮状态 if (this.highlightedClass) { document.querySelectorAll(`.${this.highlightedClass}`).forEach((el) => { el.classList.remove(\"highlighted\"); }); } // 更新当前要高亮的类名 this.highlightedClass = targetClass; // 查找所有具有目标类名的元素并应用高亮 document.querySelectorAll(`.${targetClass}`).forEach((el) => { el.classList.add(\"highlighted\"); }); }, // 开始排程的英文 startSchedulingProductionFun() { this.$refs.schedulingWindow.showDialogVisible = true; }, // 当鼠标按下时触发,记录起始位置和滚动条位置。 startDrag(e) { this.dragging = true; this.startX = e.pageX; this.startY = e.pageY; this.scrollLeft = this.ganTeTable.scrollLeft; this.scrollTop = this.ganTeTable.scrollTop; }, // 当鼠标移动时触发,计算鼠标移动距离并更新滚动条位置。 handleDrag: _.throttle(function (e) { if (this.dragging) { const dx = e.pageX - this.startX; const dy = e.pageY - this.startY; this.ganTeTable.scrollLeft = this.scrollLeft - dx; this.ganTeTable.scrollTop = this.scrollTop - dy; } }, 16), // 使用节流函数,16ms对应60fps的更新频率 // 鼠标松开,移出 endDrag() { this.dragging = false; }, //全屏退出全屏 fullScreenFunc() { if (!document.fullscreenElement) { this.enterFullScreen(); this.isFillScreen = true; this.latticeWidth = 72; this.sliderWidthChange(); } else { this.exitFullScreen(); this.latticeWidth = 39; this.sliderWidthChange(); this.isFillScreen = false; } }, //进入全屏 enterFullScreen() { let element = document.documentElement; if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.mozRequestFullScreen) { /* Firefox */ element.mozRequestFullScreen(); } else if (element.webkitRequestFullscreen) { /* Chrome, Safari & Opera */ element.webkitRequestFullscreen(); } else if (element.msRequestFullscreen) { /* IE/Edge */ element.msRequestFullscreen(); } }, //退出全屏 exitFullScreen() { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.mozCancelFullScreen) { /* Firefox */ document.mozCancelFullScreen(); } else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */ document.webkitExitFullscreen(); } else if (document.msExitFullscreen) { /* IE/Edge */ document.msExitFullscreen(); } this.isFillScreen = false; }, async getGanttChartData() { this.loading = true; console.log(\"开始\"); //判断是否传递过来id let id = null; if (typeof this.$route.query.id !== \"undefined\") { id = JSON.parse(this.$route.query.id); } let repos = null; await getGanttChart() .then((res) => { repos = res; this.processGanttData(repos); this.loading = false; }) .catch((error) => { // 请求失败的处理 console.error(\"请求数据失败:\", error); this.loading = false; // 可选:设置加载状态为false // 可以进行错误处理或者用户提示 }); }, processGanttData(repos) { // 在获取到数据后进行处理 this.timeArr = repos.data.dayList.map((element) => element + \" 00:00:00\"); this.eleData.workcenterList = repos.data.workcenterList; this.treeToTableData(); for (let i = 0; i < this.tableData.length; i++) { for (let j = 0; j < this.tableData[i].workPlanList.data.length; j++) { this.tableData[i].workPlanList.data[j].onlyId = this.generateRandomKey(); // this.tableData[i].workPlanList.data[j].pxObj={...this.timeInterval(this.tableData[i].workPlanList.data[j].startTime,this.tableData[i].workPlanList.data[j].endTime)} } } // 根据时间长度设置 latticeWidth if (this.timeArr.length <= 2) { this.latticeWidth = 24; this.minWidth = 24; this.maxWidth = 72; } else if (this.timeArr.length >= 3) { this.latticeWidth = 21; this.minWidth = 21; this.maxWidth = 72; } else if (this.timeArr.length == 1) { this.latticeWidth = 66; this.minWidth = 66; this.maxWidth = 66; this.isSlider = false; } // 计算表格宽度 this.widthAA = this.timeArr.length * 24 * this.latticeWidth; this.tableWidth = this.widthAA + 240; // 更新表格布局 this.$nextTick(() => { if (this.$refs.ganTeTable) { this.$refs.ganTeTable.doLayout(); } this.$forceUpdate(); }); }, //切换视图 switchView() { this.isView = !this.isView; if (this.isView === true) { this.getGanttChartData(); } }, //物料信息展开 //行高回调 iCellStyle() { return \"height:\" + this.rowHeight + \"px\"; }, //改变行高 sliderHeightChange() { //重新布局表格 this.$nextTick(() => { this.styleCache = {}; this.iCellStyle(); this.$refs.ganTeTable.doLayout(); // this.tableKey = Math.random(); }); }, //改变行宽 sliderWidthChange() { //重新布局表格 this.$nextTick(() => { this.styleCache = {}; this.widthAA = this.timeArr.length * 24 * this.latticeWidth; this.tableWidth = this.widthAA + 240; this.$refs.ganTeTable.doLayout(); // this.tableKey = Math.random(); }); }, // 当鼠标移动时触发 updateXY(e) { let x = e.clientX; //计算装有指针的盒子距离浏览器左侧的距离,指针减去这个盒子距离浏览器左侧的偏移量得到正确时间指针 this.pointObj.pointBoxLeft = document .getElementById(\"pointBox\") .getBoundingClientRect().left; this.$nextTick(() => { this.boble = false; document.querySelector( \"#pointBox\" ).innerHTML = `<div style=\"width: 1px; height: 225px; position: absolute; background: red;top:-18px; left:${ x - this.pointObj.pointBoxLeft }px;\" id=\"head-pointer\" class=\"head-pointer\">