|
|
|
|
<template>
|
|
|
|
|
<div>
|
|
|
|
|
<el-form label-width="80px" :model="formLabelAlign">
|
|
|
|
|
<el-row>
|
|
|
|
|
<!-- 新增查询条件 -->
|
|
|
|
|
<el-col :span="5">
|
|
|
|
|
<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="5">
|
|
|
|
|
<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="5">
|
|
|
|
|
<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="5">
|
|
|
|
|
<el-form-item label="时间:">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="formLabelAlign.startTime"
|
|
|
|
|
type="date"
|
|
|
|
|
value-format="YYYY-MM-DD"
|
|
|
|
|
placeholder="选择日期"
|
|
|
|
|
size="medium"
|
|
|
|
|
></el-date-picker>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<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>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
<div class="gantt-container">
|
|
|
|
|
<!-- 头部标题和图例 -->
|
|
|
|
|
<div class="gantt-header">
|
|
|
|
|
<div class="status-legend">
|
|
|
|
|
<div class="legend-item">
|
|
|
|
|
<el-button type="primary" size="medium" @click="exportXls">导出</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="legend-item">
|
|
|
|
|
<span class="legend-color completed"></span>
|
|
|
|
|
<span>已完成</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="legend-item">
|
|
|
|
|
<span class="legend-color processing"></span>
|
|
|
|
|
<span>进行中</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="legend-item">
|
|
|
|
|
<span class="legend-color pending"></span>
|
|
|
|
|
<span>未开始</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 甘特图主体 -->
|
|
|
|
|
<div class="gantt-wrapper">
|
|
|
|
|
<!-- 左侧信息列表 - 展示车间订单号等信息 -->
|
|
|
|
|
<div class="info-list">
|
|
|
|
|
<div class="info-item-title">
|
|
|
|
|
<div class="info-title-cell">订单信息</div>
|
|
|
|
|
<!-- <div class="info-title-cell info-title-no">车间订单号</div>
|
|
|
|
|
<div class="info-title-cell info-title-no">零件号</div>
|
|
|
|
|
<div class="info-title-cell">批次号</div>
|
|
|
|
|
<div class="info-title-cell info-title-num">数量</div>
|
|
|
|
|
<div class="info-title-cell info-title-num">质量等级</div> -->
|
|
|
|
|
</div>
|
|
|
|
|
<div class="info-container">
|
|
|
|
|
<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">
|
|
|
|
|
<el-row>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<div class="info-item-txt">订单号:{{ order.woCode }}</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<div class="info-item-txt">零件号:{{ order.partCode }}</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<div class="info-item-txt">批次号:{{ order.batchNo }}</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<div class="info-item-txt">数量:{{ order.makeQty }}</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<div class="info-item-txt">质量等级:{{ order.productIdent }}</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<!-- <div class="info-cell info-title-no">{{ order.woCode }}</div>
|
|
|
|
|
<div class="info-cell info-title-no">{{ order.partCode }}</div>
|
|
|
|
|
<div class="info-cell">{{ order.batchNo }}</div>
|
|
|
|
|
<div class="info-cell info-title-num">{{ order.makeQty }}</div>
|
|
|
|
|
<div class="info-cell info-title-num">{{ order.productIdent }}</div> -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧时间轴 @wheel.prevent="handleWheel"-->
|
|
|
|
|
<div class="timeline-container" ref="timelineContainer">
|
|
|
|
|
<!-- 图表X轴区域 -->
|
|
|
|
|
<div class="chart-axis">
|
|
|
|
|
<div class="time-labels" :style="{ width: `${timelineWidth}%` }">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(time, index) in majorTickLabels"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="major-label"
|
|
|
|
|
:style="{ left: `${(index / 24) * 100}%` }"
|
|
|
|
|
>
|
|
|
|
|
{{ time }}
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="zoomLevel >= 2" class="minor-labels">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(time, index) in minorTickLabels"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="minor-label"
|
|
|
|
|
:style="{ left: `${(index / (24 * 2)) * 100}%` }"
|
|
|
|
|
>
|
|
|
|
|
{{ time }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="zoomLevel >= 3" class="quarter-labels">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(time, index) in quarterTickLabels"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="quarter-label"
|
|
|
|
|
:style="{ left: `${(index / (24 * 4)) * 100}%` }"
|
|
|
|
|
>
|
|
|
|
|
{{ time }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tick-lines" :style="{ width: `${timelineWidth}%` }">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(time, index) in majorTickLabels"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="major-tick-line"
|
|
|
|
|
:style="{ left: `${(index / 24) * 100}%` }"
|
|
|
|
|
></div>
|
|
|
|
|
<div v-if="zoomLevel >= 2" class="minor-tick-lines">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(time, index) in minorTickLabels"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="minor-tick-line"
|
|
|
|
|
:style="{ left: `${(index / (24 * 2)) * 100}%` }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="zoomLevel >= 3" class="quarter-tick-lines">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(time, index) in quarterTickLabels"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="quarter-tick-line"
|
|
|
|
|
:style="{ left: `${(index / (24 * 4)) * 100}%` }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="axis-base-line"></div>
|
|
|
|
|
<!-- 当前时间线 -->
|
|
|
|
|
<div class="current-time-line" :style="{ left: `${currentTimePosition}%` }"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 甘特图内容区域 -->
|
|
|
|
|
<div class="chart-content" :style="{ width: `${timelineWidth}%` }">
|
|
|
|
|
<div class="grid-lines">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(time, index) in majorTickLabels"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="grid-line"
|
|
|
|
|
:style="{ left: `${(index / 24) * 100}%` }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="tasks-container">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(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"
|
|
|
|
|
:style="{
|
|
|
|
|
left: `${getPositionPercent(task.startTime)}%`,
|
|
|
|
|
width: `${getWidthPercent(task.startTime, task.endTime)}%`,
|
|
|
|
|
backgroundColor: getStatusColor(task),
|
|
|
|
|
top: `${getLayerOffset(
|
|
|
|
|
layerIndex,
|
|
|
|
|
getLayeredTasks(order.woCode).length,
|
|
|
|
|
order.woCode
|
|
|
|
|
)}px`,
|
|
|
|
|
height: `${getLayerTaskHeight(
|
|
|
|
|
getLayeredTasks(order.woCode).length,
|
|
|
|
|
order.woCode
|
|
|
|
|
)}px`,
|
|
|
|
|
}"
|
|
|
|
|
@mouseenter="showTooltip($event, task, order.woCode)"
|
|
|
|
|
@mouseleave="hideTooltip()"
|
|
|
|
|
>
|
|
|
|
|
<div class="task-label">
|
|
|
|
|
<span
|
|
|
|
|
class="task-label-txt"
|
|
|
|
|
:style="{
|
|
|
|
|
top: `${getTastTopOffset(
|
|
|
|
|
layerIndex,
|
|
|
|
|
getLayeredTasks(order.woCode).length,
|
|
|
|
|
order.woCode
|
|
|
|
|
)}px`,
|
|
|
|
|
}"
|
|
|
|
|
>{{ task.processName }}</span
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 分页控件 sizes :page-sizes="[10, 20, 50]" ,sizes-->
|
|
|
|
|
<div class="pagination-container">
|
|
|
|
|
<el-pagination
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
:current-page="currentPage"
|
|
|
|
|
:page-size="pageSize"
|
|
|
|
|
layout="total, 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.factStartTime || '-' }}</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="detail-item">
|
|
|
|
|
<span class="label">计划完成时间:</span>
|
|
|
|
|
<span class="value">{{ tooltipData.planEndTime || '-' }}</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="detail-item">
|
|
|
|
|
<span class="label">实际完成时间:</span>
|
|
|
|
|
<span class="value">{{ tooltipData.factEndTime || '-' }}</span>
|
|
|
|
|
</li>
|
|
|
|
|
<li class="detail-item">
|
|
|
|
|
<span class="label">状态:</span>
|
|
|
|
|
<span class="value">
|
|
|
|
|
<el-tag :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 {
|
|
|
|
|
formLabelAlign: {
|
|
|
|
|
startTime: null, //时间
|
|
|
|
|
teamName: '', //班组
|
|
|
|
|
teamId: '', //班组
|
|
|
|
|
equipName: '', //设备
|
|
|
|
|
equipId: '', //设备
|
|
|
|
|
processName: '', //工序
|
|
|
|
|
processId: '', //工序
|
|
|
|
|
woCode: '', //车间订单号
|
|
|
|
|
},
|
|
|
|
|
zoomLevel: 12, // 缩放级别 (1-4)
|
|
|
|
|
minZoom: 1,
|
|
|
|
|
maxZoom: 4,
|
|
|
|
|
|
|
|
|
|
// 订单列表和任务数据
|
|
|
|
|
allOrders: [], // 所有订单信息
|
|
|
|
|
currentPageOrders: [], // 当前页订单
|
|
|
|
|
taskData: [],
|
|
|
|
|
totalOrders: 0,
|
|
|
|
|
|
|
|
|
|
// 分页参数
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
pageSize: 6,
|
|
|
|
|
|
|
|
|
|
// 提示框相关
|
|
|
|
|
tooltipVisible: false,
|
|
|
|
|
tooltipData: {},
|
|
|
|
|
tooltipX: 0,
|
|
|
|
|
tooltipY: 0,
|
|
|
|
|
|
|
|
|
|
// 样式相关
|
|
|
|
|
baseRowHeight: 70,
|
|
|
|
|
baseRowChartHeight: 71,
|
|
|
|
|
rowHeights: {},
|
|
|
|
|
selectTeamOptions: [],
|
|
|
|
|
selectEquipOptions: [],
|
|
|
|
|
selectProcessOptions: [],
|
|
|
|
|
|
|
|
|
|
// 当前时间位置
|
|
|
|
|
currentTimePosition: 0,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
timelineWidth() {
|
|
|
|
|
return 100 * this.zoomLevel;
|
|
|
|
|
},
|
|
|
|
|
majorTickLabels() {
|
|
|
|
|
return Array.from({ length: 24 }, (_, i) => `${i}:00`);
|
|
|
|
|
},
|
|
|
|
|
minorTickLabels() {
|
|
|
|
|
const labels = [];
|
|
|
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
|
|
|
labels.push('');
|
|
|
|
|
labels.push(`${hour}:30`);
|
|
|
|
|
}
|
|
|
|
|
return labels;
|
|
|
|
|
},
|
|
|
|
|
quarterTickLabels() {
|
|
|
|
|
const labels = [];
|
|
|
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
|
|
|
labels.push(''); // 0分位置留空(与小时刻度重叠)
|
|
|
|
|
labels.push(`${hour}:15`);
|
|
|
|
|
labels.push(''); // 30分位置留空(与半小时刻度重叠)
|
|
|
|
|
labels.push(`${hour}:45`);
|
|
|
|
|
}
|
|
|
|
|
return labels;
|
|
|
|
|
},
|
|
|
|
|
tenMinuteTickLabels() {
|
|
|
|
|
const labels = [];
|
|
|
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
|
|
|
// 每小时包含6个10分钟刻度(0,10,20,30,40,50)
|
|
|
|
|
// 0分和30分位置留空避免与已有刻度重叠
|
|
|
|
|
labels.push(''); // 0分
|
|
|
|
|
labels.push(`${hour}:10`);
|
|
|
|
|
labels.push(`${hour}:20`);
|
|
|
|
|
labels.push(''); // 30分
|
|
|
|
|
labels.push(`${hour}:40`);
|
|
|
|
|
labels.push(`${hour}:50`);
|
|
|
|
|
}
|
|
|
|
|
return labels;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.formLabelAlign.startTime = new Date().toISOString().substr(0, 10);
|
|
|
|
|
this.getSelectTeam();
|
|
|
|
|
this.getSelectEquip();
|
|
|
|
|
this.getProcessSet();
|
|
|
|
|
this.getData();
|
|
|
|
|
this.calcCurrentTimePosition();
|
|
|
|
|
// 定时更新当前时间线位置
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
this.calcCurrentTimePosition();
|
|
|
|
|
}, 600000); // 每分钟更新一次
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
processChange(val) {
|
|
|
|
|
this.formLabelAlign.processName = val.name;
|
|
|
|
|
},
|
|
|
|
|
equipChange(val) {
|
|
|
|
|
this.formLabelAlign.equipName = val.deviceName;
|
|
|
|
|
},
|
|
|
|
|
teamChange(val) {
|
|
|
|
|
this.formLabelAlign.teamName = val.tsName;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 导出
|
|
|
|
|
exportXls() {
|
|
|
|
|
exportBlob(`/blade-scheduling/workOrder/exportBoard`, this.formLabelAlign).then(res => {
|
|
|
|
|
//
|
|
|
|
|
console.log(this.$route.path == '/productionSchedulingPlan/schedulingDashboard/index');
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
// 计算当前时间在时间轴上的位置百分比
|
|
|
|
|
calcCurrentTimePosition() {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const hours = now.getHours();
|
|
|
|
|
const minutes = now.getMinutes();
|
|
|
|
|
const totalMinutes = hours * 60 + minutes;
|
|
|
|
|
this.currentTimePosition = (totalMinutes / (24 * 60)) * 100;
|
|
|
|
|
|
|
|
|
|
// 自动滚动到当前时间位置
|
|
|
|
|
this.scrollToCurrentTime();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 滚动到当前时间位置
|
|
|
|
|
scrollToCurrentTime() {
|
|
|
|
|
const container = this.$refs.timelineContainer;
|
|
|
|
|
if (container) {
|
|
|
|
|
const scrollPosition =
|
|
|
|
|
(this.currentTimePosition / 100) * container.scrollWidth - container.clientWidth / 2;
|
|
|
|
|
container.scrollLeft = scrollPosition;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 分页相关方法
|
|
|
|
|
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);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 数据获取
|
|
|
|
|
getData() {
|
|
|
|
|
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 = [];
|
|
|
|
|
const workOrders = Object.keys(rawData);
|
|
|
|
|
|
|
|
|
|
workOrders.forEach(woCode => {
|
|
|
|
|
const woTasks = rawData[woCode] || [];
|
|
|
|
|
// 提取订单信息(取第一个任务的基础信息)
|
|
|
|
|
if (woTasks.length > 0) {
|
|
|
|
|
const firstTask = woTasks[0];
|
|
|
|
|
orders.push({
|
|
|
|
|
woCode,
|
|
|
|
|
partCode: firstTask.partCode,
|
|
|
|
|
batchNo: firstTask.batchNo,
|
|
|
|
|
makeQty: firstTask.makeQty,
|
|
|
|
|
productIdent: firstTask.productIdent,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// 处理任务数据
|
|
|
|
|
woTasks.forEach(task => {
|
|
|
|
|
tasks.push({
|
|
|
|
|
...task,
|
|
|
|
|
status: this.calcTaskStatus(task.startTime, task.endTime),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.allOrders = orders;
|
|
|
|
|
this.totalOrders = orders.length;
|
|
|
|
|
this.taskData = tasks;
|
|
|
|
|
this.updateCurrentPageOrders();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 任务状态计算
|
|
|
|
|
calcTaskStatus(startTime, endTime) {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const current = now.getHours() * 60 + now.getMinutes();
|
|
|
|
|
const start = this.timeToMinutes(startTime);
|
|
|
|
|
let end = this.timeToMinutes(endTime);
|
|
|
|
|
|
|
|
|
|
if (end < start) end += 24 * 60;
|
|
|
|
|
|
|
|
|
|
if (current >= end) return '已完成';
|
|
|
|
|
else if (current >= start) return '进行中';
|
|
|
|
|
else return '未开始';
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 事件处理
|
|
|
|
|
handleSubmit() {
|
|
|
|
|
this.getData();
|
|
|
|
|
},
|
|
|
|
|
handleReset() {
|
|
|
|
|
this.formLabelAlign = {
|
|
|
|
|
startTime: new Date().toISOString().substr(0, 10),
|
|
|
|
|
teamName: '',
|
|
|
|
|
teamId: '',
|
|
|
|
|
equipName: '',
|
|
|
|
|
equipId: '',
|
|
|
|
|
processName: '',
|
|
|
|
|
processId: '',
|
|
|
|
|
woCode: '',
|
|
|
|
|
};
|
|
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 甘特图位置计算
|
|
|
|
|
getPositionPercent(startTime) {
|
|
|
|
|
const totalMinutes = 24 * 60;
|
|
|
|
|
const startMinutes = this.timeToMinutes(startTime);
|
|
|
|
|
return (startMinutes / totalMinutes) * 100;
|
|
|
|
|
},
|
|
|
|
|
getWidthPercent(startTime, endTime) {
|
|
|
|
|
const startMinutes = this.timeToMinutes(startTime);
|
|
|
|
|
let endMinutes = this.timeToMinutes(endTime);
|
|
|
|
|
if (endMinutes < startMinutes) endMinutes += 24 * 60;
|
|
|
|
|
const duration = endMinutes - startMinutes;
|
|
|
|
|
return (duration / (24 * 60)) * 100;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 任务 样式计算
|
|
|
|
|
getStatusColor(row) {
|
|
|
|
|
switch (row.planStatus) {
|
|
|
|
|
case '5':
|
|
|
|
|
return '#28a745';
|
|
|
|
|
case '2':
|
|
|
|
|
case '3':
|
|
|
|
|
return '#007bff';
|
|
|
|
|
case '1':
|
|
|
|
|
return '#6c757d';
|
|
|
|
|
case '6':
|
|
|
|
|
return '#dc3545';
|
|
|
|
|
default:
|
|
|
|
|
return '#ccc';
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
getStatusTagType(row) {
|
|
|
|
|
switch (row.planStatus) {
|
|
|
|
|
case '5':
|
|
|
|
|
return 'success';
|
|
|
|
|
case '2':
|
|
|
|
|
case '3':
|
|
|
|
|
return 'primary';
|
|
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
// 右侧行高计算
|
|
|
|
|
getRowChartHeight(device) {
|
|
|
|
|
// 固定行高,不随层数变化
|
|
|
|
|
return this.baseRowChartHeight;
|
|
|
|
|
},
|
|
|
|
|
getLayerOffset(layerIndex, totalLayers, device) {
|
|
|
|
|
const rowHeight = this.getRowChartHeight(device);
|
|
|
|
|
if (totalLayers <= 1) return 2;
|
|
|
|
|
|
|
|
|
|
// 计算总间隔和可用高度
|
|
|
|
|
const totalSpacing = (totalLayers - 1) * 4; // 层间间隔
|
|
|
|
|
const availableHeight = rowHeight - 40 - totalSpacing; // 减去上下边距
|
|
|
|
|
const layerHeight = availableHeight / totalLayers;
|
|
|
|
|
|
|
|
|
|
// 计算当前层的偏移量
|
|
|
|
|
return 20 + layerIndex * (layerHeight + 4); // 20是顶部边距
|
|
|
|
|
},
|
|
|
|
|
getTastTopOffset(layerIndex, totalLayers, device) {
|
|
|
|
|
const rowHeight = this.getRowChartHeight(device);
|
|
|
|
|
if (totalLayers <= 1) return 8;
|
|
|
|
|
|
|
|
|
|
// 计算总间隔和可用高度
|
|
|
|
|
const totalSpacing = (totalLayers - 1) * 4; // 层间间隔
|
|
|
|
|
const availableHeight = rowHeight - 40 - totalSpacing; // 减去上下边距
|
|
|
|
|
const layerHeight = availableHeight / totalLayers;
|
|
|
|
|
// 计算当前层的偏移量
|
|
|
|
|
return 0; // 20是顶部边距
|
|
|
|
|
},
|
|
|
|
|
getLayerTaskHeight(totalLayers, device) {
|
|
|
|
|
const rowHeight = this.getRowChartHeight(device);
|
|
|
|
|
if (totalLayers <= 1) {
|
|
|
|
|
return rowHeight - 40; // 减去上下边距
|
|
|
|
|
} else {
|
|
|
|
|
const totalSpacing = (totalLayers - 1) * 4;
|
|
|
|
|
const availableHeight = rowHeight - 30 - totalSpacing;
|
|
|
|
|
return availableHeight / totalLayers; // 平均分配高度
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 任务分层
|
|
|
|
|
getLayeredTasks(device) {
|
|
|
|
|
const tasks = this.getDeviceTasks(device);
|
|
|
|
|
if (!tasks.length) return [];
|
|
|
|
|
|
|
|
|
|
const sortedTasks = [...tasks].sort((a, b) => {
|
|
|
|
|
const aStart = this.timeToMinutes(a.startTime);
|
|
|
|
|
const bStart = this.timeToMinutes(b.startTime);
|
|
|
|
|
if (this.timeToMinutes(a.endTime) < aStart && this.timeToMinutes(b.endTime) >= bStart) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
return aStart - bStart;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const layers = [];
|
|
|
|
|
sortedTasks.forEach(task => {
|
|
|
|
|
let placed = false;
|
|
|
|
|
const taskStart = this.timeToMinutes(task.startTime);
|
|
|
|
|
const taskEnd = this.timeToMinutes(task.endTime);
|
|
|
|
|
const adjustedEnd = taskEnd < taskStart ? taskEnd + 24 * 60 : taskEnd;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < layers.length; i++) {
|
|
|
|
|
const lastTask = layers[i][layers[i].length - 1];
|
|
|
|
|
const lastEnd = this.timeToMinutes(lastTask.endTime);
|
|
|
|
|
const lastAdjustedEnd =
|
|
|
|
|
lastEnd < this.timeToMinutes(lastTask.startTime) ? lastEnd + 24 * 60 : lastEnd;
|
|
|
|
|
|
|
|
|
|
if (taskStart >= lastAdjustedEnd) {
|
|
|
|
|
layers[i].push(task);
|
|
|
|
|
placed = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!placed) {
|
|
|
|
|
layers.push([task]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return layers;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.gantt-container {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
height: calc(100% - 72px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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-color {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
width: 12px;
|
|
|
|
|
height: 12px;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.legend-color.completed {
|
|
|
|
|
background-color: #28a745;
|
|
|
|
|
}
|
|
|
|
|
.legend-color.processing {
|
|
|
|
|
background-color: #007bff;
|
|
|
|
|
}
|
|
|
|
|
.legend-color.pending {
|
|
|
|
|
background-color: #6c757d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-wrapper {
|
|
|
|
|
display: flex;
|
|
|
|
|
height: calc(100% - 180px);
|
|
|
|
|
/* border: 1px solid #eee; */
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 左侧信息列表样式 */
|
|
|
|
|
.info-list {
|
|
|
|
|
width: 450px;
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
/* border-right: 1px solid #eee; */
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.info-container {
|
|
|
|
|
/* margin-bottom:-16px;
|
|
|
|
|
padding-bottom:16px */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-item-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
background: #284c89;
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
height: 30px;
|
|
|
|
|
}
|
|
|
|
|
.info-item-txt {
|
|
|
|
|
line-height: 25px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-title-cell {
|
|
|
|
|
flex: 1;
|
|
|
|
|
text-align: center;
|
|
|
|
|
line-height: 30px;
|
|
|
|
|
border-right: 1px solid #eee;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
.info-title-num {
|
|
|
|
|
width: 50px;
|
|
|
|
|
flex: none !important;
|
|
|
|
|
}
|
|
|
|
|
.info-title-no {
|
|
|
|
|
width: 125px;
|
|
|
|
|
flex: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
height: 100%;
|
|
|
|
|
padding: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.info-cell {
|
|
|
|
|
flex: 1;
|
|
|
|
|
text-align: center;
|
|
|
|
|
/* padding: 5px; */
|
|
|
|
|
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: hidden;
|
|
|
|
|
/* 添加固定高度,确保滚动条不会挤压内容 */
|
|
|
|
|
/* height: calc(100vh - 240px); */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 图表X轴区域样式 */
|
|
|
|
|
.chart-axis {
|
|
|
|
|
height: 30px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-labels {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 30px;
|
|
|
|
|
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: 30px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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: 30px;
|
|
|
|
|
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;
|
|
|
|
|
/* background-color: #ff4d4f; */
|
|
|
|
|
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;
|
|
|
|
|
/* 预留横向滚动条高度 */
|
|
|
|
|
/* padding-bottom: 16px;
|
|
|
|
|
margin-bottom: -16px; */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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%;
|
|
|
|
|
/* height:500px; */
|
|
|
|
|
|
|
|
|
|
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: 18px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.task-bar:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.task-label {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
.task-label-txt {
|
|
|
|
|
position: absolute;
|
|
|
|
|
/* top: 8px; */
|
|
|
|
|
left: -5px;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
color: white;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 提示框样式 */
|
|
|
|
|
.tooltip {
|
|
|
|
|
position: fixed;
|
|
|
|
|
background-color: white;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 5px;
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tooltip-content div {
|
|
|
|
|
margin: 3px 0;
|
|
|
|
|
}
|
|
|
|
|
.wo-code-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #1f2d3d;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
border-bottom: 1px solid #e6e6e6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-list {
|
|
|
|
|
list-style: none;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
|
|
.detail-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 5px;
|
|
|
|
|
line-height: 20px;
|
|
|
|
|
|
|
|
|
|
.label {
|
|
|
|
|
width: 100px;
|
|
|
|
|
color: #666;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.value {
|
|
|
|
|
flex: 1;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 分页样式 */
|
|
|
|
|
.pagination-container {
|
|
|
|
|
/* position: fixed; */
|
|
|
|
|
/* right: 20px;
|
|
|
|
|
bottom: 20px; */
|
|
|
|
|
height: 35px;
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
background-color: #fff; /* 增加背景色避免与内容重叠时看不清 */
|
|
|
|
|
padding: 10px; /* 增加内边距 */
|
|
|
|
|
z-index: 10;
|
|
|
|
|
float: right;
|
|
|
|
|
width: calc(100% - 20px);
|
|
|
|
|
/* width:100%; */
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-pagination) {
|
|
|
|
|
float: right;
|
|
|
|
|
}
|
|
|
|
|
:deep(.el-button--primary) {
|
|
|
|
|
background-color: #284c89 !important;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
</style>
|