You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
742 lines
17 KiB
742 lines
17 KiB
<template> |
|
<basic-container> |
|
<avue-form :option="option" @submit="submit" @error="error"> |
|
<template #menu-form> |
|
<el-button type="primary" icon="el-icon-search" @click="handleSubmit"> 搜索 </el-button> |
|
<el-button icon="el-icon-delete" @click="handleSubmit"> 清空 </el-button> |
|
</template> |
|
</avue-form> |
|
|
|
<div class="gantt-container"> |
|
<!-- 头部标题和图例 --> |
|
<div class="gantt-header"> |
|
<!-- <h2>设备生产任务甘特图</h2> --> |
|
<div class="status-legend"> |
|
<div class="legend-item"> |
|
<span class="legend-color completed"></span> |
|
<span>已完成</span> |
|
</div> |
|
<div class="legend-item"> |
|
<span class="legend-color processing"></span> |
|
<span>进行中</span> |
|
</div> |
|
<div class="legend-item"> |
|
<span class="legend-color pending"></span> |
|
<span>未开始</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 甘特图主体 --> |
|
<div class="gantt-wrapper"> |
|
<!-- 左侧设备列表 --> |
|
<div class="device-list"> |
|
<div class="device-item device-item-title" :style="{ height: '36px' }">设备</div> |
|
<div |
|
v-for="(device, index) in devices" |
|
:key="index" |
|
class="device-item" |
|
:style="{ height: rowHeight + 'px' }" |
|
> |
|
{{ device }} |
|
</div> |
|
</div> |
|
|
|
<!-- 右侧时间轴 (时间在上,刻度线在下) --> |
|
<div class="timeline-container" @wheel.prevent="handleWheel"> |
|
<!-- 图表X轴区域(时间在上,刻度线在下) --> |
|
<div class="chart-axis"> |
|
<!-- 时间标签 --> |
|
<div class="time-labels" :style="{ width: `${timelineWidth}%` }"> |
|
<!-- 主刻度标签(小时) --> |
|
<div |
|
v-for="(time, index) in majorTickLabels" |
|
:key="index" |
|
class="major-label" |
|
:style="{ left: `${(index / 24) * 100}%` }" |
|
> |
|
{{ time }} |
|
</div> |
|
|
|
<!-- 副刻度标签(30分钟,放大后显示) --> |
|
<div v-if="zoomLevel >= 2" class="minor-labels"> |
|
<div |
|
v-for="(time, index) in minorTickLabels" |
|
:key="index" |
|
class="minor-label" |
|
:style="{ left: `${(index / (24 * 2)) * 100}%` }" |
|
> |
|
{{ time }} |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 刻度线(在下方) --> |
|
<div class="tick-lines" :style="{ width: `${timelineWidth}%` }"> |
|
<!-- 主刻度线(小时) --> |
|
<div |
|
v-for="(time, index) in majorTickLabels" |
|
:key="index" |
|
class="major-tick-line" |
|
:style="{ left: `${(index / 24) * 100}%` }" |
|
:title="time" |
|
></div> |
|
|
|
<!-- 副刻度线(30分钟,放大后显示) --> |
|
<div v-if="zoomLevel >= 2" class="minor-tick-lines"> |
|
<div |
|
v-for="(time, index) in minorTickLabels" |
|
:key="index" |
|
class="minor-tick-line" |
|
:style="{ left: `${(index / (24 * 2)) * 100}%` }" |
|
:title="time" |
|
></div> |
|
</div> |
|
|
|
<!-- X轴基线 --> |
|
<div class="axis-base-line"></div> |
|
</div> |
|
</div> |
|
|
|
<!-- 甘特图内容区域 --> |
|
<div class="chart-content" :style="{ width: `${timelineWidth}%` }"> |
|
<!-- 网格线 --> |
|
<div class="grid-lines"> |
|
<div |
|
v-for="(time, index) in majorTickLabels" |
|
:key="index" |
|
class="grid-line" |
|
:style="{ left: `${(index / 24) * 100}%` }" |
|
></div> |
|
</div> |
|
|
|
<!-- 任务容器 --> |
|
<div class="tasks-container"> |
|
<div |
|
v-for="(device, devIndex) in devices" |
|
:key="devIndex" |
|
class="device-task-row" |
|
:style="{ height: rowHeight + 'px' }" |
|
> |
|
<div |
|
v-for="(task, taskIndex) in getDeviceTasks(device)" |
|
:key="taskIndex" |
|
class="task-bar" |
|
:style="{ |
|
left: `${getPositionPercent(task.start)}%`, |
|
width: `${getWidthPercent(task.start, task.end)}%`, |
|
backgroundColor: getStatusColor(task.status), |
|
}" |
|
@mouseenter="showTooltip($event, task, device)" |
|
@mouseleave="hideTooltip()" |
|
> |
|
<span class="task-label">{{ task.task }}</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 悬浮提示框 --> |
|
<div |
|
v-if="tooltipVisible" |
|
class="tooltip" |
|
:style="{ |
|
left: `${tooltipX}px`, |
|
top: `${tooltipY}px`, |
|
}" |
|
> |
|
<div class="tooltip-content"> |
|
<div><strong>设备:</strong>{{ tooltipData.device }}</div> |
|
<div><strong>任务:</strong>{{ tooltipData.task }}</div> |
|
<div><strong>时间:</strong>{{ tooltipData.start }} - {{ tooltipData.end }}</div> |
|
<div><strong>状态:</strong>{{ tooltipData.status }}</div> |
|
</div> |
|
</div> |
|
</div> |
|
</basic-container> |
|
</template> |
|
|
|
<script> |
|
export default { |
|
name: 'GanttChart', |
|
data() { |
|
return { |
|
rowHeight: 36, |
|
zoomLevel: 1, // 缩放级别 (1-4) |
|
minZoom: 1, |
|
maxZoom: 4, |
|
|
|
// 设备列表 |
|
devices: [ |
|
'铜合金零件化学镀镍线(9652248)', |
|
'铜合金化学镀镍烤箱(9652248-01)', |
|
'铜合金化学镀镍烤箱(9652248-02)', |
|
'铜合金化学镀镍烤箱(9652248-03)', |
|
'铝合金化学镀镍生产线(9653582)', |
|
'铝合金化学镀镍烤箱(9653582-01)', |
|
'铝合金化学镀镍烤箱(9653582-02)', |
|
'铝合金化学镀镍烤箱(9653582-03)', |
|
'铝合金化学镀镍烤箱(9653582-04)', |
|
'镀金生产线(9652249)', |
|
'热表线烘箱(9652249-01)', |
|
'热表线烘箱(9652249-02)', |
|
'热表线烘箱(9652249-03)', |
|
'热表线烘箱(9652249-04)', |
|
'喷漆生产线(965396)', |
|
'喷码机(9652055)', |
|
'喷漆生产线(965396)', |
|
'喷码机(9652055)', |
|
], |
|
|
|
// 任务数据 |
|
taskData: [ |
|
{ |
|
device: '铜合金零件化学镀镍线(9652248)', |
|
task: 'WO-N261026761', |
|
start: '00:15', |
|
end: '08:45', |
|
status: '已完成', |
|
}, |
|
{ |
|
device: '铜合金零件化学镀镍线(9652248)', |
|
task: 'WO-N261026762', |
|
start: '09:30', |
|
end: '12:15', |
|
status: '已完成', |
|
}, |
|
{ |
|
device: '铜合金零件化学镀镍线(9652248)', |
|
task: 'WO-N261026764', |
|
start: '13:20', |
|
end: '16:50', |
|
status: '已完成', |
|
}, |
|
{ |
|
device: '铜合金零件化学镀镍线(9652248)', |
|
task: 'WO-N261026763', |
|
start: '16:00', |
|
end: '18:30', |
|
status: '进行中', |
|
}, |
|
{ |
|
device: '铜合金零件化学镀镍线(9652248)', |
|
task: 'WO-N2610287265', |
|
start: '19:10', |
|
end: '23:45', |
|
status: '未开始', |
|
}, |
|
|
|
{ |
|
device: '铜合金化学镀镍烤箱(9652248-01)', |
|
task: 'WO-N261026727', |
|
start: '09:15', |
|
end: '11:30', |
|
status: '已完成', |
|
}, |
|
{ |
|
device: '铜合金化学镀镍烤箱(9652248-01)', |
|
task: 'WO-N261026729', |
|
start: '12:20', |
|
end: '14:40', |
|
status: '已完成', |
|
}, |
|
{ |
|
device: '铜合金化学镀镍烤箱(9652248-01)', |
|
task: 'WO-N261026721', |
|
start: '15:50', |
|
end: '17:20', |
|
status: '进行中', |
|
}, |
|
{ |
|
device: '铜合金化学镀镍烤箱(9652248-01)', |
|
task: 'WO-N2610287244', |
|
start: '18:10', |
|
end: '20:30', |
|
status: '未开始', |
|
}, |
|
{ |
|
device: '铜合金化学镀镍烤箱(9652248-01)', |
|
task: 'WO-N261026778', |
|
start: '21:25', |
|
end: '23:55', |
|
status: '未开始', |
|
}, |
|
{ |
|
device: '铜合金化学镀镍烤箱(9652248-01)', |
|
task: 'WO-N2610287244', |
|
start: '18:10', |
|
end: '20:30', |
|
status: '未开始', |
|
}, |
|
{ |
|
device: '铜合金化学镀镍烤箱(9652248-01)', |
|
task: 'WO-N261026778', |
|
start: '21:25', |
|
end: '23:55', |
|
status: '未开始', |
|
}, |
|
], |
|
|
|
// 提示框相关 |
|
tooltipVisible: false, |
|
tooltipData: {}, |
|
tooltipX: 0, |
|
tooltipY: 0, |
|
option: { |
|
menuSpan: 4, |
|
submitBtn: false, |
|
emptyBtn: false, |
|
menuPosition: 'right', |
|
column: [ |
|
{ |
|
label: '设备', |
|
prop: 'name', |
|
span: 5, |
|
type: 'select', |
|
dicData: [ |
|
{ |
|
label: '车间订单', |
|
value: 1, |
|
}, |
|
{ |
|
label: '设备', |
|
value: 2, |
|
}, |
|
{ |
|
label: '班组', |
|
value: 3, |
|
}, |
|
], |
|
}, |
|
{ |
|
label: '车间订单号', |
|
prop: 'name', |
|
span: 5, |
|
}, |
|
{ |
|
label: '班组', |
|
prop: 'name', |
|
span: 5, |
|
type: 'select', |
|
dicData: [ |
|
{ |
|
label: '班组1', |
|
value: 1, |
|
}, |
|
{ |
|
label: '班组2', |
|
value: 2, |
|
}, |
|
{ |
|
label: '班组3', |
|
value: 3, |
|
}, |
|
], |
|
}, |
|
{ |
|
label: '时间', |
|
prop: 'name', |
|
span: 5, |
|
type: 'date', |
|
}, |
|
], |
|
}, |
|
}; |
|
}, |
|
computed: { |
|
// 时间轴总宽度 |
|
timelineWidth() { |
|
return 100 * this.zoomLevel; |
|
}, |
|
// 主刻度标签(小时) |
|
majorTickLabels() { |
|
return Array.from({ length: 24 }, (_, i) => `${i}:00`); |
|
}, |
|
// 副刻度标签(30分钟) |
|
minorTickLabels() { |
|
const labels = []; |
|
for (let hour = 0; hour < 24; hour++) { |
|
labels.push(''); // 小时位置留空(主刻度已显示) |
|
labels.push(`${hour}:30`); |
|
} |
|
return labels; |
|
}, |
|
}, |
|
methods: { |
|
// 根据设备筛选任务 |
|
getDeviceTasks(device) { |
|
return this.taskData.filter(task => task.device === device); |
|
}, |
|
|
|
// 时间转分钟数 |
|
timeToMinutes(timeStr) { |
|
const [hours, minutes] = timeStr.split(':').map(Number); |
|
return hours * 60 + minutes; |
|
}, |
|
|
|
// 计算任务起始位置百分比 |
|
getPositionPercent(startTime) { |
|
const totalMinutes = 24 * 60; |
|
const startMinutes = this.timeToMinutes(startTime); |
|
return (startMinutes / totalMinutes) * 100; |
|
}, |
|
|
|
// 计算任务宽度百分比 |
|
getWidthPercent(startTime, endTime) { |
|
const startMinutes = this.timeToMinutes(startTime); |
|
const endMinutes = this.timeToMinutes(endTime); |
|
const duration = endMinutes - startMinutes; |
|
return (duration / (24 * 60)) * 100; |
|
}, |
|
|
|
// 根据状态获取颜色 |
|
getStatusColor(status) { |
|
switch (status) { |
|
case '已完成': |
|
return '#28a745'; |
|
case '进行中': |
|
return '#007bff'; |
|
case '未开始': |
|
return '#6c757d'; |
|
default: |
|
return '#ccc'; |
|
} |
|
}, |
|
|
|
// 鼠标滚轮缩放 |
|
handleWheel(e) { |
|
if (e.deltaY < 0 && this.zoomLevel < this.maxZoom) { |
|
this.zoomLevel += 0.5; |
|
} else if (e.deltaY > 0 && this.zoomLevel > this.minZoom) { |
|
this.zoomLevel -= 0.5; |
|
} |
|
}, |
|
|
|
// 放大/缩小/重置 |
|
zoomIn() { |
|
if (this.zoomLevel < this.maxZoom) this.zoomLevel += 0.5; |
|
}, |
|
zoomOut() { |
|
if (this.zoomLevel > this.minZoom) this.zoomLevel -= 0.5; |
|
}, |
|
resetZoom() { |
|
this.zoomLevel = this.minZoom; |
|
}, |
|
|
|
// 提示框控制 |
|
showTooltip(e, task, device) { |
|
this.tooltipData = { ...task, device }; |
|
this.tooltipX = e.pageX + 10; |
|
this.tooltipY = e.pageY + 10; |
|
this.tooltipVisible = true; |
|
}, |
|
hideTooltip() { |
|
this.tooltipVisible = false; |
|
}, |
|
}, |
|
}; |
|
</script> |
|
|
|
<style scoped> |
|
.gantt-container { |
|
width: 100%; |
|
padding: 20px; |
|
box-sizing: border-box; |
|
font-family: Arial, sans-serif; |
|
} |
|
|
|
.gantt-header { |
|
height: 40px; |
|
} |
|
|
|
.zoom-controls { |
|
display: flex; |
|
gap: 10px; |
|
margin-bottom: 15px; |
|
padding-left: 265px; |
|
align-items: center; |
|
} |
|
|
|
.zoom-btn { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
padding: 4px 10px; |
|
background-color: #f1f5f9; |
|
border: 1px solid #ddd; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
font-size: 12px; |
|
transition: all 0.2s; |
|
} |
|
|
|
.zoom-btn:hover { |
|
background-color: #e2e8f0; |
|
} |
|
|
|
.zoom-info { |
|
font-size: 12px; |
|
color: #666; |
|
} |
|
|
|
.status-legend { |
|
display: flex; |
|
gap: 20px; |
|
float: right; |
|
} |
|
|
|
.legend-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
font-size: 14px; |
|
} |
|
|
|
.legend-color { |
|
display: inline-block; |
|
width: 12px; |
|
height: 12px; |
|
border-radius: 2px; |
|
} |
|
|
|
.legend-color.completed { |
|
background-color: #28a745; |
|
} |
|
.legend-color.processing { |
|
background-color: #007bff; |
|
} |
|
.legend-color.pending { |
|
background-color: #6c757d; |
|
} |
|
|
|
.gantt-wrapper { |
|
display: flex; |
|
height: calc(100% - 120px); |
|
border: 1px solid #eee; |
|
overflow: hidden; |
|
} |
|
|
|
.device-list { |
|
width: 250px; |
|
background-color: #f8f9fa; |
|
border-right: 1px solid #eee; |
|
/* overflow-y: auto; */ |
|
flex-shrink: 0; |
|
} |
|
|
|
.device-item { |
|
font-size: 16px; |
|
color: #333; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
position: relative; |
|
line-height: 36px; |
|
padding-left: 15px; |
|
padding-right: 15px; |
|
&::after { |
|
content: ''; |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 1px; |
|
background-color: #eee; |
|
} |
|
} |
|
.device-item-title { |
|
background: var(--el-color-primary); |
|
text-align: center; |
|
color: #fff; |
|
} |
|
|
|
.timeline-container { |
|
flex: 1; |
|
display: flex; |
|
flex-direction: column; |
|
overflow: auto; |
|
} |
|
|
|
/* 图表X轴区域样式(时间在上,刻度线在下) */ |
|
.chart-axis { |
|
height: 36px; |
|
position: relative; |
|
} |
|
|
|
/* 时间标签容器 */ |
|
.time-labels { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 30px; |
|
display: flex; |
|
background-color: var(--el-color-primary); |
|
} |
|
|
|
/* 主刻度标签(小时) */ |
|
.major-label { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
/* transform: translateX(-50%); */ |
|
font-size: 16px; |
|
color: #fff; |
|
font-weight: 500; |
|
white-space: nowrap; |
|
line-height: 30px; |
|
} |
|
|
|
/* 副刻度标签(30分钟) */ |
|
.minor-labels { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.minor-label { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
/* transform: translateX(-50%); */ |
|
font-size: 16px; |
|
color: #fff; |
|
white-space: nowrap; |
|
line-height: 30px; |
|
padding: 0 2px; |
|
} |
|
|
|
/* 刻度线容器(在下方) */ |
|
.tick-lines { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 6px; |
|
background-color: var(--el-color-primary); |
|
} |
|
|
|
/* 主刻度线(小时) */ |
|
.major-tick-line { |
|
position: absolute; |
|
bottom: 0; |
|
width: 1px; |
|
height: 4px; |
|
background-color: #fff; |
|
transform: translateX(-50%); |
|
} |
|
|
|
/* 副刻度线(30分钟) */ |
|
.minor-tick-lines { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.minor-tick-line { |
|
position: absolute; |
|
bottom: 0; |
|
width: 1px; |
|
height: 4px; |
|
background-color: #fff; |
|
transform: translateX(-50%); |
|
} |
|
|
|
/* X轴基线 */ |
|
.axis-base-line { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 1px; |
|
background-color: #dee2e6; |
|
} |
|
|
|
/* 图表内容区域 */ |
|
.chart-content { |
|
flex: 1; |
|
position: relative; |
|
overflow-y: auto; |
|
} |
|
|
|
/* 网格线样式 */ |
|
.grid-lines { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
pointer-events: none; |
|
} |
|
|
|
.grid-line { |
|
position: absolute; |
|
top: 0; |
|
bottom: 0; |
|
width: 0px; |
|
background-color: #e9ecef; |
|
} |
|
|
|
/* 任务容器 */ |
|
.tasks-container { |
|
position: relative; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.device-task-row { |
|
position: relative; |
|
border-bottom: 1px solid #e9ecef; |
|
box-sizing: border-box; |
|
} |
|
|
|
.task-bar { |
|
position: absolute; |
|
top: 5px; |
|
height: 26px; |
|
border-radius: 4px; |
|
display: flex; |
|
align-items: center; |
|
padding: 0 10px; |
|
box-sizing: border-box; |
|
cursor: pointer; |
|
overflow: hidden; |
|
transition: all 0.2s; |
|
} |
|
|
|
.task-bar:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.task-label { |
|
font-size: 12px; |
|
color: white; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
} |
|
|
|
/* 提示框 */ |
|
.tooltip { |
|
position: fixed; |
|
background-color: white; |
|
border: 1px solid #ddd; |
|
border-radius: 4px; |
|
padding: 10px; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
z-index: 1000; |
|
font-size: 12px; |
|
pointer-events: none; |
|
} |
|
|
|
.tooltip-content div { |
|
margin: 3px 0; |
|
} |
|
</style>
|
|
|