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.
1010 lines
28 KiB
1010 lines
28 KiB
<template> |
|
<!-- <basic-container> --> |
|
<div> |
|
<el-form label-width="80px" :model="formLabelAlign"> |
|
<el-row> |
|
<el-col :span="6"> |
|
<el-form-item label="维度:"> |
|
<el-select v-model="formLabelAlign.type" placeholder="请选择" @change="typeChange"> |
|
<el-option label="车间订单" value="1"> </el-option> |
|
<el-option label="班组" value="2"> </el-option> |
|
<el-option label="设备" value="3"> </el-option> |
|
</el-select> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6" v-if="formLabelAlign.type == '1'"> |
|
<el-form-item label="车间订单号:" label-width="120px"> |
|
<el-input v-model="formLabelAlign.woCode" placeholder="请输入"></el-input> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6" v-if="formLabelAlign.type == '2'"> |
|
<el-form-item label="班组:"> |
|
<!-- <el-input v-model="formLabelAlign.teamame" placeholder="请输入"></el-input> --> |
|
<el-select v-model="formLabelAlign.teamName" filterable placeholder="请选择"> |
|
<el-option |
|
v-for="(item, index) in selectTeamOptions" |
|
:label="item" |
|
:value="item" |
|
:key="index" |
|
> |
|
</el-option> |
|
<!-- <el-option label="班组2" value="2"> </el-option> --> |
|
</el-select> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6" v-if="formLabelAlign.type == '3'"> |
|
<el-form-item label="设备:"> |
|
<!-- <el-input v-model="formLabelAlign.equipName" placeholder="请输入"></el-input> --> |
|
<el-select v-model="formLabelAlign.equipName" filterable placeholder="请选择"> |
|
<el-option |
|
v-for="(item, index) in selectEquipOptions" |
|
:label="item" |
|
:value="item" |
|
:key="index" |
|
> |
|
</el-option> |
|
</el-select> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6"> |
|
<el-form-item label="时间:"> |
|
<el-date-picker |
|
v-model="formLabelAlign.startTime" |
|
type="date" |
|
value-format="YYYY-MM-DD" |
|
placeholder="选择日期" |
|
> |
|
</el-date-picker> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6"> |
|
<el-button type="primary" icon="el-icon-search" @click="handleSubmit"> 搜索 </el-button> |
|
<el-button icon="el-icon-delete" @click="handleSubmit"> 清空 </el-button> |
|
</el-col> |
|
</el-row> |
|
</el-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 |
|
v-if="searchType == '1'" |
|
class="device-item device-item-title" |
|
:style="{ height: '36px' }" |
|
> |
|
车间订单号 |
|
</div> |
|
<div |
|
v-if="searchType == '2'" |
|
class="device-item device-item-title" |
|
:style="{ height: '36px' }" |
|
> |
|
班组 |
|
</div> |
|
<div |
|
v-if="searchType == '3'" |
|
class="device-item device-item-title" |
|
:style="{ height: '36px' }" |
|
> |
|
设备 |
|
</div> |
|
<div |
|
v-for="(device, index) in devices" |
|
:key="index" |
|
:style="{ |
|
height: getRowHeight(device) + 'px', |
|
lineHeight: getRowHeight(device) + 'px', |
|
textAlign: 'center', |
|
borderBottom: '1px solid #ccc', |
|
}" |
|
:title="device" |
|
> |
|
{{ 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: getRowHeight(device) + 'px' }" |
|
> |
|
<template v-for="(layer, layerIndex) in getLayeredTasks(device)" :key="layerIndex"> |
|
<div |
|
v-for="(task, taskIndex) in layer" |
|
:key="taskIndex" |
|
class="task-bar" |
|
:style="{ |
|
left: `${getPositionPercent(task.startTime)}%`, |
|
width: `${getWidthPercent(task.startTime, task.endTime)}%`, |
|
backgroundColor: getStatusColor(task), |
|
top: `${getLayerOffset( |
|
layerIndex, |
|
getLayeredTasks(device).length, |
|
device |
|
)}px`, |
|
height: `${getLayerTaskHeight(getLayeredTasks(device).length, device)}px`, |
|
}" |
|
@mouseenter="showTooltip($event, task, device)" |
|
@mouseleave="hideTooltip()" |
|
> |
|
<!-- 任务标签内容不变 --> |
|
<span class="task-label" v-if="searchType == '1'">{{ task.processName }}</span> |
|
<span class="task-label" v-if="searchType == '2'">{{ task.woCode }}</span> |
|
<span class="task-label" v-if="searchType == '3'">{{ task.woCode }}</span> |
|
</div> |
|
</template> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 悬浮提示框 --> |
|
<div |
|
v-if="tooltipVisible" |
|
class="tooltip" |
|
:style="{ |
|
left: `${tooltipX}px`, |
|
top: `${tooltipY}px`, |
|
}" |
|
> |
|
<div class="tooltip-content"> |
|
<!-- 工单标题 --> |
|
<div class="wo-code-title" v-if="searchType == '1'">{{ tooltipData.woCode }}</div> |
|
<div class="wo-code-title" v-if="searchType == '2'">{{ tooltipData.teamName }}</div> |
|
<div class="wo-code-title" v-if="searchType == '3'">{{ tooltipData.equipName }}</div> |
|
<!-- 详情列表 --> |
|
<ul class="detail-list"> |
|
<li class="detail-item" v-if="searchType == '1'"> |
|
<span class="label">工序:</span> |
|
<span class="value">{{ tooltipData.processName || '-' }}</span> |
|
</li> |
|
<li class="detail-item" v-if="searchType == '1'"> |
|
<span class="label">班组:</span> |
|
<span class="value">{{ tooltipData.teamName || '-' }}</span> |
|
</li> |
|
<li class="detail-item" v-if="searchType == '2' || searchType == '3'"> |
|
<span class="label">车间订单号:</span> |
|
<span class="value">{{ tooltipData.woCode || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">零件号:</span> |
|
<span class="value">{{ tooltipData.partCode || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">流程卡号:</span> |
|
<span class="value">{{ tooltipData.cardNo || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">批次号:</span> |
|
<span class="value">{{ tooltipData.batchNo || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">当前工序:</span> |
|
<span class="value">{{ tooltipData.currentProcessName || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">数量:</span> |
|
<span class="value">{{ tooltipData.makeQty || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">计划开始时间:</span> |
|
<span class="value">{{ tooltipData.planStartTime || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">实际开始时间:</span> |
|
<span class="value">{{ tooltipData.factStartTime || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">计划完成时间:</span> |
|
<span class="value">{{ tooltipData.planEndTime || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">实际完成时间:</span> |
|
<span class="value">{{ tooltipData.factEndTime || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">状态:</span> |
|
<span class="value"> |
|
<el-tag v-if="searchType == '1'"> |
|
<i v-if="tooltipData.planStatus == '1'">未开始</i> |
|
<i v-if="tooltipData.planStatus == '2'">加工中</i> |
|
<i v-if="tooltipData.planStatus == '3'">报工完成</i> |
|
<i v-if="tooltipData.planStatus == '5'">已完成</i> |
|
<i v-if="tooltipData.planStatus == '6'">已返工</i> |
|
</el-tag> |
|
<el-tag v-if="searchType == '2' || searchType == '3'" :type="tooltipData.status"> |
|
<i v-if="tooltipData.orderStatus == '1'">未开始</i> |
|
<i v-if="tooltipData.orderStatus == '2'">进行中</i> |
|
<i v-if="tooltipData.orderStatus == '3'">已完成</i> |
|
</el-tag> |
|
</span> |
|
</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<!-- </basic-container> --> |
|
</template> |
|
|
|
<script> |
|
import { getData, selectEquip, selectTeam } from '@/api/productionSchedulingPlan/scheduling'; |
|
export default { |
|
name: 'GanttChart', |
|
data() { |
|
return { |
|
searchType: '', |
|
formLabelAlign: { |
|
type: '1', //维度类型 |
|
startTime: null, //时间 |
|
teamName: '', //班组 |
|
equipName: '', //设备 |
|
woCode: '', //车间订单号 |
|
}, |
|
rowHeight: 36, |
|
zoomLevel: 2, // 缩放级别 (1-4) |
|
minZoom: 1, |
|
maxZoom: 4, |
|
|
|
// 设备列表 |
|
devices: [], |
|
// 任务数据 |
|
taskData: [], |
|
|
|
// 提示框相关 |
|
tooltipVisible: false, |
|
tooltipData: {}, |
|
tooltipX: 0, |
|
tooltipY: 0, |
|
baseRowHeight: 36, // 基础行高(单任务时的高度) |
|
rowHeights: {}, // 存储每个设备的动态行高 |
|
selectTeamOptions: [], //班组列表 |
|
selectEquipOptions: [], //设备列表 |
|
}; |
|
}, |
|
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; |
|
}, |
|
}, |
|
mounted() { |
|
this.searchType = this.formLabelAlign.type; |
|
this.formLabelAlign.startTime = new Date().toISOString().substr(0, 10); |
|
this.getData(); |
|
this.getSelectTeam(); |
|
this.getSelectEquip() |
|
}, |
|
methods: { |
|
typeChange(){ |
|
this.formLabelAlign.teamName = '' |
|
this.formLabelAlign.equipName = '' |
|
this.formLabelAlign.woCode = '' |
|
}, |
|
getData() { |
|
getData(this.formLabelAlign).then(res => { |
|
this.processData(res.data.data); |
|
}); |
|
}, |
|
getSelectEquip() { |
|
selectEquip().then(res => { |
|
this.selectEquipOptions = res.data.data; |
|
}); |
|
}, |
|
getSelectTeam() { |
|
selectTeam().then(res => { |
|
this.selectTeamOptions = res.data.data; |
|
}); |
|
}, |
|
processData(rawData) { |
|
const tasks = []; |
|
const workOrders = Object.keys(rawData); |
|
|
|
// 遍历每个工单的任务 |
|
workOrders.forEach(woCode => { |
|
const woTasks = rawData[woCode] || []; |
|
woTasks.forEach(task => { |
|
tasks.push({ |
|
...task, |
|
status: this.calcTaskStatus(task.startTime, task.endTime), |
|
}); |
|
}); |
|
}); |
|
this.devices = workOrders; |
|
this.taskData = tasks; |
|
}, |
|
// 计算任务状态(已完成/进行中/未开始) |
|
calcTaskStatus(startTime, endTime) { |
|
const now = new Date(); |
|
const current = now.getHours() * 60 + now.getMinutes(); |
|
const start = this.timeToMinutes(startTime); |
|
let end = this.timeToMinutes(endTime); |
|
|
|
// 处理跨天(结束时间小于开始时间) |
|
if (end < start) { |
|
end += 24 * 60; |
|
} |
|
|
|
if (current >= end) { |
|
return '已完成'; |
|
} else if (current >= start) { |
|
return '进行中'; |
|
} else { |
|
return '未开始'; |
|
} |
|
}, |
|
// 判断任务状态 |
|
getTaskStatus(startTime, endTime) { |
|
const now = new Date(); |
|
const currentHours = now.getHours(); |
|
const currentMinutes = now.getMinutes(); |
|
const currentTotal = currentHours * 60 + currentMinutes; |
|
|
|
const startTotal = this.timeToMinutes(startTime); |
|
const endTotal = this.timeToMinutes(endTime); |
|
|
|
// 处理跨天情况(结束时间小于开始时间) |
|
if (endTotal < startTotal) { |
|
// 现在在[start, 24:00)或[00:00, end)之间为进行中 |
|
if (currentTotal >= startTotal || currentTotal < endTotal) { |
|
return '进行中'; |
|
} else if (currentTotal >= endTotal) { |
|
return '已完成'; |
|
} else { |
|
return '未开始'; |
|
} |
|
} else { |
|
// 正常时间段 |
|
if (currentTotal >= endTotal) { |
|
return '已完成'; |
|
} else if (currentTotal >= startTotal) { |
|
return '进行中'; |
|
} else { |
|
return '未开始'; |
|
} |
|
} |
|
}, |
|
handleSubmit() { |
|
this.searchType = this.formLabelAlign.type; |
|
this.devices = []; |
|
this.taskData = []; |
|
this.getData(); |
|
}, |
|
// 根据设备筛选任务 |
|
getDeviceTasks(device) { |
|
if (this.searchType == '1') { |
|
return this.taskData.filter(task => task.woCode === device); |
|
} |
|
if (this.searchType == '2') { |
|
return this.taskData.filter(task => task.teamName === device); |
|
} |
|
if (this.searchType == '3') { |
|
return this.taskData.filter(task => task.equipName === 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(row) { |
|
let staus = this.searchType == '1' ? row.planStatus : row.orderStatus; |
|
|
|
switch (staus) { |
|
case '3': |
|
return '#28a745'; |
|
case '2': |
|
return '#007bff'; |
|
case '1': |
|
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.tooltipVisible = true; // 先显示tooltip以便获取尺寸 |
|
|
|
this.$nextTick(() => { |
|
const tooltipEl = document.querySelector('.tooltip'); |
|
if (!tooltipEl) return; // 容错处理 |
|
|
|
const tooltipWidth = tooltipEl.offsetWidth; |
|
const tooltipHeight = tooltipEl.offsetHeight; |
|
const padding = 5; // 缩小边距,让tooltip更靠近鼠标 |
|
const mouseOffsetX = 8; // 水平偏移量(原10px改为8px) |
|
const mouseOffsetY = 8; // 垂直偏移量(原10px改为8px) |
|
|
|
// 初始位置(更靠近鼠标) |
|
let x = e.pageX + mouseOffsetX; |
|
let y = e.pageY + mouseOffsetY; |
|
|
|
// 右侧边界检测 |
|
if (x + tooltipWidth > document.documentElement.clientWidth) { |
|
x = e.pageX - tooltipWidth - mouseOffsetX; |
|
} |
|
|
|
// 底部边界检测 |
|
if (y + tooltipHeight > document.documentElement.clientHeight) { |
|
y = e.pageY - tooltipHeight - mouseOffsetY; |
|
} |
|
|
|
// 确保不超出顶部和左侧 |
|
x = Math.max(padding, x); |
|
y = Math.max(padding, y); |
|
|
|
this.tooltipX = x; |
|
this.tooltipY = y; |
|
}); |
|
}, |
|
hideTooltip() { |
|
this.tooltipVisible = false; |
|
}, |
|
// 计算每个设备的行高 |
|
getRowHeight(device) { |
|
const layers = this.getLayeredTasks(device); |
|
const layerCount = layers.length; |
|
// 行高 = 基础行高 * 层数(确保至少有基础行高) |
|
const height = Math.max(this.baseRowHeight, layerCount * this.baseRowHeight); |
|
// 缓存计算结果避免重复计算 |
|
this.rowHeights[device] = height; |
|
return height; |
|
}, |
|
|
|
// 修复层级偏移计算(基于实际行高) |
|
getLayerOffset(layerIndex, totalLayers, device) { |
|
const rowHeight = this.getRowHeight(device); |
|
if (totalLayers <= 1) return 2; // 单层级时的顶部间距 |
|
|
|
// 计算每层可用高度(减去总间距) |
|
const totalSpacing = totalLayers * 4; // 每层4px间距 |
|
const availableHeight = rowHeight - totalSpacing; |
|
const layerHeight = availableHeight / totalLayers; |
|
return layerIndex * (layerHeight + 4) + 2; // 2px顶部边距 |
|
}, |
|
|
|
// 修复任务高度计算(基于实际行高) |
|
getLayerTaskHeight(totalLayers, device) { |
|
const rowHeight = this.getRowHeight(device); |
|
if (totalLayers <= 1) { |
|
return rowHeight - 4; // 减去上下间距 |
|
} else { |
|
const totalSpacing = totalLayers * 4; |
|
const availableHeight = rowHeight - totalSpacing; |
|
return availableHeight / totalLayers; |
|
} |
|
}, |
|
|
|
// 优化层级计算逻辑 |
|
getLayeredTasks(device) { |
|
const tasks = this.getDeviceTasks(device); |
|
if (!tasks.length) return []; |
|
|
|
// 按开始时间排序,并处理跨天任务 |
|
const sortedTasks = [...tasks].sort((a, b) => { |
|
const aStart = this.timeToMinutes(a.startTime); |
|
const bStart = this.timeToMinutes(b.startTime); |
|
// 跨天任务(结束时间小于开始时间)排在前面 |
|
if (this.timeToMinutes(a.endTime) < aStart && this.timeToMinutes(b.endTime) >= bStart) { |
|
return -1; |
|
} |
|
return aStart - bStart; |
|
}); |
|
|
|
const layers = []; |
|
sortedTasks.forEach(task => { |
|
let placed = false; |
|
const taskStart = this.timeToMinutes(task.startTime); |
|
const taskEnd = this.timeToMinutes(task.endTime); |
|
|
|
// 处理跨天任务的结束时间(转换为第二天的分钟数) |
|
const adjustedEnd = taskEnd < taskStart ? taskEnd + 24 * 60 : taskEnd; |
|
|
|
for (let i = 0; i < layers.length; i++) { |
|
const lastTask = layers[i][layers[i].length - 1]; |
|
const lastEnd = this.timeToMinutes(lastTask.endTime); |
|
const lastAdjustedEnd = |
|
lastEnd < this.timeToMinutes(lastTask.startTime) ? lastEnd + 24 * 60 : lastEnd; |
|
|
|
if (taskStart >= lastAdjustedEnd) { |
|
layers[i].push(task); |
|
placed = true; |
|
break; |
|
} |
|
} |
|
if (!placed) { |
|
layers.push([task]); |
|
} |
|
}); |
|
|
|
return layers; |
|
}, |
|
}, |
|
}; |
|
</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: #284c89 !important; |
|
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: #284c89 !important; |
|
} |
|
|
|
/* 主刻度标签(小时) */ |
|
.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: #284c89 !important; |
|
} |
|
|
|
/* 主刻度线(小时) */ |
|
.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; |
|
padding: 0; |
|
margin: 0; |
|
} |
|
|
|
.task-bar { |
|
position: absolute; |
|
border-radius: 18px; |
|
display: flex; |
|
align-items: center; |
|
padding: 0 10px; |
|
box-sizing: border-box; |
|
cursor: pointer; |
|
overflow: hidden; |
|
transition: all 0.2s; |
|
white-space: nowrap; |
|
} |
|
|
|
.task-bar:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.task-label { |
|
font-size: 14px; |
|
color: white; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
} |
|
|
|
/* 提示框 */ |
|
.tooltip { |
|
position: fixed; |
|
background-color: white; |
|
border: 1px solid #ddd; |
|
border-radius: 4px; |
|
padding: 5px; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
z-index: 1000; |
|
font-size: 12px; |
|
pointer-events: none; |
|
/* z-index: 100; */ |
|
} |
|
|
|
.tooltip-content div { |
|
margin: 3px 0; |
|
} |
|
.wo-code-title { |
|
font-size: 18px; |
|
font-weight: bold; |
|
color: #1f2d3d; |
|
margin-bottom: 15px; |
|
padding-bottom: 10px; |
|
border-bottom: 1px solid #e6e6e6; |
|
} |
|
|
|
.detail-list { |
|
list-style: none; |
|
padding: 0; |
|
margin: 0; |
|
|
|
.detail-item { |
|
display: flex; |
|
align-items: center; |
|
margin-bottom: 5px; |
|
line-height: 20px; |
|
|
|
.label { |
|
width: 100px; |
|
color: #666; |
|
font-weight: 500; |
|
} |
|
|
|
.value { |
|
flex: 1; |
|
color: #333; |
|
} |
|
} |
|
} |
|
:deep(.el-button--primary) { |
|
background-color: #284c89 !important; |
|
color: #fff; |
|
} |
|
</style>
|
|
|