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.
1978 lines
56 KiB
1978 lines
56 KiB
<template> |
|
<div v-loading="loading"> |
|
<el-form label-width="80px" :model="formLabelAlign"> |
|
<el-row> |
|
<!-- 新增查询条件 --> |
|
<el-col :span="6"> |
|
<el-form-item label="班组:"> |
|
<el-select |
|
v-model="formLabelAlign.teamName" |
|
clearable |
|
filterable |
|
placeholder="请选择" |
|
size="medium" |
|
value-key="id" |
|
@change="teamChange" |
|
> |
|
<el-option |
|
v-for="(item, index) in selectTeamOptions" |
|
:label="item.tsName" |
|
:value="item" |
|
:key="item" |
|
></el-option> |
|
</el-select> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6"> |
|
<el-form-item label="设备:"> |
|
<el-select |
|
v-model="formLabelAlign.equipName" |
|
clearable |
|
filterable |
|
placeholder="请选择" |
|
size="medium" |
|
value-key="id" |
|
@change="equipChange" |
|
> |
|
<el-option |
|
v-for="(item, index) in selectEquipOptions" |
|
:label="item.deviceName" |
|
:value="item" |
|
:key="index" |
|
></el-option> |
|
</el-select> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6"> |
|
<el-form-item label="工序:"> |
|
<el-select |
|
v-model="formLabelAlign.processId" |
|
clearable |
|
filterable |
|
placeholder="请选择" |
|
size="medium" |
|
@change="processChange" |
|
value-key="id" |
|
> |
|
<el-option |
|
v-for="(item, index) in selectProcessOptions" |
|
:label="item.name" |
|
: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.timeRange" |
|
type="daterange" |
|
value-format="YYYY-MM-DD" |
|
format="YYYY-MM-DD" |
|
range-separator="至" |
|
start-placeholder="开始日期" |
|
end-placeholder="结束日期" |
|
size="medium" |
|
:clearable="false" |
|
:shortcuts="pickerOptions.shortcuts" |
|
></el-date-picker> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6"> |
|
<el-form-item label="零件号:"> |
|
<el-input v-model="formLabelAlign.partCode" placeholder="请输入"></el-input> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6"> |
|
<el-form-item label="批次号:"> |
|
<el-input v-model="formLabelAlign.batchNo" placeholder="请输入"></el-input> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6"> |
|
<el-form-item label="接收时间:"> |
|
<el-date-picker |
|
v-model="formLabelAlign.receiveTime" |
|
type="date" |
|
value-format="YYYY-MM-DD" |
|
format="YYYY-MM-DD" |
|
placeholder="请选择" |
|
size="medium" |
|
:clearable="true" |
|
style="width: 100%" |
|
></el-date-picker> |
|
</el-form-item> |
|
</el-col> |
|
<el-col :span="6"> |
|
<div style="float: right"> |
|
<el-button type="primary" icon="el-icon-search" @click="handleSubmit" size="medium"> |
|
搜索 |
|
</el-button> |
|
<el-button icon="el-icon-delete" @click="handleReset" size="medium"> 清空 </el-button> |
|
<el-button type="primary" size="medium" @click="exportXls">导出</el-button> |
|
</div> |
|
</el-col> |
|
</el-row> |
|
</el-form> |
|
|
|
<div class="gantt-container"> |
|
<!-- 头部标题和图例 --> |
|
<div class="gantt-header"> |
|
<div class="status-legend"> |
|
<div class="legend-item"> |
|
<el-checkbox |
|
v-model="legendStatus.pending" |
|
:style="{ '--checkbox-color': '#6c757d' }" |
|
@change="handleLegendChange('pending')" |
|
> |
|
<span class="legend-text" style="color: #6c757d">未开始</span> |
|
</el-checkbox> |
|
</div> |
|
|
|
<div class="legend-item"> |
|
<el-checkbox |
|
v-model="legendStatus.processing" |
|
:style="{ '--checkbox-color': '#28a745' }" |
|
@change="handleLegendChange('processing')" |
|
> |
|
<span class="legend-text" style="color: #28a745">进行中</span> |
|
</el-checkbox> |
|
</div> |
|
<div class="legend-item"> |
|
<el-checkbox |
|
v-model="legendStatus.completed" |
|
:style="{ '--checkbox-color': '#007bff' }" |
|
@change="handleLegendChange('completed')" |
|
> |
|
<span class="legend-text" style="color: #007bff">已完成</span> |
|
</el-checkbox> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 甘特图主体 --> |
|
<div class="gantt-wrapper"> |
|
<!-- 左侧信息列表 - 展示车间订单号等信息 --> |
|
<div class="info-list" :style="{ height: ganttWrapperHeightLeft }"> |
|
<div class="info-item-title"> |
|
<div class="info-title-cell info-title-num">#</div> |
|
<div class="info-title-cell">订单信息</div> |
|
</div> |
|
<div class="info-container" ref="leftScrollContainer"> |
|
<div |
|
v-for="(order, index) in currentPageOrders" |
|
:key="index" |
|
:style="{ |
|
height: getRowHeight(order.woCode) + 'px', |
|
borderBottom: '1px solid #ccc', |
|
lineHeight: getRowHeight(order.woCode) + 'px', |
|
}" |
|
> |
|
<div class="info-item"> |
|
<div class="info-cell info-title-num">{{ index + 1 }}</div> |
|
<el-row> |
|
<el-col :span="24"> |
|
<div class="info-item-txt info-item-info"> |
|
<span class="order-code" style="font-weight: 600">{{ order.woCode }}</span> |
|
<span class="part-code">{{ order.partCode }}</span> |
|
</div> |
|
</el-col> |
|
<el-col :span="24"> |
|
<div class="info-item-txt info-item-orderInfo"> |
|
<div class="info-item-content"> |
|
<span class="batch-no">{{ order.batchNo }}</span> |
|
<span class="make-qty">{{ order.makeQty }}</span> |
|
<span class="product-ident">{{ order.productIdent }}</span> |
|
<span v-if="order.priorityAps != ''" class="priority-aps">{{ |
|
order.priorityAps |
|
}}</span> |
|
</div> |
|
</div> |
|
</el-col> |
|
</el-row> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 右侧时间轴 @wheel.prevent="handleWheel"--> |
|
<div |
|
class="timeline-container" |
|
ref="timelineContainer" |
|
:style="{ height: ganttWrapperHeight }" |
|
> |
|
<!-- 图表X轴区域 --> |
|
<div class="chart-axis"> |
|
<!-- 日期行 --> |
|
<div class="date-labels" :style="{ width: `${timelineWidth}%` }"> |
|
<template v-for="(dateInfo, dateIndex) in dateLabels" :key="dateIndex"> |
|
<div |
|
class="date-label" |
|
:style="{ |
|
left: `${dateInfo.position}%`, |
|
width: |
|
dateIndex < dateLabels.length - 1 |
|
? `${dateLabels[dateIndex + 1].position - dateInfo.position}%` |
|
: `${100 - dateInfo.position}%`, |
|
borderRight: dateIndex < dateLabels.length - 1 ? '1px solid #fff' : 'none', |
|
}" |
|
> |
|
<div class="date-main">{{ dateInfo.label }}</div> |
|
</div> |
|
</template> |
|
</div> |
|
|
|
<!-- 时间标签行 --> |
|
<div class="time-labels" :style="{ width: `${timelineWidth}%` }"> |
|
<div |
|
v-for="(time, index) in majorTickLabels" |
|
:key="index" |
|
class="major-label" |
|
:style="{ left: `${time.position}%` }" |
|
> |
|
{{ formatHourLabel(time.label) }} |
|
</div> |
|
<div v-if="zoomLevel >= 1.5" class="minor-labels"> |
|
<div |
|
v-for="(time, index) in minorTickLabels" |
|
:key="'minor-' + index" |
|
class="minor-label" |
|
:style="{ left: `${time.position}%` }" |
|
> |
|
{{ time.label }} |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 刻度线 --> |
|
<div class="tick-lines" :style="{ width: `${timelineWidth}%` }"> |
|
<div |
|
v-for="(time, index) in majorTickLabels" |
|
:key="'tick-' + index" |
|
class="major-tick-line" |
|
:style="{ left: `${time.position || (index / 72) * 100}%` }" |
|
></div> |
|
<div v-if="zoomLevel >= 1.5" class="minor-tick-lines"> |
|
<div |
|
v-for="(time, index) in minorTickLabels" |
|
:key="'minor-tick-' + index" |
|
class="minor-tick-line" |
|
:style="{ left: `${time.position}%` }" |
|
></div> |
|
</div> |
|
<div class="axis-base-line"></div> |
|
|
|
<!-- 当前时间线 --> |
|
<div |
|
class="current-time-line" |
|
:style="{ left: `${currentTimePosition}%` }" |
|
v-if="currentTimePosition >= 0" |
|
> |
|
<div class="current-time-indicator">现在</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 甘特图内容区域 --> |
|
<div |
|
class="chart-content" |
|
ref="timelineContainerTest" |
|
:style="{ width: `${timelineWidth}%` }" |
|
> |
|
<div class="grid-lines"> |
|
<div |
|
v-for="(time, index) in majorTickLabels" |
|
:key="index" |
|
class="grid-line" |
|
:style="{ left: `${time.position}%` }" |
|
></div> |
|
</div> |
|
|
|
<div class="tasks-container"> |
|
<div |
|
v-for="(order, index) in currentPageOrders" |
|
:key="index" |
|
class="device-task-row" |
|
:style="{ height: getRowChartHeight(order.woCode) + 'px' }" |
|
> |
|
<template |
|
v-for="(layer, layerIndex) in getLayeredTasks(order.woCode)" |
|
:key="layerIndex" |
|
> |
|
<div |
|
v-for="(task, taskIndex) in layer" |
|
:key="taskIndex" |
|
class="task-bar" |
|
:class="{ |
|
'task-bar-narrow': getWidthPercent(task.planStartTime, task.planEndTime) < 2.1, |
|
}" |
|
:style="{ |
|
left: `${getPositionPercent(task.planStartTime)}%`, |
|
width: `${getWidthPercent(task.planStartTime, task.planEndTime)}%`, |
|
backgroundColor: getStatusColor(task), |
|
top: `${getLayerOffset( |
|
layerIndex, |
|
getLayerTaskHeight(getLayeredTasks(order.woCode).length, order.woCode), |
|
order.woCode |
|
)}px`, |
|
height: `${getLayerTaskHeight( |
|
getLayeredTasks(order.woCode).length, |
|
order.woCode |
|
)}px`, |
|
}" |
|
@mouseenter="showTooltip($event, task, order.woCode)" |
|
@mouseleave="hideTooltip()" |
|
> |
|
|
|
<div class="task-label" v-if="getWidthPercent(task.planStartTime, task.planEndTime) >= 2.1&&task.processName.length<4"> |
|
<span |
|
class="task-label-txt task-label-txt-inside" |
|
>{{ task.processName }}</span |
|
> |
|
</div> |
|
|
|
<!-- 为窄任务添加带偏移的文本显示 --> |
|
<div |
|
v-else |
|
class="task-overlay-text" |
|
:style="{ |
|
top: `${getNarrowTaskOffsetByLayer(layerIndex)}px`, |
|
left: `${getNarrowTaskOffsetByLayerLeft(task.processName)}px`, |
|
}" |
|
> |
|
{{ task.processName }} |
|
</div> |
|
</div> |
|
</template> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 分页控件 :page-sizes="[10, 20, 50]" ,sizes--> |
|
<div class="pagination-container"> |
|
<el-pagination |
|
@size-change="handleSizeChange" |
|
@current-change="handleCurrentChange" |
|
:current-page="currentPage" |
|
:page-size="pageSize" |
|
:page-sizes="[10, 20, 50]" |
|
layout="total,sizes, prev, pager, next, jumper" |
|
:total="totalOrders" |
|
></el-pagination> |
|
</div> |
|
|
|
<!-- 悬浮提示框 --> |
|
<div |
|
v-if="tooltipVisible" |
|
class="tooltip" |
|
:style="{ |
|
left: `${tooltipX}px`, |
|
top: `${tooltipY}px`, |
|
}" |
|
> |
|
<div class="tooltip-content"> |
|
<div class="wo-code-title">{{ tooltipData.woCode }}</div> |
|
<ul class="detail-list"> |
|
<li class="detail-item"> |
|
<span class="label">工序:</span> |
|
<span class="value">{{ tooltipData.processName || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">班组:</span> |
|
<span class="value">{{ tooltipData.teamName || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">设备:</span> |
|
<span class="value">{{ tooltipData.equipName || '-' }}</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.batchNo || '-' }}</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.planEndTime || '-' }}</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.factEndTime || '-' }}</span> |
|
</li> |
|
<li class="detail-item"> |
|
<span class="label">工序状态:</span> |
|
<span class="value"> |
|
<el-tag :type="getStatusTagType(tooltipData)"> |
|
<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> |
|
</span> |
|
</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</template> |
|
|
|
<script> |
|
import { getData, selectEquip, selectTeam } from '@/api/productionSchedulingPlan/scheduling'; |
|
import { |
|
getProcessSet, |
|
exportBlob, |
|
getEquipment, |
|
getTeamSet, |
|
} from '@/api/productionSchedulingPlan/basic'; |
|
import { downloadXls } from '@/utils/util'; |
|
// import { exportBlob } from '@/api/common'; |
|
export default { |
|
name: 'GanttChart', |
|
data() { |
|
return { |
|
valuerrr:'铜合金化学镀镍',// |
|
loading: false, |
|
formLabelAlign: { |
|
startTime: null, //时间 |
|
teamName: '', //班组 |
|
teamId: '', //班组 |
|
equipName: '', //设备 |
|
equipId: '', //设备 |
|
processName: '', //工序 |
|
processId: '', //工序 |
|
woCode: '', //车间订单号 |
|
timeRange: [], //时间范围 |
|
receiveTime: '', //接收时间 |
|
partCode: '', //订单号 |
|
batchNo: '', //批次号 |
|
planStatusList: [], //工序状态 |
|
}, |
|
zoomLevel: 1, // 缩放级别 (1-4) |
|
minZoom: 1, |
|
maxZoom: 4, |
|
|
|
// 订单列表和任务数据 |
|
allOrders: [], // 所有订单信息 |
|
currentPageOrders: [], // 当前页订单 |
|
taskData: [], |
|
totalOrders: 0, |
|
|
|
// 分页参数 |
|
currentPage: 1, |
|
pageSize: 10, |
|
|
|
// 提示框相关 |
|
tooltipVisible: false, |
|
tooltipData: {}, |
|
tooltipX: 0, |
|
tooltipY: 0, |
|
|
|
// 样式相关 |
|
baseRowHeight: 50, |
|
baseRowChartHeight: 50, |
|
rowHeights: {}, |
|
selectTeamOptions: [], |
|
selectEquipOptions: [], |
|
selectProcessOptions: [], |
|
|
|
// 当前时间位置 |
|
currentTimePosition: 0, |
|
|
|
timelineWidth: 300, // 3天的宽度 (每天100%) |
|
baseStartTime: null, // 基准开始时间 |
|
currentViewStartTime: null, // 当前视图开始时间 |
|
currentViewEndTime: null, // 当前视图结束时间 |
|
|
|
legendStatus: { |
|
completed: false, |
|
processing: true, |
|
pending: true, |
|
}, |
|
handleRightScrollBound: null, |
|
handleLeftScrollBound: null, |
|
pickerOptions: { |
|
shortcuts: [ |
|
{ |
|
text: '最近三天', |
|
value: [new Date(), new Date(new Date().getTime() + 3 * 24 * 60 * 60 * 1000)], |
|
}, |
|
{ |
|
text: '最近七天', |
|
value: [new Date(), new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000)], |
|
}, |
|
], |
|
}, |
|
}; |
|
}, |
|
watch: { |
|
// 监听参数变化 |
|
'$route.query': { |
|
handler(newQuery) { |
|
this.handleParamsChange(); |
|
}, |
|
immediate: true, |
|
}, |
|
}, |
|
computed: { |
|
// 通过 Vue Router 获取查询参数 |
|
tsId() { |
|
return this.$route.query.tsId || ''; |
|
}, |
|
tsName() { |
|
return this.$route.query.tsName || ''; |
|
}, |
|
// 计算基准开始时间(今天0点) |
|
baseDate() { |
|
if (this.formLabelAlign.timeRange && this.formLabelAlign.timeRange.length === 2) { |
|
const start = new Date(this.formLabelAlign.timeRange[0]); |
|
start.setHours(0, 0, 0, 0); |
|
return start; |
|
} |
|
// 默认显示当前日期 |
|
const now = new Date(); |
|
const base = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); |
|
return base; |
|
}, |
|
// 计算结束日期 |
|
endDate() { |
|
if (this.formLabelAlign.timeRange && this.formLabelAlign.timeRange.length === 2) { |
|
const end = new Date(this.formLabelAlign.timeRange[1]); |
|
end.setHours(23, 59, 59, 999); |
|
return end; |
|
} |
|
// 默认显示当前日期+2天 |
|
const now = new Date(); |
|
const base = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2, 23, 59, 59, 999); |
|
return base; |
|
}, |
|
// 计算总小时数 |
|
totalHours() { |
|
const diffTime = this.endDate - this.baseDate; |
|
return Math.ceil(diffTime / (1000 * 60 * 60)); // 计算总小时数 |
|
}, |
|
|
|
// 计算当前视图开始时间(当前小时) |
|
viewStartTime() { |
|
return this.baseDate; |
|
}, |
|
|
|
// 生成3天的时间刻度 |
|
timelineHours() { |
|
const hours = []; |
|
const start = new Date(this.baseDate); |
|
|
|
for (let i = 0; i < this.totalHours; i++) { |
|
const hour = new Date(start); |
|
hour.setHours(start.getHours() + i); |
|
hours.push(hour); |
|
} |
|
return hours; |
|
}, |
|
|
|
// 生成主要时间标签(每小时) |
|
majorTickLabels() { |
|
const labels = []; |
|
const start = new Date(this.baseDate); |
|
const end = new Date(this.endDate); |
|
|
|
for (let i = 0; i < this.totalHours; i++) { |
|
const hour = new Date(start); |
|
hour.setHours(start.getHours() + i); |
|
|
|
// 检查时间是否超出结束时间 |
|
if (hour > end) { |
|
break; // 如果超出结束时间则停止添加 |
|
} |
|
|
|
// 计算时间点在时间轴上的位置百分比 |
|
const timeDiff = hour - this.baseDate; |
|
const totalDuration = this.totalHours * 60 * 60 * 1000; // 总毫秒数 |
|
const positionPercent = (timeDiff / totalDuration) * 100; |
|
|
|
labels.push({ |
|
time: hour, |
|
label: `${hour.getMonth() + 1}-${hour.getDate()} ${hour.getHours()}:00`, |
|
position: Math.min(100, positionPercent), // 确保不超过100% |
|
}); |
|
} |
|
return labels; |
|
}, |
|
|
|
// 生成日期标签 |
|
dateLabels() { |
|
const dates = []; |
|
const start = new Date(this.baseDate); |
|
const end = new Date(this.endDate); |
|
|
|
// 计算总天数 |
|
const totalDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)); |
|
|
|
for (let i = 0; i <= totalDays; i++) { |
|
const date = new Date(start); |
|
date.setDate(start.getDate() + i); |
|
|
|
// 检查日期是否超出结束日期 |
|
if (date > end) { |
|
break; // 如果超出结束日期则停止添加 |
|
} |
|
|
|
// 计算每个日期在时间轴上的开始位置百分比 |
|
const timeDiff = date - this.baseDate; |
|
const totalDuration = this.totalHours * 60 * 60 * 1000; // 总毫秒数 |
|
const positionPercent = (timeDiff / totalDuration) * 100; |
|
|
|
// 确保位置在有效范围内 |
|
if (positionPercent <= 100) { |
|
dates.push({ |
|
date: date, |
|
position: Math.max(0, Math.min(100, positionPercent)), |
|
label: `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`, // 显示年/月/日 |
|
dayName: this.getDayName(date.getDay()), |
|
}); |
|
} |
|
} |
|
return dates; |
|
}, |
|
|
|
// 生成次要时间标签(每半小时) |
|
minorTickLabels() { |
|
const labels = []; |
|
const start = new Date(this.baseDate); |
|
const end = new Date(this.endDate); // 添加结束时间变量 |
|
const totalHalfHours = this.totalHours * 2; // 每小时2个刻度 |
|
|
|
for (let i = 0; i < totalHalfHours; i++) { |
|
const halfHour = new Date(start); |
|
halfHour.setHours(start.getHours() + Math.floor(i / 2), (i % 2) * 30, 0, 0); |
|
|
|
// 检查时间是否超出结束时间 |
|
if (halfHour > end) { |
|
break; // 如果超出结束时间则停止添加 |
|
} |
|
|
|
// 计算时间点在时间轴上的位置百分比 |
|
const timeDiff = halfHour - this.baseDate; |
|
const totalDuration = this.totalHours * 60 * 60 * 1000; // 总毫秒数 |
|
const positionPercent = (timeDiff / totalDuration) * 100; |
|
|
|
if (i % 2 === 1) { |
|
// 只显示半小时标记 |
|
labels.push({ |
|
time: halfHour, |
|
label: `${halfHour.getMonth() + 1}-${halfHour.getDate()} ${halfHour.getHours()}:30`, |
|
position: Math.min(100, positionPercent), // 确保不超过100% |
|
}); |
|
} |
|
} |
|
return labels; |
|
}, |
|
|
|
// 当前时间位置 |
|
currentTimePosition() { |
|
const now = new Date(); |
|
const viewStart = this.baseDate; |
|
const viewEnd = this.endDate; |
|
|
|
// 如果当前时间不在显示范围内,返回-1表示不显示 |
|
if (now < viewStart || now > viewEnd) { |
|
return -1; |
|
} |
|
|
|
const diffMs = now - viewStart; |
|
const totalDuration = this.totalHours * 60 * 60 * 1000; |
|
const positionPercent = (diffMs / totalDuration) * 100; |
|
|
|
return Math.max(0, Math.min(100, positionPercent)); |
|
}, |
|
// 动态计算时间轴宽度 |
|
timelineWidth() { |
|
// 根据时间范围动态调整时间轴宽度 |
|
return Math.max(100, this.totalHours * (100 / 24)); // 每24小时为100%宽度,最小为100% |
|
}, |
|
// 右侧动态计算甘特图容器高度 |
|
ganttWrapperHeightLeft() { |
|
if (this.$route.path == '/productionSchedulingPlan/schedulingDashboard/index') { |
|
// 根据实际页面元素计算可用高度 |
|
return `calc(100vh - 250px - 120px - 40px)`; // 调整数值以适应您的页面布局 |
|
} else { |
|
// 根据实际页面元素计算可用高度 |
|
return `calc(100vh - 250px)`; // 调整数值以适应您的页面布局 |
|
} |
|
}, |
|
// 动态计算甘特图容器高度 |
|
ganttWrapperHeight() { |
|
if (this.$route.path == '/productionSchedulingPlan/schedulingDashboard/index') { |
|
// 根据实际页面元素计算可用高度 |
|
return `calc(100vh - 250px - 120px - 40px)`; // 调整数值以适应您的页面布局 |
|
} else { |
|
// 根据实际页面元素计算可用高度 |
|
return `calc(100vh - 250px)`; // 调整数值以适应您的页面布局 |
|
} |
|
}, |
|
}, |
|
|
|
mounted() { |
|
this.getSelectTeam(); |
|
this.getSelectEquip(); |
|
this.getProcessSet(); |
|
// this.updateTime(); |
|
this.updateTimeAxis(); |
|
this.$nextTick(() => { |
|
this.calcCurrentTimePosition(); |
|
// 添加滚动事件监听器 |
|
if (this.$refs.timelineContainerTest) { |
|
this.$refs.timelineContainerTest.addEventListener('scroll', this.handleRightScrollBound); |
|
} |
|
if (this.$refs.leftScrollContainer) { |
|
this.$refs.leftScrollContainer.addEventListener('scroll', this.handleLeftScrollBound); |
|
} |
|
}); |
|
|
|
// 定时更新当前时间线位置 |
|
// setInterval(() => { |
|
// this.calcCurrentTimePosition(); |
|
// }, 60000); |
|
}, |
|
created() { |
|
// 绑定方法,确保this指向正确 |
|
this.handleRightScrollBound = this.handleRightScroll.bind(this); |
|
this.handleLeftScrollBound = this.handleLeftScroll.bind(this); |
|
}, |
|
beforeDestroy() { |
|
// 移除事件监听器 |
|
if (this.$refs.timelineContainerTest) { |
|
this.$refs.timelineContainerTest.removeEventListener('scroll', this.handleRightScrollBound); |
|
} |
|
if (this.$refs.leftScrollContainer) { |
|
this.$refs.leftScrollContainer.removeEventListener('scroll', this.handleLeftScrollBound); |
|
} |
|
}, |
|
methods: { |
|
// 根据层索引确定窄任务文本的垂直偏移 |
|
getNarrowTaskOffsetByLayer(layerIndex) { |
|
// 偶数层在上方显示(负值),奇数层在下方显示(正值) |
|
const offset = layerIndex % 2 === 0 ? -20 : 15; // -20px 在上方,5px 在下方 |
|
return offset; |
|
}, |
|
// 根据层索引确定窄任务文本的垂直偏移 |
|
getNarrowTaskOffsetByLayerLeft(processName) { |
|
// 偶数层在上方显示(负值),奇数层在下方显示(正值) |
|
// const offset = processName.length >= 5 ? -30 : -15; |
|
const offset = null; |
|
if (processName.length >= 7) { |
|
return -33; |
|
} |
|
if (processName.length >= 6) { |
|
return -20; |
|
} |
|
if (processName.length >= 5) { |
|
return -17; |
|
} |
|
return -7 |
|
}, |
|
handleParamsChange() { |
|
// 参数变化时的处理逻辑 |
|
this.formLabelAlign.teamId = this.tsId; |
|
this.formLabelAlign.teamName = this.tsName; |
|
this.updateTime(); |
|
// 根据参数加载数据 |
|
this.getData(); |
|
}, |
|
// 计算重叠的窄任务的垂直偏移量 |
|
getNarrowTaskOffset(orderWoCode, taskIndex, layerIndex) { |
|
const tasks = this.getDeviceTasks(orderWoCode); |
|
const currentTask = tasks[taskIndex]; |
|
|
|
// 获取当前任务的时间段 |
|
const currentStart = this.parseTimeToHours(currentTask.planStartTime); |
|
const currentEnd = this.parseTimeToHours(currentTask.planEndTime); |
|
const currentWidth = this.getWidthPercent(currentTask.planStartTime, currentTask.planEndTime); |
|
|
|
// 如果宽度大于等于1%,不需要偏移 |
|
if (currentWidth >= 1) { |
|
return 0; |
|
} |
|
|
|
// 查找在同一时间段内的其他窄任务 |
|
const overlappingTasks = []; |
|
for (let i = 0; i < tasks.length; i++) { |
|
const task = tasks[i]; |
|
const taskWidth = this.getWidthPercent(task.planStartTime, task.planEndTime); |
|
|
|
if (taskWidth < 1) { |
|
// 只考虑窄任务 |
|
const taskStart = this.parseTimeToHours(task.planStartTime); |
|
const taskEnd = this.parseTimeToHours(task.planEndTime); |
|
|
|
// 检查时间是否重叠(考虑浮点数精度) |
|
if (Math.max(currentStart, taskStart) <= Math.min(currentEnd, taskEnd) + 0.01) { |
|
overlappingTasks.push({ index: i, task, start: taskStart, end: taskEnd }); |
|
} |
|
} |
|
} |
|
|
|
// 按开始时间排序 |
|
overlappingTasks.sort((a, b) => a.start - b.start); |
|
|
|
// 创建层来分配任务,避免同一层中的任务重叠 |
|
const layers = []; |
|
for (const overlapTask of overlappingTasks) { |
|
let assigned = false; |
|
|
|
// 尝试将任务分配到现有层 |
|
for (let layerIdx = 0; layerIdx < layers.length; layerIdx++) { |
|
const lastTask = layers[layerIdx][layers[layerIdx].length - 1]; |
|
// 检查是否与该层最后一个任务重叠 |
|
if ( |
|
Math.max(lastTask.start, overlapTask.start) > Math.min(lastTask.end, overlapTask.end) |
|
) { |
|
// 不重叠,可以分配到这一层 |
|
layers[layerIdx].push(overlapTask); |
|
assigned = true; |
|
break; |
|
} |
|
} |
|
|
|
// 如果没有合适的层,创建新层 |
|
if (!assigned) { |
|
layers.push([overlapTask]); |
|
} |
|
} |
|
|
|
// 找到当前任务所在的层索引 |
|
const currentTaskLayerIndex = layers.findIndex(layer => |
|
layer.some(item => item.index === taskIndex) |
|
); |
|
|
|
// 根据层索引返回偏移量 |
|
const offsetStep = 20; // 每个任务之间的偏移量 |
|
return currentTaskLayerIndex * offsetStep; |
|
}, |
|
|
|
// 检查任务是否为窄任务 |
|
isNarrowTask(task) { |
|
return this.getWidthPercent(task.planStartTime, task.planEndTime) < 1; |
|
}, |
|
// 右侧滚动时同步左侧滚动 |
|
handleRightScroll(event) { |
|
if (this.$refs.leftScrollContainer) { |
|
const rightScrollTop = event.target.scrollTop; |
|
this.$refs.leftScrollContainer.scrollTop = rightScrollTop; |
|
} |
|
}, |
|
|
|
// 左侧滚动时同步右侧滚动 |
|
handleLeftScroll(event) { |
|
if (this.$refs.timelineContainerTest) { |
|
const leftScrollTop = event.target.scrollTop; |
|
this.$refs.timelineContainerTest.scrollTop = leftScrollTop; |
|
} |
|
}, |
|
updateTime() { |
|
// 设置默认时间范围为今天到后两天 |
|
const today = new Date(); |
|
const endDay = new Date(today); |
|
endDay.setDate(today.getDate() + 2); |
|
// 设置日期范围,格式为 YYYY-MM-DD |
|
const startDate = today.toISOString().split('T')[0]; |
|
const endDate = endDay.toISOString().split('T')[0]; |
|
|
|
this.formLabelAlign.timeRange = [startDate, endDate]; |
|
this.handleLegendChange(); |
|
}, |
|
// 处理图例状态变化 |
|
handleLegendChange() { |
|
// 如果需要重新加载数据,可以调用 |
|
this.formLabelAlign.planStatusList = []; |
|
if (this.legendStatus.completed) { |
|
this.formLabelAlign.planStatusList.push('5'); |
|
} |
|
if (this.legendStatus.processing) { |
|
this.formLabelAlign.planStatusList.push('2', '3'); |
|
} |
|
if (this.legendStatus.pending) { |
|
this.formLabelAlign.planStatusList.push('1'); |
|
} |
|
|
|
this.getData(); |
|
}, |
|
// 获取星期名称 |
|
getDayName(dayIndex) { |
|
const days = ['', '一', '二', '三', '四', '五', '六']; |
|
return `星期${days[dayIndex]}`; |
|
}, |
|
processChange(val) { |
|
if (val) { |
|
this.formLabelAlign.processName = val.name; |
|
} else { |
|
this.formLabelAlign.processName = ''; |
|
} |
|
}, |
|
equipChange(val) { |
|
if (val) { |
|
this.formLabelAlign.equipName = val.deviceName; |
|
} else { |
|
this.formLabelAlign.equipName = ''; |
|
} |
|
}, |
|
teamChange(val) { |
|
if (val) { |
|
this.formLabelAlign.teamName = val.tsName; |
|
} else { |
|
this.formLabelAlign.teamName = ''; |
|
} |
|
}, |
|
|
|
// 导出 |
|
exportXls() { |
|
exportBlob(`/blade-scheduling/workOrder/exportBoard`, this.formLabelAlign).then(res => { |
|
// |
|
if (this.$route.path == '/productionSchedulingPlan/schedulingDashboard/index') { |
|
downloadXls(res.data, `排产看板数据.xlsx`); |
|
} else { |
|
const blob = res.data; |
|
const reader = new FileReader(); |
|
reader.onload = function (e) { |
|
const base64 = e.target.result; // 格式:data:application/octet-stream;base64,... |
|
const fileName = '排产看板数据.xlsx'; // 可从接口响应头获取(如 Content-Disposition) |
|
|
|
// 4. 通过 postMessage 通知父页面 |
|
window.parent.postMessage( |
|
{ |
|
type: 'DOWNLOAD_FILE', |
|
base64: base64, |
|
fileName: fileName, |
|
}, |
|
'*' // 生产环境建议指定父页面域名(如 "https://parent-domain.com"),避免安全风险 |
|
); |
|
}; |
|
reader.readAsDataURL(blob); |
|
} |
|
}); |
|
}, |
|
|
|
// 滚动到当前时间位置 |
|
scrollToCurrentTime() { |
|
const container = this.$refs.timelineContainer; |
|
if (container && this.currentTimePosition >= 0 && this.currentTimePosition <= 100) { |
|
// 计算当前时间位置的像素值 |
|
const scrollWidth = container.scrollWidth; |
|
const containerWidth = container.clientWidth; |
|
|
|
// 计算当前时间位置的像素坐标 |
|
const timePositionPx = (this.currentTimePosition / 100) * scrollWidth; |
|
|
|
// 计算需要滚动到的位置,使当前时间线在左侧可见 |
|
// 使用容器宽度的1/4位置作为目标,使当前时间在左侧区域显示 |
|
const targetPosition = timePositionPx - containerWidth / 100; |
|
|
|
// 平滑滚动到指定位置 |
|
container.scrollTo({ |
|
left: Math.max(0, Math.min(scrollWidth - containerWidth, targetPosition)), |
|
behavior: 'smooth', // 平滑滚动 |
|
}); |
|
} |
|
}, |
|
|
|
// 分页相关方法 |
|
handleSizeChange(val) { |
|
this.pageSize = val; |
|
this.currentPage = 1; |
|
this.updateCurrentPageOrders(); |
|
}, |
|
handleCurrentChange(val) { |
|
this.currentPage = val; |
|
this.updateCurrentPageOrders(); |
|
}, |
|
updateCurrentPageOrders() { |
|
const start = (this.currentPage - 1) * this.pageSize; |
|
const end = start + this.pageSize; |
|
this.currentPageOrders = this.allOrders.slice(start, end); |
|
}, |
|
|
|
// 数据获取 |
|
async getData(params) { |
|
if (this.formLabelAlign.timeRange.length > 0) { |
|
this.formLabelAlign.startTime = this.formLabelAlign.timeRange[0]; |
|
this.formLabelAlign.endTime = this.formLabelAlign.timeRange[1]; |
|
} |
|
if (this.formLabelAlign.planStatusList.length <= 0) { |
|
this.formLabelAlign.planStatusList = null; |
|
} |
|
this.loading = true; |
|
await getData(this.formLabelAlign).then(res => { |
|
this.processData(res.data.data); |
|
}); |
|
}, |
|
getSelectEquip() { |
|
getEquipment().then(res => { |
|
this.selectEquipOptions = res.data.data; |
|
}); |
|
}, |
|
getSelectTeam() { |
|
getTeamSet().then(res => { |
|
this.selectTeamOptions = res.data.data; |
|
}); |
|
}, |
|
getProcessSet() { |
|
getProcessSet().then(res => { |
|
this.selectProcessOptions = res.data.data; |
|
}); |
|
}, |
|
|
|
// 数据处理 |
|
processData(rawData) { |
|
const tasks = []; |
|
const orders = []; |
|
|
|
// 遍历数组对象 |
|
rawData.forEach(item => { |
|
const woCode = item.woCode; // 提取车间订单号 |
|
const woTasks = item.workOrderList || []; // 提取任务列表 |
|
|
|
// 提取订单信息(取第一个任务的基础信息) |
|
if (woTasks.length > 0) { |
|
const firstTask = woTasks[0]; |
|
orders.push({ |
|
woCode, |
|
partCode: firstTask.partCode, |
|
batchNo: firstTask.batchNo, |
|
makeQty: firstTask.makeQty, |
|
productIdent: firstTask.productIdent, |
|
priorityAps: firstTask.priorityAps, |
|
}); |
|
} |
|
|
|
// 处理任务数据 |
|
woTasks.forEach(task => { |
|
tasks.push({ |
|
...task, |
|
woCode, // 将 woCode 添加到任务数据中,便于后续关联 |
|
}); |
|
}); |
|
}); |
|
|
|
this.allOrders = orders; |
|
this.totalOrders = orders.length; |
|
this.taskData = tasks; |
|
|
|
this.updateCurrentPageOrders(); |
|
this.loading = false; |
|
}, |
|
|
|
// 任务状态计算 |
|
calcTaskStatus(startTime, endTime) { |
|
const now = new Date(); |
|
const current = now.getHours() * 60 + now.getMinutes(); |
|
|
|
// 解析开始时间 |
|
let startMinutes; |
|
if (startTime.includes(' ')) { |
|
const timePart = startTime.split(' ')[1]; |
|
const [startHour, startMinute] = timePart.split(':').map(Number); |
|
startMinutes = startHour * 60 + startMinute; |
|
} else { |
|
const [startHour, startMinute] = startTime.split(':').map(Number); |
|
startMinutes = startHour * 60 + startMinute; |
|
} |
|
|
|
// 解析结束时间 |
|
let endMinutes; |
|
if (endTime.includes(' ')) { |
|
const timePart = endTime.split(' ')[1]; |
|
const [endHour, endMinute] = timePart.split(':').map(Number); |
|
endMinutes = endHour * 60 + endMinute; |
|
} else { |
|
const [endHour, endMinute] = endTime.split(':').map(Number); |
|
endMinutes = endHour * 60 + endMinute; |
|
} |
|
|
|
if (endMinutes < startMinutes) endMinutes += 24 * 60; |
|
|
|
if (current >= endMinutes) return '已完成'; |
|
else if (current >= startMinutes) return '进行中'; |
|
else return '未开始'; |
|
}, |
|
|
|
// 事件处理 |
|
handleSubmit() { |
|
if (!this.formLabelAlign.timeRange || this.formLabelAlign.timeRange.length !== 2) { |
|
this.$message.warning('请选择时间范围'); |
|
return; |
|
} |
|
// 将时间范围设置到请求参数中 |
|
this.formLabelAlign.startTime = this.formLabelAlign.timeRange[0]; |
|
this.formLabelAlign.endTime = this.formLabelAlign.timeRange[1]; |
|
|
|
this.getData(); |
|
}, |
|
handleReset() { |
|
this.formLabelAlign = { |
|
timeRange: [], // 重置为默认值 |
|
startTime: null, |
|
endTime: null, |
|
teamName: '', |
|
teamId: '', |
|
equipName: '', |
|
equipId: '', |
|
processName: '', |
|
processId: '', |
|
woCode: '', |
|
receiveTime: '', //接收时间 |
|
partCode: '', //订单号 |
|
batchNo: '', //批次号 |
|
}; |
|
this.legendStatus.completed = false; |
|
this.legendStatus.processing = true; |
|
this.legendStatus.pending = true; |
|
this.updateTime(); |
|
this.getData(); |
|
}, |
|
|
|
// 任务数据过滤 |
|
getDeviceTasks(device) { |
|
return this.taskData.filter(task => task.woCode === device); |
|
}, |
|
|
|
// 时间转换工具 |
|
timeToMinutes(timeStr) { |
|
const [hours, minutes] = timeStr.split(':').map(Number); |
|
return hours * 60 + minutes; |
|
}, |
|
// 任务 样式计算 |
|
getStatusColor(row) { |
|
switch (row.planStatus) { |
|
case '5': |
|
return '#007bff'; |
|
case '2': |
|
case '3': |
|
return '#28a745'; |
|
case '1': |
|
return '#6c757d'; |
|
case '6': |
|
return '#dc3545'; |
|
default: |
|
return '#ccc'; |
|
} |
|
}, |
|
getStatusTagType(row) { |
|
switch (row.planStatus) { |
|
case '5': |
|
return 'primary'; |
|
case '2': |
|
case '3': |
|
return 'success'; |
|
case '1': |
|
return 'info'; |
|
case '6': |
|
return 'danger'; |
|
default: |
|
return 'default'; |
|
} |
|
}, |
|
|
|
// 缩放控制 |
|
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; |
|
} |
|
}, |
|
|
|
// 提示框控制 |
|
showTooltip(e, task, device) { |
|
this.tooltipData = { ...task, device }; |
|
this.tooltipVisible = true; |
|
|
|
this.$nextTick(() => { |
|
const tooltipEl = document.querySelector('.tooltip'); |
|
if (!tooltipEl) return; |
|
|
|
const tooltipWidth = tooltipEl.offsetWidth; |
|
const tooltipHeight = tooltipEl.offsetHeight; |
|
const padding = 5; |
|
let x = e.pageX + 8; |
|
let y = e.pageY + 8; |
|
|
|
if (x + tooltipWidth > document.documentElement.clientWidth) { |
|
x = e.pageX - tooltipWidth - 8; |
|
} |
|
if (y + tooltipHeight > document.documentElement.clientHeight) { |
|
y = e.pageY - tooltipHeight - 8; |
|
} |
|
|
|
this.tooltipX = Math.max(padding, x); |
|
this.tooltipY = Math.max(padding, y); |
|
}); |
|
}, |
|
hideTooltip() { |
|
this.tooltipVisible = false; |
|
}, |
|
|
|
// 左侧行高计算 |
|
getRowHeight(device) { |
|
return this.baseRowHeight - 1; |
|
}, |
|
// 右侧行高计算 |
|
getRowChartHeight(device) { |
|
// 固定行高,不随层数变化 |
|
return this.baseRowChartHeight; |
|
}, |
|
getLayerOffset(layerIndex, height, device) { |
|
const rowHeight = this.getRowChartHeight(device); |
|
return Math.max(0, (rowHeight - height) / 2); |
|
}, |
|
getTastTopOffset(layerIndex, totalLayers, device) { |
|
const taskHeight = this.getLayerTaskHeight(totalLayers, device); |
|
// 计算任务条内部文字的垂直居中位置 |
|
return Math.max(0, (taskHeight - 31) / 2); // 文字高度约10px |
|
}, |
|
getLayerTaskHeight(totalLayers, device) { |
|
const rowHeight = this.getRowChartHeight(device); |
|
return rowHeight - 31; |
|
}, |
|
|
|
// 任务分层 |
|
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); |
|
return aStart - bStart; |
|
}); |
|
|
|
const layers = []; |
|
sortedTasks.forEach(task => { |
|
const taskStart = this.timeToMinutes(task.startTime); |
|
const taskEnd = this.timeToMinutes(task.endTime); |
|
|
|
// 处理跨天情况 |
|
const adjustedTaskEnd = taskEnd < taskStart ? taskEnd + 24 * 60 : taskEnd; |
|
|
|
// 寻找可以放置任务的层 |
|
let placed = false; |
|
for (let i = 0; i < layers.length; i++) { |
|
const layer = layers[i]; |
|
const lastTask = layer[layer.length - 1]; |
|
const lastEnd = this.timeToMinutes(lastTask.endTime); |
|
const adjustedLastEnd = |
|
lastEnd < this.timeToMinutes(lastTask.startTime) ? lastEnd + 24 * 60 : lastEnd; |
|
|
|
// 检查是否有时间重叠 |
|
if (taskStart >= adjustedLastEnd) { |
|
// 没有时间重叠,可以放在这一层 |
|
layer.push(task); |
|
placed = true; |
|
break; |
|
} |
|
} |
|
// 如果没有找到合适的层,创建新层 |
|
if (!placed) { |
|
layers.push([task]); |
|
} |
|
}); |
|
return layers; |
|
}, |
|
// 格式化小时标签,只显示小时部分 |
|
formatHourLabel(timeStr) { |
|
return timeStr.split(' ')[1]; // 只返回时间部分 |
|
}, |
|
|
|
// 格式化分钟标签 |
|
formatMinuteLabel(timeStr) { |
|
return timeStr.split(' ')[1]; // 只返回时间部分 |
|
}, |
|
|
|
// 更新时间轴数据 |
|
updateTimeAxis() { |
|
// 更新当前视图时间范围 |
|
this.currentViewStartTime = new Date(this.baseDate); |
|
this.currentViewEndTime = new Date(this.endDate); |
|
}, |
|
|
|
// 计算任务位置百分比(基于3天时间轴) |
|
getPositionPercent(startTime) { |
|
const startHour = this.parseTimeToHours(startTime); |
|
const totalDuration = this.totalHours; // 使用实际总小时数 |
|
|
|
// 确保百分比在有效范围内 |
|
const percent = (startHour / totalDuration) * 100; |
|
return Math.max(0, Math.min(100, percent)); |
|
}, |
|
|
|
// 计算任务宽度百分比 |
|
getWidthPercent(startTime, endTime) { |
|
const startHour = this.parseTimeToHours(startTime); |
|
const endHour = this.parseTimeToHours(endTime); |
|
|
|
const totalDuration = this.totalHours; // 使用实际总小时数 |
|
|
|
let duration = endHour - startHour; |
|
if (duration < 0) { |
|
// 处理跨天情况 - 这里需要根据实际业务逻辑调整 |
|
duration = this.totalHours - startHour + endHour; |
|
} |
|
|
|
// 确保宽度在有效范围内 |
|
const width = (duration / totalDuration) * 100; |
|
|
|
return Math.max(0, Math.min(100, width)); |
|
}, |
|
|
|
// // 将时间字符串转换为小时数(从基准日期开始计算) |
|
parseTimeToHours(timeStr) { |
|
if (!timeStr) { |
|
console.error('timeStr is undefined or null'); |
|
return 0; |
|
} |
|
|
|
// timeStr 格式是 "YYYY-MM-DD HH:mm" 如 "2025-12-25 18:35" |
|
const [datePart, timePart] = timeStr.split(' '); |
|
const [year, month, day] = datePart.split('-').map(Number); |
|
const [hours, minutes] = timePart.split(':').map(Number); |
|
|
|
// 创建完整日期时间对象 |
|
const fullDate = new Date(year, month - 1, day, hours, minutes); |
|
|
|
// 计算与基准开始时间的差值(小时) |
|
const diffMs = fullDate - this.baseDate; |
|
const diffHours = diffMs / (1000 * 60 * 60); |
|
|
|
// 确保结果在合理范围内 |
|
return Math.max(0, Math.min(this.totalHours, diffHours)); |
|
}, |
|
|
|
// 更新当前时间位置 |
|
calcCurrentTimePosition() { |
|
const now = new Date(); |
|
const viewStart = this.baseDate; |
|
const viewEnd = this.endDate; |
|
|
|
// 如果当前时间不在显示范围内,不显示当前时间线 |
|
if (now < viewStart || now > viewEnd) { |
|
this.currentTimePosition = -1; // 特殊值表示不显示 |
|
return; |
|
} |
|
|
|
// 计算当前时间相对于视图开始时间的毫秒差 |
|
const diffMs = now - viewStart; |
|
|
|
// 计算整个视图范围的总毫秒数 |
|
const totalDurationMs = viewEnd - viewStart; |
|
|
|
// 计算百分比位置 |
|
const positionPercent = (diffMs / totalDurationMs) * 100; |
|
|
|
// 确保百分比在有效范围内 [0, 100] |
|
this.currentTimePosition = Math.max(0, Math.min(100, positionPercent)); |
|
|
|
// 自动滚动到当前时间位置 |
|
this.$nextTick(() => { |
|
this.scrollToCurrentTime(); |
|
}); |
|
}, |
|
}, |
|
}; |
|
</script> |
|
|
|
<style scoped> |
|
.gantt-container { |
|
width: 100%; |
|
padding: 20px; |
|
box-sizing: border-box; |
|
font-family: Arial, sans-serif; |
|
height: calc(100% - 72px - 100px); |
|
} |
|
|
|
.gantt-header { |
|
height: 35px; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.status-legend { |
|
display: flex; |
|
gap: 20px; |
|
float: right; |
|
} |
|
|
|
.legend-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
font-size: 14px; |
|
} |
|
.legend-text { |
|
margin-left: 5px; |
|
} |
|
:deep(.el-checkbox__input.is-checked .el-checkbox__inner:after) { |
|
border-color: #fff; |
|
left: 5px; |
|
top: 2px; |
|
} |
|
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) { |
|
background-color: var(--checkbox-color); |
|
} |
|
|
|
:deep(.el-checkbox__inner) { |
|
border-color: var(--checkbox-color); |
|
background-color: var(--checkbox-color); |
|
width: 16px; |
|
height: 16px; |
|
} |
|
:deep(.el-checkbox__inner:hover) { |
|
border-color: var(--checkbox-color); |
|
} |
|
:deep(.el-checkbox__label) { |
|
color: #606266; |
|
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; |
|
overflow: hidden; |
|
} |
|
|
|
/* 左侧信息列表样式 */ |
|
.info-list { |
|
width: 360px; |
|
background-color: #f8f9fa; |
|
flex-shrink: 0; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
.info-container { |
|
flex: 1; |
|
overflow-y: auto; /* 允许垂直滚动 */ |
|
overflow-x: hidden; /* 隐藏水平滚动条 */ |
|
} |
|
/* 隐藏滚动条的样式(Webkit浏览器) */ |
|
.info-container::-webkit-scrollbar { |
|
display: none; /* 隐藏滚动条 */ |
|
} |
|
|
|
.info-item-title { |
|
display: flex; |
|
background: #284c89; |
|
color: #fff; |
|
font-weight: bold; |
|
height: 50px; |
|
line-height: 50px; |
|
} |
|
.info-item-txt { |
|
line-height: 25px; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
font-size: 12px; |
|
text-align: left; |
|
padding-left: 15px; |
|
} |
|
.info-item-txt i { |
|
font-style: normal; |
|
} |
|
.info-item-info { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding-right: 15px; |
|
} |
|
|
|
.info-title-cell { |
|
flex: 1; |
|
text-align: center; |
|
line-height: 50px; |
|
border-right: 1px solid #eee; |
|
font-size: 14px; |
|
} |
|
.info-title-num { |
|
width: 30px; |
|
flex: none !important; |
|
} |
|
.info-title-no { |
|
width: 125px; |
|
flex: none !important; |
|
} |
|
|
|
.info-item { |
|
display: flex; |
|
height: 100%; |
|
} |
|
|
|
.info-cell { |
|
flex: 1; |
|
text-align: center; |
|
box-sizing: border-box; |
|
border-right: 1px solid #eee; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
font-size: 10px; |
|
} |
|
|
|
.timeline-container { |
|
flex: 1; |
|
display: flex; |
|
flex-direction: column; |
|
overflow-y: auto; /* 允许垂直滚动 */ |
|
overflow-x: auto; /* 允许水平滚动 */ |
|
} |
|
|
|
/* 图表X轴区域样式 */ |
|
.chart-axis { |
|
height: 50px; |
|
position: relative; |
|
} |
|
|
|
.time-labels { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 50px; |
|
display: flex; |
|
background-color: #284c89; |
|
} |
|
|
|
.major-label { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
font-size: 14px; |
|
color: #fff; |
|
font-weight: 500; |
|
white-space: nowrap; |
|
line-height: 50px; |
|
} |
|
|
|
.minor-labels { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.minor-label { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
font-size: 12px; |
|
color: #fff; |
|
white-space: nowrap; |
|
line-height: 50px; |
|
padding: 0 2px; |
|
} |
|
|
|
.tick-lines { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 6px; |
|
background-color: #284c89; |
|
} |
|
|
|
.major-tick-line { |
|
position: absolute; |
|
bottom: 0; |
|
width: 1px; |
|
height: 4px; |
|
background-color: #fff; |
|
transform: translateX(-50%); |
|
} |
|
|
|
.minor-tick-lines { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
/* 15分钟刻度样式 */ |
|
.quarter-labels { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.quarter-label { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
font-size: 11px; |
|
color: #e0e0e0; |
|
white-space: nowrap; |
|
line-height: 30px; |
|
padding: 0 2px; |
|
} |
|
|
|
.quarter-tick-lines { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.quarter-tick-line { |
|
position: absolute; |
|
bottom: 0; |
|
width: 1px; |
|
height: 2px; |
|
background-color: #e0e0e0; |
|
transform: translateX(-50%); |
|
} |
|
.minor-tick-line { |
|
position: absolute; |
|
bottom: 0; |
|
width: 1px; |
|
height: 3px; |
|
background-color: #fff; |
|
transform: translateX(-50%); |
|
} |
|
|
|
.axis-base-line { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 1px; |
|
background-color: #dee2e6; |
|
} |
|
|
|
/* 当前时间线样式 */ |
|
.current-time-line { |
|
position: absolute; |
|
top: 0px; |
|
bottom: 0; |
|
width: 2px; |
|
transform: translateX(-50%); |
|
z-index: 10; |
|
height: calc(100vh - 130px); |
|
border-left: 1px dashed #ccc; |
|
} |
|
|
|
/* 图表内容区域 */ |
|
.chart-content { |
|
flex: 1; |
|
position: relative; |
|
overflow-y: auto; |
|
overflow-x: 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%; |
|
min-height: 100%; |
|
|
|
overflow-x: hidden; |
|
} |
|
|
|
.device-task-row { |
|
position: relative; |
|
border-bottom: 1px solid #e9ecef; |
|
box-sizing: border-box; |
|
padding: 0; |
|
margin: 0; |
|
} |
|
|
|
.task-bar { |
|
position: absolute; |
|
border-radius: 5px; |
|
display: flex; |
|
align-items: center; |
|
padding: 2px 0px; |
|
box-sizing: border-box; |
|
cursor: pointer; |
|
overflow: hidden; |
|
transition: all 0.2s; |
|
white-space: nowrap; |
|
border: 1px solid rgba(255, 255, 255, 0.3); |
|
min-width: 5px; |
|
z-index: 1; |
|
} |
|
.task-bar.task-bar-narrow { |
|
overflow: visible; /* 允许覆盖元素超出边界 */ |
|
} |
|
|
|
.task-bar:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.task-label { |
|
position: relative; |
|
width: 100%; |
|
height: 100%; |
|
text-align: center; |
|
display: inline-block; |
|
transform: scale(0.7); |
|
} |
|
.task-label-txt { |
|
position: absolute; |
|
font-size: 9px; |
|
color: white; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
max-width: 120%; |
|
text-align: center; |
|
/* 正确的居中方式 */ |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
} |
|
.task-label-txt-inside { |
|
} |
|
.task-overlay-text { |
|
position: absolute; |
|
top: -20px; |
|
left: -7px; |
|
color: rgba(0, 0, 0); |
|
padding: 2px 4px 2px 0; |
|
border-radius: 3px; |
|
font-size: 10px; |
|
white-space: nowrap; |
|
z-index: 10; |
|
transform: none; |
|
min-width: max-content; |
|
pointer-events: none; |
|
display: inline-block; |
|
transform: scale(0.7); |
|
} |
|
|
|
/* 提示框样式 */ |
|
.tooltip { |
|
position: fixed; |
|
background-color: white; |
|
border: 1px solid #ddd; |
|
border-radius: 4px; |
|
padding: 20px; |
|
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; |
|
} |
|
.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: 60px; |
|
color: #666; |
|
font-weight: 500; |
|
text-align: right; |
|
} |
|
|
|
.value { |
|
flex: 1; |
|
color: #333; |
|
} |
|
} |
|
} |
|
|
|
/* 分页样式 */ |
|
.pagination-container { |
|
height: 35px; |
|
margin-top: 15px; |
|
background-color: #fff; /* 增加背景色避免与内容重叠时看不清 */ |
|
padding: 10px; /* 增加内边距 */ |
|
z-index: 10; |
|
float: right; |
|
width: calc(100% - 20px); |
|
} |
|
:deep(.el-pagination) { |
|
float: right; |
|
} |
|
:deep(.el-button--primary) { |
|
background-color: #284c89 !important; |
|
color: #fff; |
|
} |
|
:deep(.el-col) { |
|
margin-bottom: 0px; |
|
} |
|
|
|
/* 日期标签行 */ |
|
.date-labels { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
height: 20px; |
|
display: flex; |
|
} |
|
|
|
.date-label { |
|
position: absolute; |
|
top: 0; |
|
height: 20px; |
|
background-color: #1a3a6c; |
|
color: #fff; |
|
font-size: 12px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
border-right: 1px solid #fff; |
|
box-sizing: border-box; |
|
} |
|
|
|
.time-labels { |
|
position: absolute; |
|
top: 20px; /* 为日期行留出空间 */ |
|
left: 0; |
|
width: 100%; |
|
height: 30px; |
|
display: flex; |
|
background-color: #284c89; |
|
} |
|
|
|
.major-label { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
font-size: 12px; |
|
color: #fff; |
|
white-space: nowrap; |
|
line-height: 30px; |
|
transform: translateX(-50%); |
|
} |
|
|
|
.current-time-line { |
|
position: absolute; |
|
top: 0; |
|
bottom: 0; |
|
width: 2px; |
|
transform: translateX(-50%); |
|
z-index: 10; |
|
height: 100%; |
|
} |
|
|
|
.current-time-indicator { |
|
position: absolute; |
|
top: -20px; |
|
left: -15px; |
|
background-color: #ff4d4f; |
|
color: white; |
|
padding: 2px 5px; |
|
border-radius: 3px; |
|
font-size: 10px; |
|
white-space: nowrap; |
|
} |
|
.info-item-content { |
|
display: flex; |
|
width: 100%; |
|
} |
|
/* 左侧信息颜色区分 */ |
|
.order-code { |
|
color: #1a73e8; /* 谷歌蓝 - 专业稳重 */ |
|
font-weight: bold; |
|
} |
|
|
|
.part-code { |
|
color: #34a853; /* 谷歌绿 - 清新自然 */ |
|
font-weight: bold; |
|
} |
|
|
|
.batch-no { |
|
color: #fbbc05; /* 谷歌黄 - 温暖明亮 */ |
|
font-weight: bold; |
|
flex: 0 0 100px; /* 固定宽度80px,不伸缩 */ |
|
} |
|
|
|
.make-qty { |
|
color: #ea4335; /* 谷歌红 - 醒目突出 */ |
|
font-weight: bold; |
|
flex: 1; /* 均匀分配剩余空间 */ |
|
} |
|
|
|
.product-ident { |
|
color: #9c27b0; /* 紫色 - 优雅独特 */ |
|
font-weight: bold; |
|
flex: 1; /* 均匀分配剩余空间 */ |
|
} |
|
|
|
.priority-aps { |
|
color: #ff6d01; /* 橙色 - 温暖活力 */ |
|
font-weight: bold; |
|
flex: 0 0 90px; /* 固定宽度80px,不伸缩 */ |
|
text-align: right; |
|
padding-right: 15px; |
|
} |
|
</style>
|
|
|