|
|
|
|
<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="true"
|
|
|
|
|
: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" @dblclick="showTaskDetail(order)">
|
|
|
|
|
<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>
|
|
|
|
|
<span class="make-qty">{{ order.makeQty }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<div class="info-item-txt info-item-orderInfo">
|
|
|
|
|
<div class="info-item-content">
|
|
|
|
|
<span class="batch-data">{{ order.receiveTime }}</span>
|
|
|
|
|
<span class="batch-no">{{ order.batchNo }}</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-text-wrapper"
|
|
|
|
|
:class="{
|
|
|
|
|
'text-inside': canFitText(
|
|
|
|
|
task.processName,
|
|
|
|
|
task.planStartTime,
|
|
|
|
|
task.planEndTime
|
|
|
|
|
),
|
|
|
|
|
'text-outside': !canFitText(
|
|
|
|
|
task.processName,
|
|
|
|
|
task.planStartTime,
|
|
|
|
|
task.planEndTime
|
|
|
|
|
),
|
|
|
|
|
}"
|
|
|
|
|
:style="getTaskTextStyle(task, layerIndex)"
|
|
|
|
|
>
|
|
|
|
|
{{ task.processName }}
|
|
|
|
|
</div>
|
|
|
|
|
<!-- <div
|
|
|
|
|
class="task-label"
|
|
|
|
|
v-if="getWidthPercent(task.planStartTime, task.planEndTime) >= 1.5"
|
|
|
|
|
>
|
|
|
|
|
<span class="task-label-txt task-label-txt-inside">{{
|
|
|
|
|
task.processName
|
|
|
|
|
}}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-else
|
|
|
|
|
class="task-overlay-text"
|
|
|
|
|
:style="{
|
|
|
|
|
width: `${getWidthPercent(task.planStartTime, task.planEndTime)}%`,
|
|
|
|
|
top: `${getNarrowTaskOffsetByLayer(layerIndex)}px`,
|
|
|
|
|
left: `${getNarrowTaskOffsetByLayerLeft(
|
|
|
|
|
task.processName,
|
|
|
|
|
getWidthPercent(task.planStartTime, task.planEndTime),
|
|
|
|
|
layerIndex
|
|
|
|
|
)}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.teamMembers || '-' }}</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 {
|
|
|
|
|
latestPlanEndTime: null, //新增:用于存储最晚计划结束时间
|
|
|
|
|
earliestPlanStartTime: null, // 新增:用于存储最早计划开始时间
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
if (this.earliestPlanStartTime) {
|
|
|
|
|
const base = new Date(this.earliestPlanStartTime);
|
|
|
|
|
base.setHours(0, 0, 0, 0); // 这就是“年月日 00:00:00”
|
|
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 默认显示当前日期
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
// 如果没有时间范围,但有数据,则用最晚计划结束时间
|
|
|
|
|
if (this.latestPlanEndTime) {
|
|
|
|
|
const end = new Date(this.latestPlanEndTime);
|
|
|
|
|
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: {
|
|
|
|
|
showTaskDetail(task) {
|
|
|
|
|
console.log('showTaskDetail', task);
|
|
|
|
|
// 4. 通过 postMessage 通知父页面
|
|
|
|
|
window.parent.postMessage(
|
|
|
|
|
{
|
|
|
|
|
type: 'TASK_DETAILS',
|
|
|
|
|
batchNo: task.batchNo,
|
|
|
|
|
woCode: task.woCode,
|
|
|
|
|
partCode: task.partCode,
|
|
|
|
|
cardNo: task.cardNo,
|
|
|
|
|
},
|
|
|
|
|
'*' // 生产环境建议指定父页面域名(如 "https://parent-domain.com"),避免安全风险
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
// 根据层索引确定窄任务文本的垂直偏移
|
|
|
|
|
getNarrowTaskOffsetByLayer(layerIndex) {
|
|
|
|
|
// 偶数层在上方显示(负值),奇数层在下方显示(正值)
|
|
|
|
|
const offset = layerIndex % 2 === 0 ? -20 : 15; // -20px 在上方,5px 在下方
|
|
|
|
|
return offset;
|
|
|
|
|
},
|
|
|
|
|
// 根据层索引确定窄任务文本的垂直偏移
|
|
|
|
|
getNarrowTaskOffsetByLayerLeft(processName, widthPercent, layerIndex) {
|
|
|
|
|
// 偶数层在上方显示(负值),奇数层在下方显示(正值)
|
|
|
|
|
const textWidth = processName.length * 2;
|
|
|
|
|
const timeLine = this.$refs.timelineContainer.clientWidth;
|
|
|
|
|
const taskBarWidth = (timeLine * widthPercent) / 100;
|
|
|
|
|
|
|
|
|
|
if (textWidth > taskBarWidth) {
|
|
|
|
|
if (processName <= 4) {
|
|
|
|
|
return -7; // 文字超出任务条时,向左偏移一半文字宽度
|
|
|
|
|
} else {
|
|
|
|
|
return -textWidth * 1.5; // 文字超出任务条时,向左偏移一半文字宽度
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// console.log('小于宽度的', textWidth, taskBarWidth,processName);
|
|
|
|
|
if (processName <= 4) {
|
|
|
|
|
return -7; // 文字超出任务条时,向左偏移一半文字宽度
|
|
|
|
|
} else {
|
|
|
|
|
return -textWidth; // 文字超出任务条时,向左偏移一半文字宽度
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 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(type) {
|
|
|
|
|
const today = new Date();
|
|
|
|
|
const startDate = new Date(today);
|
|
|
|
|
startDate.setDate(today.getDate() - 3); // 前3天
|
|
|
|
|
const endDate = new Date(today);
|
|
|
|
|
endDate.setDate(today.getDate() + 3); // 后3天
|
|
|
|
|
|
|
|
|
|
// 格式化为 YYYY-MM-DD
|
|
|
|
|
const format = date => date.toISOString().split('T')[0];
|
|
|
|
|
this.formLabelAlign.timeRange = [format(startDate), format(endDate)];
|
|
|
|
|
|
|
|
|
|
// 如果是重置操作,也应触发图例状态同步
|
|
|
|
|
if (type === 'reset') {
|
|
|
|
|
this.legendStatus.completed = false;
|
|
|
|
|
this.legendStatus.processing = true;
|
|
|
|
|
this.legendStatus.pending = true;
|
|
|
|
|
this.handleLegendChange('reset');
|
|
|
|
|
} else {
|
|
|
|
|
this.handleLegendChange(); // 触发数据加载
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 处理图例状态变化
|
|
|
|
|
handleLegendChange(type) {
|
|
|
|
|
// 如果需要重新加载数据,可以调用
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
if (type == 'reset') {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.planStatusList.length <= 0) {
|
|
|
|
|
// this.formLabelAlign.planStatusList = [];
|
|
|
|
|
// }
|
|
|
|
|
let seeData = sessionStorage.getItem('formLabelAlign');
|
|
|
|
|
if (seeData) {
|
|
|
|
|
this.formLabelAlign = JSON.parse(seeData);
|
|
|
|
|
}
|
|
|
|
|
if (this.formLabelAlign.timeRange && this.formLabelAlign.timeRange.length > 0) {
|
|
|
|
|
this.formLabelAlign.startTime = this.formLabelAlign.timeRange[0];
|
|
|
|
|
this.formLabelAlign.endTime = this.formLabelAlign.timeRange[1];
|
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
|
teamMembers: firstTask.teamMembers,
|
|
|
|
|
receiveTime: firstTask.receiveTime, // 假设这里存在
|
|
|
|
|
cardNo: firstTask.cardNo,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ... 合并任务逻辑保持不变 ...
|
|
|
|
|
const groupedTasks = {};
|
|
|
|
|
woTasks.forEach(task => {
|
|
|
|
|
if (!groupedTasks[task.processName]) {
|
|
|
|
|
groupedTasks[task.processName] = [];
|
|
|
|
|
}
|
|
|
|
|
groupedTasks[task.processName].push(task);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Object.values(groupedTasks).forEach(group => {
|
|
|
|
|
// 按计划开始时间升序排序
|
|
|
|
|
const sortedTasks = [...group].sort((a, b) => {
|
|
|
|
|
return new Date(a.planStartTime) - new Date(b.planStartTime);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const mergedTasks = [];
|
|
|
|
|
let currentMergedTask = null;
|
|
|
|
|
|
|
|
|
|
sortedTasks.forEach(task => {
|
|
|
|
|
if (!currentMergedTask) {
|
|
|
|
|
currentMergedTask = { ...task };
|
|
|
|
|
} else {
|
|
|
|
|
const prevEndTime = new Date(currentMergedTask.planEndTime);
|
|
|
|
|
const currStartTime = new Date(task.planStartTime);
|
|
|
|
|
const currEndTime = new Date(task.planEndTime);
|
|
|
|
|
|
|
|
|
|
// 判断是否重叠或连续:当前任务开始时间 <= 已合并任务的结束时间
|
|
|
|
|
if (currStartTime.getTime() <= prevEndTime.getTime()) {
|
|
|
|
|
// 合并时间区间:结束时间取最大值
|
|
|
|
|
if (currEndTime.getTime() > prevEndTime.getTime()) {
|
|
|
|
|
currentMergedTask.planEndTime = task.planEndTime;
|
|
|
|
|
}
|
|
|
|
|
// 可选:合并其他字段,如数量、人员等
|
|
|
|
|
// currentMergedTask.makeQty += task.makeQty;
|
|
|
|
|
} else {
|
|
|
|
|
// 无重叠,推入并开启新合并任务
|
|
|
|
|
mergedTasks.push(currentMergedTask);
|
|
|
|
|
currentMergedTask = { ...task };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (currentMergedTask) {
|
|
|
|
|
mergedTasks.push(currentMergedTask);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tasks.push(...mergedTasks);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
// console.log(909090, tasks);
|
|
|
|
|
// >>>>>>>>>> 新增排序逻辑 <<<<<<<<<<
|
|
|
|
|
// 1. 对 orders 按 receiveTime 升序
|
|
|
|
|
orders.sort((a, b) => new Date(a.receiveTime) - new Date(b.receiveTime));
|
|
|
|
|
|
|
|
|
|
// 2. 构建 woCode 到 receiveTime 的映射
|
|
|
|
|
const woReleaseMap = {};
|
|
|
|
|
orders.forEach(order => {
|
|
|
|
|
woReleaseMap[order.woCode] = order.receiveTime;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 3. 对 tasks 按其所属订单的 receiveTime
|
|
|
|
|
tasks.sort((a, b) => {
|
|
|
|
|
const dateA = new Date(woReleaseMap[a.woCode] || '1970-01-01');
|
|
|
|
|
const dateB = new Date(woReleaseMap[b.woCode] || '1970-01-01');
|
|
|
|
|
return dateA - dateB;
|
|
|
|
|
});
|
|
|
|
|
// >>>>>>>>>> 排序结束 <<<<<<<<<<
|
|
|
|
|
|
|
|
|
|
this.allOrders = orders;
|
|
|
|
|
this.totalOrders = orders.length;
|
|
|
|
|
this.taskData = tasks;
|
|
|
|
|
let earliest = null;
|
|
|
|
|
let latest = null;
|
|
|
|
|
tasks.forEach(task => {
|
|
|
|
|
if (task.planStartTime) {
|
|
|
|
|
const taskTime = new Date(task.planStartTime).getTime();
|
|
|
|
|
|
|
|
|
|
if (earliest === null || taskTime < earliest) {
|
|
|
|
|
earliest = taskTime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (task.planEndTime) {
|
|
|
|
|
const taskTime = new Date(task.planEndTime).getTime();
|
|
|
|
|
if (latest === null || taskTime > latest) {
|
|
|
|
|
latest = taskTime;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.earliestPlanStartTime = earliest ? new Date(earliest) : null;
|
|
|
|
|
this.latestPlanEndTime = latest ? new Date(latest) : null;
|
|
|
|
|
|
|
|
|
|
console.log(new Date(earliest), this.earliestPlanStartTime, 'earliest');
|
|
|
|
|
this.updateCurrentPageOrders();
|
|
|
|
|
this.loading = false;
|
|
|
|
|
},
|
|
|
|
|
formatHoursToTime(hours) {
|
|
|
|
|
const baseDate = new Date(this.baseDate);
|
|
|
|
|
const date = new Date(baseDate.getTime() + hours * 60 * 60 * 1000);
|
|
|
|
|
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
const hour = String(date.getHours()).padStart(2, '0');
|
|
|
|
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
|
|
|
|
|
|
|
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 任务状态计算
|
|
|
|
|
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('请选择时间范围');
|
|
|
|
|
// 将时间范围设置到请求参数中
|
|
|
|
|
this.formLabelAlign.startTime = '';
|
|
|
|
|
this.formLabelAlign.endTime = '';
|
|
|
|
|
// return;
|
|
|
|
|
} else {
|
|
|
|
|
// 将时间范围设置到请求参数中
|
|
|
|
|
this.formLabelAlign.startTime = this.formLabelAlign.timeRange[0];
|
|
|
|
|
this.formLabelAlign.endTime = this.formLabelAlign.timeRange[1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(this.formLabelAlign, '99999');
|
|
|
|
|
sessionStorage.setItem('formLabelAlign', JSON.stringify(this.formLabelAlign));
|
|
|
|
|
|
|
|
|
|
this.getData();
|
|
|
|
|
},
|
|
|
|
|
handleReset() {
|
|
|
|
|
this.formLabelAlign = {
|
|
|
|
|
timeRange: [], // 重置为默认值
|
|
|
|
|
startTime: null,
|
|
|
|
|
endTime: null,
|
|
|
|
|
teamName: '',
|
|
|
|
|
teamId: '',
|
|
|
|
|
equipName: '',
|
|
|
|
|
equipId: '',
|
|
|
|
|
processName: '',
|
|
|
|
|
processId: '',
|
|
|
|
|
woCode: '',
|
|
|
|
|
receiveTime: '', //接收时间
|
|
|
|
|
partCode: '', //订单号
|
|
|
|
|
batchNo: '', //批次号
|
|
|
|
|
planStatusList: ['1', '2', '3'],
|
|
|
|
|
};
|
|
|
|
|
this.legendStatus.completed = false;
|
|
|
|
|
this.legendStatus.processing = true;
|
|
|
|
|
this.legendStatus.pending = true;
|
|
|
|
|
|
|
|
|
|
// this.updateTime('reset');
|
|
|
|
|
sessionStorage.setItem('formLabelAlign', JSON.stringify(this.formLabelAlign));
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
if (row.planStatus === '1') {
|
|
|
|
|
if (row.remindStatus === '1') {
|
|
|
|
|
return '#FFD700'; // 黄色(可自定义)
|
|
|
|
|
} else if (row.remindStatus === '2') {
|
|
|
|
|
return '#dc3545'; // 红色(与返工状态一致,或自定义)
|
|
|
|
|
}
|
|
|
|
|
// 如果没有 remindStatus,默认保持原灰色
|
|
|
|
|
return '#6c757d';
|
|
|
|
|
}
|
|
|
|
|
switch (row.planStatus) {
|
|
|
|
|
case '5':
|
|
|
|
|
return '#007bff';
|
|
|
|
|
case '2':
|
|
|
|
|
case '3':
|
|
|
|
|
return '#28a745';
|
|
|
|
|
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) {
|
|
|
|
|
let labelsWidth = document.querySelector('.date-label').offsetWidth;
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
formatDate(date) {
|
|
|
|
|
if (!date) return '';
|
|
|
|
|
const d = new Date(date);
|
|
|
|
|
const year = d.getFullYear();
|
|
|
|
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
|
|
|
return `${year}-${month}-${day}`;
|
|
|
|
|
},
|
|
|
|
|
// 判断文字是否能在任务条内完整显示
|
|
|
|
|
canFitText(text, startTime, endTime) {
|
|
|
|
|
if (!text || !startTime || !endTime || !this.$refs.timelineContainer) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const taskWidthPercent = this.getWidthPercent(startTime, endTime);
|
|
|
|
|
|
|
|
|
|
const containerWidth = this.$refs.timelineContainer.scrollWidth;
|
|
|
|
|
if (containerWidth <= 0) return false; // 防止除零或无效计算
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const taskPixelWidth = (containerWidth * taskWidthPercent)/100;
|
|
|
|
|
const textWidth = this.getTextWidth(text, 10); // 确保字体大小匹配实际样式
|
|
|
|
|
return taskPixelWidth >= textWidth + 4; // 4px 是左右 padding 缓冲
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 估算文字像素宽度(简单方法)
|
|
|
|
|
getTextWidth(text, fontSize = 12) {
|
|
|
|
|
// 创建一个临时 canvas 来测量文字宽度
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
const context = canvas.getContext('2d');
|
|
|
|
|
context.font = `${fontSize}px Arial`;
|
|
|
|
|
const metrics = context.measureText(text);
|
|
|
|
|
return Math.ceil(metrics.width);
|
|
|
|
|
},
|
|
|
|
|
// 根据是否能容纳文字,返回不同的样式
|
|
|
|
|
getTaskTextStyle(task, layerIndex) {
|
|
|
|
|
const canFit = this.canFitText(task.processName, task.planStartTime, task.planEndTime);
|
|
|
|
|
|
|
|
|
|
if (canFit) {
|
|
|
|
|
// 文字在内部居中
|
|
|
|
|
return {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: '50%',
|
|
|
|
|
left: '50%',
|
|
|
|
|
transform: 'translate(-50%, -50%)',
|
|
|
|
|
color: 'white',
|
|
|
|
|
fontSize: '10px',
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
textShadow: '0 0 2px rgba(0,0,0,0.5)',
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
// 文字在外部:偶数层在上方,奇数层在下方
|
|
|
|
|
const isEvenLayer = layerIndex % 2 === 0;
|
|
|
|
|
return {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: isEvenLayer ? '-15px' : 'calc(100% )',
|
|
|
|
|
left: '50%',
|
|
|
|
|
transform: 'translateX(-50%)',
|
|
|
|
|
color: '#333',
|
|
|
|
|
fontSize: '10px',
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
padding: '2px 6px',
|
|
|
|
|
borderRadius: '3px',
|
|
|
|
|
zIndex: 10,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.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;、
|
|
|
|
|
// padding:0 10px;
|
|
|
|
|
}
|
|
|
|
|
.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%;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
.info-item:hover {
|
|
|
|
|
background-color: #e9ecef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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: 80px;
|
|
|
|
|
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: caclc(100% - 20px);
|
|
|
|
|
}
|
|
|
|
|
/* 左侧信息颜色区分 */
|
|
|
|
|
.order-code {
|
|
|
|
|
color: #1a73e8; /* 谷歌蓝 - 专业稳重 */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
padding-right: 15px;
|
|
|
|
|
padding-left: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.part-code {
|
|
|
|
|
color: #34a853; /* 谷歌绿 - 清新自然 */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
.batch-data {
|
|
|
|
|
color: #b73779; /* 谷歌黄 - 温暖明亮 */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
flex: 0 0 100px; /* 固定宽度80px,不伸缩 */
|
|
|
|
|
padding-left: 10px;
|
|
|
|
|
}
|
|
|
|
|
.batch-no {
|
|
|
|
|
color: #fbbc05; /* 谷歌黄 - 温暖明亮 */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
flex: 0 0 100px; /* 固定宽度80px,不伸缩 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.make-qty {
|
|
|
|
|
color: #ea4335; /* 谷歌红 - 醒目突出 */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
flex: 1; /* 均匀分配剩余空间 */
|
|
|
|
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
padding-right: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.product-ident {
|
|
|
|
|
color: #9c27b0; /* 紫色 - 优雅独特 */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
flex: 1; /* 均匀分配剩余空间 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.priority-aps {
|
|
|
|
|
color: #ff6d01; /* 橙色 - 温暖活力 */
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
flex: 1; /* 固定宽度80px,不伸缩 */
|
|
|
|
|
text-align: right;
|
|
|
|
|
padding-right: 10px;
|
|
|
|
|
}
|
|
|
|
|
.task-text-wrapper {
|
|
|
|
|
pointer-events: none; /* 避免遮挡 hover 事件 */
|
|
|
|
|
}
|
|
|
|
|
</style>
|