引言
在玻璃加工行业,高效管理切割、磨边、洗、钢化、丝印等复杂工序对于提升生产效率至关重要。本文将介绍如何利用Vue.js框架结合Element UI组件库,自定义实现一个工序甘特图,以可视化展示各道工序的时间线与进度,为生产调度带来便利。
效果
小时高度,宽度改变动态效果
全部代码:
视图部分
<template>
<div class="home group">
<el-card
v-loading="loading"
:span="24"
element-loading-text="正在加载处理数据...."
: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 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
v-show="isView"
class="ganteTable"
ref="ganTeTable"
:height="tableHeight"
:style="timeArr.length > 0 ? 'width: 100%;' : 'width:239px;'"
: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"
width="70px"
>
<template slot-scope="scope">
<el-popover
placement="top-start"
title="工作中心"
width="200"
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"
width="100px"
>
<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"
width="100px"
>
<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>
<!-- 表头遍历日期 -->
<template v-for="(timeArrItem, index1) in timeArr">
<el-table-column
height="47px"
align="center"
:label="timeArrItem.substr(0, 10)"
:key="timeArrItem + index1 + ''"
width="500px"
>
<!-- 表头遍历时间 -->
<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 @mousemove="updateXY">
{{ hourArrItem }}
</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, index3) in scope.row.workPlanList.data"
:key="
workItem.itemId +
workItem.endTime +
workItem.startTime +
index2 +
index3
"
class="item"
effect="dark"
content="Bottom Right 提示文字"
placement="bottom-end"
>
<div slot="content">
<br />
<p>信息:</p>
<p>工作中心名称:{{ scope.row.index3.label }}</p>
<p>开始时间:{{ workItem.startTime }}</p>
<p>开始时间:{{ workItem.endTime }}</p>
<p>工作中心描述:{{ workItem.workCenterDesc }}</p>
<p>工作中心编码:{{ workItem.workCenterSn }}</p>
</div>
<div
:draggable="true"
class="AAA"
:style="
'background-color:' +
workItem.color +
';' +
'width:' +
timeInterval(workItem.startTime, workItem.endTime)
.widthPx +
'px;' +
' position: absolute;' +
'left:' +
timeInterval(workItem.startTime, workItem.endTime)
.startPx +
'px;'
"
></div>
</el-tooltip>
</div>
</template>
</el-table-column>
</template>
</el-table-column>
</template>
</el-table>
<!-- <day-table-view v-show="!isView"></day-table-view> -->
<div class="slider-block" v-show="isView">
<div class="slider-block-text">小时高度:</div>
<!--
v-if="isSlider" -->
<el-slider
@change="sliderHeightChange"
style="width: 200px"
v-model="rowHeight"
:min="24"
:max="72"
:step="12"
show-stops
>
</el-slider>
</div>
<div class="slider-block" v-show="isView">
<div 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>
</el-card>
</div>
</template>
<script>
js部分
export default {
name: "dailyScheduling",
components: {
// dayTableView
},
data() {
return {
loading: false, //表格数据处理
isView: true,
content: "内容",
isSlider: true, //是否显示缩放和拉长
minWidth: 24, //最小缩放
maxWidth: 72, //最大缩放
rowHeight: 24, //每一行的高度
pointObj: {
pointX: 340,
pointBoxLeft: 0, //指针盒子距离左侧的偏移量
},
eleData: {
dayList:[ // 天数
"2023-05-08",
"2023-05-09",
"2023-05-10",
],
startTime: "2023-05-08 00:00:00", // 开始时间
endTime: "2023-05-10 00:00:00", // 结束时间
timeList:[],
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",
// "24",
],
tableKey: 0, //值改变更新table
earliestTime: "", //最早时间
latestTime: "", //最晚时间
conWidth: 0,
};
},
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 = 39;
this.minWidth = 39;
this.maxWidth = 72;
}
console.log("我被执行了", this.timeArr.length);
if (this.timeArr.length >= 3) {
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;
});
},
methods: {
getGanttChartData() {
//判断是否传递过来id
let id = null;
if (typeof this.$route.query.id !== "undefined") {
id = JSON.parse(this.$route.query.id);
}
this.loading = true;
// getGanttChartData({productPlanTaskId:id}).then(res=>{
this.timeArr = [];
this.eleData.dayList.forEach((element) => {
this.timeArr.push(element + " 00:00:00");
});
this.eleData.workcenterList = this.eleData.workcenterList;
this.treeToTableData();
//如果日期小于2天 则为39
if (this.timeArr.length <= 2) {
this.latticeWidth = 39;
this.minWidth = 39;
this.maxWidth = 72;
}
if (this.timeArr.length >= 3) {
this.latticeWidth = 21;
this.minWidth = 21;
this.maxWidth = 72;
}
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.$nextTick(() => {
this.$refs.ganTeTable.doLayout();
this.$forceUpdate();
});
this.loading = false;
// })
},
//切换视图
switchView() {
this.isView = !this.isView;
},
//物料信息展开
//行高回调
iCellStyle() {
return "height:" + this.rowHeight + "px";
},
//改变行高
sliderHeightChange() {
//重新布局表格
this.$nextTick(() => {
this.iCellStyle();
this.$refs.ganTeTable.doLayout();
// this.tableKey = Math.random()
});
},
//改变行宽
sliderWidthChange() {
//重新布局表格
this.$nextTick(() => {
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 v-if="boble" style="width: 1px; height: 25px; position: absolute; background: red;left:${
x - this.pointObj.pointBoxLeft
}px;" id="head-pointer" class="head-pointer"> </div>`;
});
this.boble = false;
},
parentW(index1, index2) {
if (this.$refs[index1 + "-" + index2 + 5]) {
console.log(this.$refs[index1 + "-" + index2 + 5][0].clientWidth);
return this.$refs[index1 + "-" + index2 + 5][0].clientWidth;
} else {
return 0;
}
},
parentH(index1, index2) {
if (this.$refs[index1 + "-" + index2 + 5]) {
console.log(this.$refs[index1 + "-" + index2 + 5][0].clientHeight);
return this.$refs[index1 + "-" + index2 + 5][0].clientHeight;
} else {
return 0;
}
},
draggableStart() {
// console.log(this.tableData[0].workPlanList.data);
},
draggableEnd() {
// console.log(this.tableData[0].workPlanList.data);
},
/**
* 计算两个时间的间隔
* 入参 开始时间,结束时间
* 回参 返回一个任务距离最开始时间的分钟[距离],和一个任务开始时间和结束时间的分钟[距离],
*/
timeInterval(startTime, endTiem) {
let time = new Date(endTiem) - 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));
// console.log("latticeWidth",inittiDiff)
return {
widthPx: minuteDiff * (this.latticeWidth / 60),
startPx: inittiDiff * (this.latticeWidth / 60),
};
},
treeToTableData() {
console.log("this.eleData.workcenterList", this.eleData.workcenterList);
//将树状结构格式转换成二维数组表格形式
let ewArr = this.parseTreeToRow(this.eleData.workcenterList);
let tableData = [];
ewArr.map((item) => {
let obj = {};
item.map((itemc, indexb) => {
obj["index" + (indexb + 1)] = {
id: itemc.id,
label: itemc.label,
};
if (typeof itemc.workPlanList !== "undefined") {
obj.workPlanList = { data: itemc.workPlanList };
}
});
tableData.push(obj);
});
this.tableData = tableData;
// console.log("tableData", this.tableData);
},
/**
* 递归-----将树结构数据格式,转化为,二维数组 表格形式
* @param node 树的源数据
* @param data 树转化为二维数组的数据
* @param row 临时存储数据
* @returns {*[]}
*/
parseTreeToRow(node, data = [], row = []) {
node.map((item) => {
let obj = {
id: item.workCenterId || item.lineId || item.machineId,
label: item.workCenterName || item.lineName || item.machineDescribe,
};
if (typeof item.workPlanList !== "undefined") {
obj.workPlanList =
item.workPlanList.length > 0 ? item.workPlanList : [];
}
if (item.children && item.children.length != 0) {
this.parseTreeToRow(item.children, data, [...row, obj]);
} else {
data.push([...row, obj]);
}
});
return data;
},
/**
* 合并行或列的计算方法
*/
tableSpanMethod({ row, column, rowIndex, columnIndex }) {
// console.log("row, column, rowIndex, columnIndex");
// console.log(
// "row, column, rowIndex, columnIndex",
// columnIndex,
// row,
// column,
// rowIndex,
// columnIndex
// );
return {
rowspan:
columnIndex < 3
? this.mergeRows(
row[column.property],
this.tableData,
rowIndex,
column.property
)
: 1,
colspan: 1,
};
},
/**
* 表格单元格合并-----行
* @param {Object} value 当前单元格的值
* @param {Object} data 当前表格所有数据
* @param {Object} index 当前单元格的值所在 行 索引
* @param {Object} property 当前列的property
* @returns {number} 待合并单元格数量
*/
mergeRows(value, data, index, property) {
// 判断 当前行的该列数据 与 上一行的该列数据 是否相等
if (index !== 0 && value.label === data[index - 1][property].label) {
// 返回 0 使表格被跨 行 的那个单元格不会渲染
return 0;
}
// 判断 当前行的该列数据 与 下一行的该列数据 是否相等
let rowSpan = 1;
for (let i = index + 1; i < data.length; i++) {
if (value.label !== data[i][property].label) {
break;
}
rowSpan++;
}
return rowSpan;
},
},
};
样式代码
<style lang="less" scoped>
#pointBox {
position: relative;
z-index: 2 !important;
// background: red;
// width: 100%;
// height: 10px;
}
.aaaaa {
width: 20px;
background: red;
height: 25px;
position: relative;
}
//表头指针
.head-pointer {
width: 1px;
height: 18px;
position: absolute;
background: red;
z-index: 2 !important;
}
//表头清楚内容
/deep/ .el-table th.el-table__cell {
overflow: visible !important;
}
//占满屏幕
.box-card {
height: calc(100vh - 100px); /*示例中顶部区域固定高度190px*/
}
/deep/ .el-table--enable-row-transition .el-table__body td.el-table__cell {
// height: 47px;
}
/deep/ .el-table_1_column_1 .el-table_1_column_2 .el-table_1_column_3 {
z-index: 300 !important;
}
/deep/ .el-table_1_column_4_column_5_column_6 > .cell {
// width: 3120px !important;
padding: 0;
}
.content-box {
// width: 2000px !important;
z-index: 2 !important;
text-align: left;
position: absolute;
top: 0;
bottom: 0;
}
.AAA {
// left: -10px;
// height: 30px;
position: relative;
width: 15px;
height: 100%;
z-index: 2 !important;
}
/deep/ .el-table_1_column_4_column_5_column_6 .is-center .el-table__cell {
// width: 3060px !important;
display: flex !important;
// z-index: 1 !important;
}
//去除鼠标移入
/deep/ .group > .el-table--enable-row-hover .el-table__body tr:hover > td {
background-color: white !important;
height: 100%;
}
/deep/
.group
> .el-table--enable-row-hover
.el-table__body
tr:hover
> td
> div {
height: 100%;
}
/deep/ .el-table .cell {
overflow: visible !important;
padding-left: 0px !important;
display: flex; //横向排列
padding-right: 0px !important;
width: 100%;
text-align: center;
}
/deep/ .el-table--mini .el-table__cell {
// z-index: 1 !important;
padding: 0 !important;
}
::v-deep .el-table th.el-table__cell > .cell {
display: contents;
line-height: 15px;
}
.timeItemBox {
display: flex;
width: 500px;
margin-left: -10px;
z-index: 1;
z-index: 200 !important;
}
.timeItem {
height: 37.9px;
width: 30px;
}
.wl-real-start {
left: 50%;
&:after {
position: absolute;
top: 0;
// left: -5px;
left: 0;
z-index: 1;
content: "";
width: 8px;
height: 36px;
// border-radius: 50%;
background: #fcc300;
}
}
.wl-real-start1 {
left: 50%;
&:after {
position: absolute;
top: 0;
left: 0;
z-index: 200 !important;
content: "";
}
}
//伸缩加长
.slider-block {
margin-top: 5px;
margin-left: 20px;
float: right;
display: flex;
line-height: 33px;
.slider-block-text {
margin-right: 5px;
font-size: 16px;
}
}
//超过一行显示
.oneLineCls {
text-overflow: -o-ellipsis-lastline;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
}
.twoLineCls {
text-overflow: -o-ellipsis-lastline;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.ganteTable {
margin-top: 20px;
}
</style>