<template>
|
<div style="padding: 0 10px">
|
|
<div style="padding: 10px 0;display: flex;">
|
<el-select
|
v-model="scaleValue"
|
size="mini"
|
placeholder="请选择"
|
@change="val=>changeTimeScale(val,true)"
|
>
|
<el-option
|
v-for="item in scaleArr"
|
:key="item.code"
|
:label="item.name"
|
:value="item.code"
|
/>
|
</el-select>
|
<el-select
|
v-model="priorityMethod"
|
size="mini"
|
style="margin-left: 10px;"
|
placeholder="请选择"
|
@change="priorityMethodChange"
|
>
|
<el-option
|
v-for="item in priorityMethodArr"
|
:key="item.code"
|
:label="item.name"
|
:value="item.code"
|
/>
|
</el-select>
|
<el-date-picker
|
v-model="ganttDateRange"
|
style="margin-left: 10px;"
|
size="mini"
|
value-format="yyyy-MM-dd"
|
type="daterange"
|
:clearable="false"
|
:picker-options="pickerOptions"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
@change="ganttDateRangeChange"
|
/>
|
<el-button type="primary" style="margin-left: 10px;" size="mini" @click="handleGetSelected">
|
获取复选框选中任务
|
</el-button>
|
<el-button size="mini" @click="handleClearSelection">
|
清空复选框选择
|
</el-button>
|
|
<el-button size="mini" type="primary" @click="prepareArrange">
|
预排
|
</el-button>
|
<el-button size="mini" disabled>
|
预排进度:{{ canArrangeNumber }}/{{ needArrangeNumber }}
|
</el-button>
|
|
</div>
|
|
<div id="gantt_here" style="width:100%; height:calc(90vh - 50px);" />
|
|
<!-- 分页组件 -->
|
<div class="pagination-container">
|
<el-pagination
|
:current-page="currentPage"
|
:page-sizes="[10, 20, 50, 100]"
|
:page-size="pageSize"
|
layout="total, sizes, prev, pager, next, jumper"
|
:total="totalTasks"
|
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
/>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { gantt } from '@/components/dhtmlxGantt' // 目前dhtmlxgantt版本8.0.x
|
import '@/components/dhtmlxGantt/codebase/dhtmlxgantt.css'
|
import { handleDateReduceOneDay, handleDatetime, handleDatetime2 } from '@/utils/global'
|
import { nanoid } from 'nanoid'
|
|
export default {
|
data() {
|
return {
|
scaleArr: [
|
{ code: '30min', name: '30min' },
|
{ code: '60min', name: '60min' },
|
{ code: '240min', name: '240min' },
|
{ code: '360min', name: '360min' }
|
],
|
scaleValue: '240min',
|
ganttDateRange: ['2026-01-22', '2026-01-25'], // '2026-01-20', '2026-01-25'
|
selectedIds: [],
|
// 分页相关数据
|
currentPage: 1,
|
pageSize: 100,
|
totalTasks: 0,
|
allTasks: [], // 存储所有任务数据
|
paginatedTasks: [], // 当前页的任务数据
|
|
fivePeriodsTimeName: ['OneStartDate', 'TwoStartDate', 'ThreeStartDate', 'FourStartDate', 'FiveStartDate'], // 五个时间段的键名
|
needArrangeNumber: 5000, // 假设需要排产数量5000
|
canArrangeNumber: 0, // 能排数量默认为 0
|
priorityMethod: 'device', // device 设备 time 时间
|
priorityMethodArr: [
|
{ code: 'device', name: '设备优先' },
|
{ code: 'time', name: '时间优先' }
|
],
|
pickerOptions: {
|
// disabledDate(time) {
|
// return time.getTime() <= Date.now() - 24 * 60 * 60 * 1000
|
// }
|
}
|
}
|
},
|
mounted() {
|
// 初始化甘特图配置
|
this.initGantt()
|
|
// 改变日期范围配置
|
this.ganttDateRangeChange(this.ganttDateRange)
|
|
// 然后加载任务数据(会自动渲染当前页)
|
// this.loadTasks()
|
},
|
methods: {
|
|
initGantt() {
|
gantt.plugins({
|
critical_path: true,
|
drag_timeline: true,
|
grouping: true,
|
keyboard_navigation: true,
|
marker: true,
|
multiselect: true,
|
tooltip: true,
|
undo: true
|
})
|
gantt.i18n.setLocale('cn')
|
gantt.config.multiselect = true // 开启多任务选择
|
gantt.config.show_links = false // 不显示连接线
|
|
// 不再对齐时间轴刻度(比如天格子),而是按“小时”对齐
|
gantt.config.round_dnd_dates = false
|
// 最小步长还是 1 小时,但你已经从“天格子”变成“小时格子”了
|
// gantt.config.time_step = 60 // 60 分钟 = 1 小时
|
gantt.config.time_step = 1 // 1分钟
|
|
gantt.config.row_height = 32 // 行高
|
gantt.config.bar_height = 20 // bar高
|
gantt.config.xml_date = '%Y-%m-%d %H:%i' // gantt的日期格式
|
gantt.config.drag_progress = false // 禁止通过拖动进度条改变任务进度
|
gantt.config.readonly = true // 只读模式
|
|
// 刻度值改变
|
this.changeTimeScale()
|
|
// 是否是工作时间
|
/* ↓↓↓ Working Time configuration ↓↓↓ */
|
// gantt.templates.scale_cell_class = function(date) {
|
// if (!gantt.isWorkTime(date)) {
|
// return 'weekend'
|
// }
|
// }
|
//
|
// gantt.templates.timeline_cell_class = function(item, date) {
|
// if (!gantt.isWorkTime({ date: date, task: item })) {
|
// return 'weekend'
|
// }
|
// }
|
//
|
// gantt.config.work_time = true
|
//
|
// gantt.addCalendar({
|
// id: 'customCalendar1',
|
// worktime: {
|
// hours: ['00:00-24:00'], // global work hours for weekdays
|
// days: [1, 1, 1, 1, 1, 1, 1]
|
// }
|
// })
|
|
// gantt.addCalendar({
|
// id: 'custom2',
|
// hours: ['12:00-21:00'],
|
// days: [1, 0, 1, 0, 1, 0, 1]
|
// })
|
/* ↑↑↑ Working Time configuration ↑↑↑ */
|
|
// 甘特图列参数设置
|
/* ↓↓↓ Grid Columns configuration ↓↓↓ */
|
gantt.config.reorder_grid_columns = true
|
const textEditor = { type: 'text', map_to: 'text' }
|
const startDateEditor = { type: 'date', map_to: 'start_date' }
|
const durationEditor = { type: 'number', map_to: 'duration', min: 1, max: 100 }
|
gantt.config.columns = [
|
{
|
name: 'checked',
|
label: '选择',
|
align: 'center',
|
width: 35,
|
resize: false,
|
// 关键:用 template 返回一个复选框
|
template: (task) => {
|
const checked = task.checked ? 'checked' : ''
|
// data-action 用于在事件委托时识别是复选框
|
return `<input type="checkbox" class="taskCheckBox" data-action="check-row" ${checked} />`
|
}
|
},
|
// { name: 'wbs', label: '节点', width: 80, template: gantt.getWBSCode },
|
{ name: 'text', tree: true, align: 'center', label: '任务名称', width: 240, resize: true, editor: textEditor },
|
{ name: 'saleOrder', align: 'center', label: '销售订单', width: 100, resize: true },
|
{ name: 'partName', align: 'center', label: '产品名称', width: 80, resize: true },
|
{ name: 'partCode', align: 'center', label: '产品编码', width: 80, resize: true },
|
{
|
name: 'progress', align: 'center', label: '进度', width: 120, resize: true, template: function(task) {
|
return `<input type="range" onmousedown="event.preventDefault()" onmouseup="event.preventDefault()" id="taskRange" value="${task.progress * 100}"/> ${task.progress * 100}%`
|
// return `<el-progress :percentage='${task.progress * 100}'></el-progress> ${task.progress * 100}%`
|
}
|
},
|
{ name: 'start_date', align: 'center', label: '开始日期', width: 80, resize: true, editor: startDateEditor },
|
{
|
name: 'duration',
|
width: 60,
|
align: 'center',
|
label: '时长(分钟)',
|
resize: true,
|
editor: durationEditor,
|
template: function(task) {
|
// 如果duration是null或undefined,返回0
|
// return (task.duration || 0) + 1 // 在当前duration的基础上加1
|
return task.duration
|
}
|
}
|
// { name: 'add', width: 44 }
|
]
|
/* ↑↑↑ Grid Columns configuration ↑↑↑ */
|
|
gantt.config.show_errors = false // 发生异常时,不允许弹出警告到 UI 界面
|
gantt.config.grid_elastic_columns = true
|
|
// 自定义浮动框的显示内容 tooltip浮动框显示的End Date被追加1的问题修复(应该显示数据库的原始值)
|
gantt.templates.tooltip_text = function(start, end, task) {
|
// return '<b>任务:</b> ' + task.text + '<br/><b>开始时间:</b> ' + `${gantt.date.date_to_str('%Y-%m-%d')(start)}` + '<br/><b>结束时间:</b> ' + handleDateReduceOneDay(end)
|
// return '<b>任务:</b> ' + task.text + '<br/><b>开始时间:</b> ' + handleDatetime2(start) + '<br/><b>结束时间:</b> ' + handleDateReduceOneDay(end) + '<br/><b>进度:</b> ' + task.progress * 100 + '%'
|
return '<b>任务:</b> ' + task.text +
|
'<br/><b>' + `${task.type === 'task' ? '产能' : '生产数量'}` + ' :</b> ' + task.producedCount +
|
'<br/><b>进度:</b> ' + task.progress * 100 + '%' +
|
'<br/><b>开始时间:</b> ' + handleDatetime2(start) +
|
'<br/><b>结束时间:</b> ' + handleDatetime2(end)
|
}
|
|
gantt.templates.task_text = function(start, end, task) {
|
// return '<span style="color: white; font-weight: bold; text-shadow: 1px 1px 1px #000;">' +
|
// task.description + ' - ' +
|
// '</span>'
|
// return task.description
|
// return task.progress * 100 + '%'
|
|
if (task.type === 'task2') {
|
return `<div class="task2Css">${task.producedCount}</div>`
|
// return task.producedCount
|
}
|
if (task.type === 'task3') {
|
return `<div class="task3Css">${task.producedCount}</div>`
|
}
|
return ''
|
}
|
|
// 设置持续时间单位为小时
|
// gantt.config.duration_unit = 'hour'
|
|
gantt.config.duration_unit = 'minute'
|
gantt.config.duration_step = 1
|
// gantt.config.show_task_cells = false //隐藏甘特图内部刻度线
|
gantt.init('gantt_here')
|
// 注意:这里不立即加载数据,而是等待loadTasks被调用
|
|
// 绑定甘特图点击事件(官方推荐的事件委托用法)
|
gantt.attachEvent('onTaskClick', (id, e) => {
|
// 找到点击的是否是复选框
|
const checkbox = e.target.closest('[data-action="check-row"]')
|
if (!checkbox) {
|
// 不是点复选框,就保持默认行为
|
return true
|
}
|
|
// 是复选框:切换选中状态
|
const task = gantt.getTask(id)
|
if (task) {
|
task.checked = !task.checked
|
// 同时更新全局数据
|
const globalTask = this.allTasks.find(t => t.id === id)
|
if (globalTask) {
|
globalTask.checked = task.checked
|
}
|
gantt.updateTask(id) // 只刷新这一行,性能更好
|
this.syncSelected() // 同步到 Vue 的 selectedIds
|
}
|
|
// 阻止默认点击行行为(避免误触发其他逻辑)
|
return false
|
})
|
|
// 初始化完成后同步一次选中状态
|
this.syncSelected()
|
|
// this.loadTasks()
|
},
|
|
// 加载任务数据
|
loadTasks() {
|
// 接口获取到的数据 //这是待排数据
|
const rows = [
|
{
|
'wo_code': null,
|
'YearDate': '2026-01-20',
|
'children': [
|
{
|
'AdvaDevicNumber': 'JG010',
|
'AdvaDevicName': '精工设备10#',
|
'AdvaDevicCropMob': '10',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
},
|
{
|
'AdvaDevicNumber': 'SB001',
|
'AdvaDevicName': '设备001',
|
'AdvaDevicCropMob': '30',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
}
|
]
|
},
|
{
|
'wo_code': null,
|
'YearDate': '2026-01-21',
|
'children': [
|
{
|
'AdvaDevicNumber': 'JG010',
|
'AdvaDevicName': '精工设备10#',
|
'AdvaDevicCropMob': '10',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
},
|
{
|
'AdvaDevicNumber': 'SB001',
|
'AdvaDevicName': '设备001',
|
'AdvaDevicCropMob': '30',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
}
|
]
|
},
|
{
|
'wo_code': null,
|
'YearDate': '2026-01-22',
|
'children': [
|
{
|
'AdvaDevicNumber': 'JG010',
|
'AdvaDevicName': '精工设备10#',
|
'AdvaDevicCropMob': '10',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
},
|
{
|
'AdvaDevicNumber': 'SB001',
|
'AdvaDevicName': '设备001',
|
'AdvaDevicCropMob': '30',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
}
|
]
|
},
|
{
|
'wo_code': null,
|
'YearDate': '2026-01-23',
|
'children': [
|
{
|
'AdvaDevicNumber': 'JG010',
|
'AdvaDevicName': '精工设备10#',
|
'AdvaDevicCropMob': '10',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
},
|
{
|
'AdvaDevicNumber': 'SB001',
|
'AdvaDevicName': '设备001',
|
'AdvaDevicCropMob': '30',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
}
|
]
|
},
|
{
|
'wo_code': null,
|
'YearDate': '2026-01-24',
|
'children': [
|
{
|
'AdvaDevicNumber': 'JG010',
|
'AdvaDevicName': '精工设备10#',
|
'AdvaDevicCropMob': '10',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
},
|
{
|
'AdvaDevicNumber': 'SB001',
|
'AdvaDevicName': '设备001',
|
'AdvaDevicCropMob': '30',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
}
|
]
|
},
|
{
|
'wo_code': null,
|
'YearDate': '2026-01-25',
|
'children': [
|
{
|
'AdvaDevicNumber': 'JG010',
|
'AdvaDevicName': '精工设备10#',
|
'AdvaDevicCropMob': '10',
|
'AdvaDevicRhythm': '5.0',
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
},
|
{
|
'AdvaDevicNumber': 'SB001',
|
'AdvaDevicName': '设备001',
|
'AdvaDevicCropMob': '30', // 稼动率 需要除100
|
'AdvaDevicRhythm': '5.0', // 生产节拍
|
'OneStartDate': '08:00~11:30',
|
'TwoStartDate': '13:00~18:00',
|
'ThreeStartDate': '',
|
'FourStartDate': '',
|
'FiveStartDate': ''
|
}
|
]
|
}
|
]
|
|
// 这是已排数据
|
const Cont = [
|
{
|
'wo_code': 'MO-2023-06-0007_1',
|
'eqp_code': 'JG010',
|
'time_start': '2026-01-21 13:51:55',
|
'time_end': '2026-01-21 18:00:00',
|
'status': 'S',
|
'alloc_qty': 298.00,
|
'part_code': '302',
|
'part_name': '8504光机',
|
'uom_name': '只'
|
},
|
{
|
'wo_code': 'MO-2023-06-0007_1',
|
'eqp_code': 'JG010',
|
'time_start': '2026-01-22 08:00:00',
|
'time_end': '2026-01-22 11:30:00',
|
'status': 'S',
|
'alloc_qty': 252.00,
|
'part_code': '302',
|
'part_name': '8504光机',
|
'uom_name': '只'
|
},
|
{
|
'wo_code': 'MO-2023-06-0007_1',
|
'eqp_code': 'JG010',
|
'time_start': '2026-01-22 13:00:00',
|
'time_end': '2026-01-22 18:00:00',
|
'status': 'S',
|
'alloc_qty': 360,
|
'part_code': '302',
|
'part_name': '8504光机',
|
'uom_name': '只'
|
},
|
{
|
'wo_code': 'MO-2023-06-0007_1',
|
'eqp_code': 'JG010',
|
'time_start': '2026-01-23 08:00:00',
|
'time_end': '2026-01-23 11:30:00',
|
'status': 'S',
|
'alloc_qty': 252.00,
|
'part_code': '302',
|
'part_name': '8504光机',
|
'uom_name': '只'
|
},
|
{
|
'wo_code': 'MO-2023-06-0007_1',
|
'eqp_code': 'JG010',
|
'time_start': '2026-01-23 13:00:00',
|
'time_end': '2026-01-23 15:00:00',
|
'status': 'S',
|
'alloc_qty': 144.00,
|
'part_code': '302',
|
'part_name': '8504光机',
|
'uom_name': '只'
|
}
|
// {
|
// 'wo_code': 'MO-2023-06-0007_1',
|
// 'eqp_code': 'JG010',
|
// 'time_start': '2026-01-24 08:00:00',
|
// 'time_end': '2026-01-24 11:30:00',
|
// 'status': 'S',
|
// 'alloc_qty': 252.00,
|
// 'part_code': '302',
|
// 'part_name': '8504光机',
|
// 'uom_name': '只'
|
// },
|
// {
|
// 'wo_code': 'MO-2023-06-0007_1',
|
// 'eqp_code': 'JG010',
|
// 'time_start': '2026-01-24 13:00:00',
|
// 'time_end': '2026-01-24 18:00:00',
|
// 'status': 'S',
|
// 'alloc_qty': 360.00,
|
// 'part_code': '302',
|
// 'part_name': '8504光机',
|
// 'uom_name': '只'
|
// },
|
// {
|
// 'wo_code': 'MO-2023-06-0007_1',
|
// 'eqp_code': 'JG010',
|
// 'time_start': '2026-01-25 08:00:00',
|
// 'time_end': '2026-01-25 11:30:00',
|
// 'status': 'S',
|
// 'alloc_qty': 252.00,
|
// 'part_code': '302',
|
// 'part_name': '8504光机',
|
// 'uom_name': '只'
|
// },
|
// {
|
// 'wo_code': 'MO-2023-06-0007_1',
|
// 'eqp_code': 'JG010',
|
// 'time_start': '2026-01-25 13:00:00',
|
// 'time_end': '2026-01-25 18:00:00',
|
// 'status': 'S',
|
// 'alloc_qty': 360.00,
|
// 'part_code': '302',
|
// 'part_name': '8504光机',
|
// 'uom_name': '只'
|
// }
|
]
|
|
const newArr = []
|
// 这一步的操作主要是要做产能背景的显示
|
rows.forEach((item, index) => {
|
// 数据接口返回的时间范围要在日期选择范围之内
|
if (new Date(item.YearDate).getTime() >= new Date(this.ganttDateRange[0]).getTime() && new Date(item.YearDate).getTime() <= new Date(this.ganttDateRange[1]).getTime()) {
|
item.children.forEach((it, ind) => {
|
// 这里应该要生成一个以设备维度为基础的数组 不重不漏
|
if (!newArr.map(i => i.partCode).includes(it.AdvaDevicNumber)) {
|
newArr.push({
|
id: it.AdvaDevicNumber,
|
type: 'project',
|
text: '我是父级',
|
partName: it.AdvaDevicName,
|
partCode: it.AdvaDevicNumber,
|
start_date: handleDatetime2(item.YearDate + ' ' + it.OneStartDate.split('~')[0]), // 这个是无效的,只是为了预排prepareArrange方法里面不报错
|
end_date: handleDatetime2(item.YearDate + ' ' + it.OneStartDate.split('~')[1]), // 这个是无效的,只是为了预排prepareArrange方法里面不报错
|
// duration: this.calculateTimeRangeInMinutes(it.OneStartDate),
|
checked: false,
|
progress: 0.6,
|
parent: 0,
|
saleOrder: 'SO-2026-01001',
|
open: true
|
})
|
console.log(it.AdvaDevicNumber + ind.toString(), 999)
|
newArr.push({
|
id: it.AdvaDevicNumber + ind.toString(),
|
// id: it.AdvaDevicNumber,
|
type: 'project',
|
text: '任务名称预留',
|
partName: it.AdvaDevicName,
|
partCode: it.AdvaDevicNumber,
|
start_date: handleDatetime2(item.YearDate + ' ' + it.OneStartDate.split('~')[0]), // 这个是无效的,只是为了预排prepareArrange方法里面不报错
|
end_date: handleDatetime2(item.YearDate + ' ' + it.OneStartDate.split('~')[1]), // 这个是无效的,只是为了预排prepareArrange方法里面不报错
|
// duration: this.calculateTimeRangeInMinutes(it.OneStartDate),
|
render: 'split', // 用于在一个工作时间段内显示不下,需要进行分割显示
|
checked: false,
|
progress: 0,
|
parent: it.AdvaDevicNumber,
|
// parent: it.AdvaDevicNumber,
|
saleOrder: 'SO-2026-01001'
|
})
|
}
|
|
// 因为是五个时间段,所有要有个循环次数为5的循环
|
for (let i = 0; i < 5; i++) { // 这次循环是为了显示产能
|
if (it[this.fivePeriodsTimeName[i]]) {
|
const duration = this.calculateTimeRangeInMinutes(it[this.fivePeriodsTimeName[i]]) // 工期 单位 分钟
|
console.log(it.AdvaDevicNumber + ind.toString(), 123)
|
newArr.push({
|
// id: index.toString() + ind.toString() + i.toString(),
|
id: nanoid(),
|
type: 'task',
|
text: '任务名称预留',
|
partName: it.AdvaDevicName,
|
partCode: it.AdvaDevicNumber,
|
start_date: handleDatetime2(item.YearDate + ' ' + it[this.fivePeriodsTimeName[i]].split('~')[0]),
|
// start_date: new Date(handleDatetime2(item.YearDate + ' ' + it[this.fivePeriodsTimeName[i]].split('~')[0])).getTime() < new Date().getTime() ? handleDatetime2(new Date()) : handleDatetime2(item.YearDate + ' ' + it[this.fivePeriodsTimeName[i]].split('~')[0]),
|
// end_date: new Date(handleDatetime2(item.YearDate + ' ' + it[this.fivePeriodsTimeName[i]].split('~')[1])).getTime() < new Date().getTime() ? handleDatetime2(new Date()) : handleDatetime2(item.YearDate + ' ' + it[this.fivePeriodsTimeName[i]].split('~')[1]),
|
end_date: handleDatetime2(item.YearDate + ' ' + it[this.fivePeriodsTimeName[i]].split('~')[1]),
|
duration,
|
checked: false,
|
progress: 0,
|
parent: it.AdvaDevicNumber + ind.toString(),
|
// parent: it.AdvaDevicNumber,
|
saleOrder: 'SO-2026-01001',
|
// 要在每一个时间段内算出能生产多少个 工期(分钟)乘以60 除以生产节拍 * 稼动率
|
producedCount: (duration * 60 / it.AdvaDevicRhythm) * (it.AdvaDevicCropMob / 100),
|
AdvaDevicRhythm: it.AdvaDevicRhythm, // 生产节拍
|
AdvaDevicCropMob: it.AdvaDevicCropMob // 稼动率 需要除100
|
})
|
}
|
}
|
})
|
}
|
})
|
|
// 这一步的操作是做已排的显示
|
const scheduledDevices = [...new Set(Cont.map(i => i.eqp_code))]// 这是已排的设备编码
|
Cont.forEach(item => {
|
if (scheduledDevices.includes(item.eqp_code)) {
|
newArr.push({
|
id: nanoid(),
|
type: 'task3',
|
text: '任务名称预留',
|
partName: item.part_name,
|
partCode: item.part_code,
|
start_date: item.time_start,
|
end_date: item.time_end,
|
// duration: this.calculateTimeRangeInMinutes(it.OneStartDate),
|
duration: this.calculateTimeRangeInMinutes(item.time_start.split(' ')[1] + '~' + item.time_end.split(' ')[1]),
|
checked: false,
|
progress: 0,
|
parent: item.eqp_code + '0',
|
saleOrder: 'SO-2026-01001',
|
producedCount: item.alloc_qty
|
})
|
}
|
})
|
|
// task 代表的是产能 task2 代表的是可以排产的值 task3 代表的是已排产的值
|
// task2 的值得从 task减去task3的时间 代表可排产时间
|
// 若同一父节点的值相同时,当task的开始时间和结束时间与task3相等时,代表此段不能再排产
|
// 当task的开始时间等于task3的开始时间,但task的结束时间大于task3的结束时间时,这时候,可排产时间为:task3的结束时间到task的结束时间
|
const task = newArr.filter(item => item.type === 'task')
|
const task3 = newArr.filter(item => item.type === 'task3')
|
task.forEach(item => { // 总产能
|
task3.forEach(it => { // 已排数据
|
if (item.parent === it.parent) { // 说明是在同一个设备下
|
// 当两个时间相等时说明肯定不能排产了
|
if (new Date(item.start_date).getTime() === new Date(it.start_date).getTime() && new Date(item.end_date).getTime() === new Date(it.end_date).getTime()) {
|
item.schedulingPossible = false
|
}
|
|
if (new Date(item.start_date).getTime() === new Date(it.start_date).getTime() && new Date(item.end_date).getTime() > new Date(it.end_date).getTime()) {
|
item.start_date2 = it.end_date
|
}
|
|
// 不知道要不要注释掉 待验证
|
// if (new Date(item.start_date).getTime() < new Date().getTime() && item.producedCount !== it.producedCount) {
|
// item.start_date2 = handleDatetime2(new Date())
|
// }
|
}
|
})
|
})
|
|
// 使用原有的示例数据作为基础
|
this.allTasks = newArr.filter(i => i.schedulingPossible !== false)
|
// this.allTasks = newArr
|
|
this.totalTasks = this.allTasks.length
|
this.updatePaginatedTasks()
|
this.renderGanttChart()
|
|
// this.prepareArrange()
|
},
|
|
// 更新分页后的任务数据
|
updatePaginatedTasks() {
|
const startIndex = (this.currentPage - 1) * this.pageSize
|
const endIndex = Math.min(startIndex + this.pageSize, this.allTasks.length)
|
this.paginatedTasks = this.allTasks.slice(startIndex, endIndex)
|
// this.paginatedTasks = JSON.parse(JSON.stringify(this.allTasks.slice(startIndex, endIndex)))
|
},
|
|
// 拆分时间字符串并分别计算分钟值
|
calculateTimeRangeInMinutes(timeRangeStr) {
|
// 分割字符串,获取开始时间和结束时间
|
const [startTimeStr, endTimeStr] = timeRangeStr.split('~')
|
|
// 将时间字符串(HH:MM)转换为总分钟数
|
const timeStringToMinutes = (timeStr) => {
|
const [hours, minutes] = timeStr.split(':').map(Number)
|
return hours * 60 + (minutes || 0)
|
}
|
|
const startMinutes = timeStringToMinutes(startTimeStr)
|
const endMinutes = timeStringToMinutes(endTimeStr)
|
|
// 返回时间差(分钟)
|
return endMinutes - startMinutes
|
},
|
|
// 刻度值改变
|
changeTimeScale(val, boolean) {
|
let scaleConfig
|
switch (this.scaleValue) {
|
case '30min':
|
scaleConfig = [
|
{ unit: 'day', step: 1, format: '%Y-%m-%d 星期%D' },
|
{ unit: 'minute', step: 30, format: '%H:%i' } // 子尺度
|
]
|
break
|
case '60min':
|
scaleConfig = [
|
{ unit: 'day', step: 1, format: '%Y-%m-%d 星期%D' },
|
// { unit: 'hour', step: 1, format: '%H:%i' },
|
{ unit: 'minute', step: 60, format: '%H:%i' }
|
]
|
break
|
case '240min':
|
scaleConfig = [
|
{ unit: 'day', step: 1, format: '%Y-%m-%d 星期%D' },
|
{ unit: 'minute', step: 240, format: '%H:%i' } // 每4小时一个刻度
|
]
|
break
|
case '360min':
|
scaleConfig = [
|
{ unit: 'day', step: 1, format: '%Y-%m-%d 星期%D' },
|
{ unit: 'minute', step: 360, format: '%H:%i' } // 每6小时一个刻度
|
// { unit: 'hour', step: 6, format: '%H:%i' } // 每6小时一个刻度
|
]
|
break
|
default:
|
scaleConfig = [
|
{ unit: 'hour', step: 1, format: '%Y-%m-%d 星期%D' },
|
{ unit: 'minute', step: 60, format: '%H:%i' }
|
]
|
}
|
gantt.config.start_date = new Date(this.ganttDateRange[0] + ' 00:00')
|
gantt.config.end_date = new Date(this.ganttDateRange[1] + ' 24:00')
|
gantt.config.scales = scaleConfig
|
if (boolean) {
|
// gantt.render()// gantt重绘
|
this.renderGanttChart()
|
}
|
},
|
|
// 渲染甘特图
|
renderGanttChart() {
|
gantt.clearAll()
|
console.log(JSON.parse(JSON.stringify(this.paginatedTasks)))
|
gantt.parse({
|
'data': this.paginatedTasks
|
})
|
// 确保甘特图重新渲染
|
// gantt.render()
|
},
|
|
// 页大小改变
|
handleSizeChange(newSize) {
|
console.log('执行2')
|
this.pageSize = newSize
|
this.currentPage = 1 // 重置到第一页
|
this.updatePaginatedTasks()
|
this.renderGanttChart()
|
this.syncSelected()
|
},
|
|
// 当前页改变
|
handleCurrentChange(newPage) {
|
console.log('执行1')
|
// 计算最大页数,防止超出范围
|
const maxPage = Math.ceil(this.totalTasks / this.pageSize)
|
if (newPage > maxPage) {
|
this.currentPage = maxPage
|
} else if (newPage < 1) {
|
this.currentPage = 1
|
} else {
|
this.currentPage = newPage
|
}
|
|
this.updatePaginatedTasks()
|
this.renderGanttChart()
|
this.syncSelected()
|
},
|
// 甘特图日期改变
|
ganttDateRangeChange(val) {
|
this.priorityMethodChange()// 清空已排值
|
gantt.config.start_date = new Date(val[0] + ' 00:00')
|
gantt.config.end_date = new Date(val[1] + ' 24:00')
|
|
this.loadTasks()
|
|
// gantt.render()
|
},
|
// 从甘特图中同步选中的 id 到 Vue data
|
syncSelected() {
|
// 同步当前页面任务到全局数据
|
gantt.eachTask((task) => {
|
const globalTask = this.allTasks.find(t => t.id === task.id)
|
if (globalTask) {
|
globalTask.checked = task.checked
|
}
|
})
|
|
// 获取所有选中的任务ID
|
// this.selectedIds = [...new Set(this.allTasks.filter(t => t.checked).map(t => t.id))]//数组去重
|
this.selectedIds = this.allTasks.filter(t => t.checked).map(t => t.id)
|
console.log(this.selectedIds)
|
},
|
|
// 获取选中任务(示例)
|
handleGetSelected() {
|
const selected = this.allTasks.filter(t => t.checked)
|
// this.$notify.success(`当前已选中${selected.length} 条任务`)
|
this.$notify.success(`点击了`)
|
},
|
|
// 清空所有选择
|
handleClearSelection() {
|
// 遍历所有任务,将 checked 属性设置为 false
|
this.allTasks.forEach(task => {
|
task.checked = false
|
})
|
|
// 更新当前页面显示的任务
|
gantt.eachTask((task) => {
|
task.checked = false
|
gantt.updateTask(task.id)
|
})
|
|
// 同步到 Vue 组件数据
|
this.syncSelected()
|
|
// 显示提示信息
|
this.$notify.success('已清空所有选择')
|
},
|
// 预排
|
prepareArrange() {
|
this.priorityMethodChange()
|
|
this.loadTasks()
|
|
// 优先方式 time device
|
if (this.priorityMethod === 'time') {
|
this.allTasks.sort((a, b) => a.start_date - b.start_date)
|
}
|
if (this.priorityMethod === 'device') {
|
this.allTasks.sort((a, b) => parseFloat(a.AdvaDevicCropMob) - parseFloat(b.AdvaDevicCropMob))
|
}
|
|
// 在这个循环里面还得考虑一个点,在已排的数据上不能再排了
|
// 相当于在task 和task3中 要在task里面剔除掉task3的时间段
|
const newArr = []
|
// this.canArrangeNumber = 0
|
let needArrangeNumber = this.needArrangeNumber
|
this.allTasks.forEach(item => {
|
// if (item.type === 'task') { // 这里的判断条件还得加个日期判断
|
if (item.type === 'task' &&
|
new Date(item.start_date).getTime() >= new Date(this.ganttDateRange[0] + ' 00:00:00').getTime() &&
|
new Date(item.end_date).getTime() >= new Date().getTime()) { // 这里的判断条件还得加个日期判断 结束时间要大于目前时间
|
let ratio = 1 // 默认系数 1
|
|
if (item.start_date2) {
|
const d = this.calculateTimeRangeInMinutes(item.start_date2.split(' ')[1] + '~' + handleDatetime2(item.end_date).split(' ')[1])
|
ratio = Math.round((d / item.duration) * 100) / 100
|
}
|
|
if (
|
new Date(item.start_date).getTime() < new Date().getTime() &&
|
new Date(item.end_date).getTime() >= new Date().getTime()
|
) {
|
const d = this.calculateTimeRangeInMinutes(handleDatetime2(new Date()).split(' ')[1] + '~' + handleDatetime2(item.end_date).split(' ')[1])
|
ratio = Math.round((d / item.duration) * 100) / 100
|
}
|
|
// 这个地方的count值 得变更 item.producedCount 得乘以个系数 默认系数 1
|
const count = needArrangeNumber > 0 && needArrangeNumber <= item.producedCount * ratio ? needArrangeNumber : item.producedCount * ratio
|
needArrangeNumber = needArrangeNumber - item.producedCount * ratio // 剩余待排值
|
|
if (count > 0 && (needArrangeNumber > 0 || Math.abs(needArrangeNumber) < item.producedCount * ratio)) { // 一定是大于零且小于整条的生产值的
|
// duration 单位 分钟
|
const duration = (count / (item.AdvaDevicCropMob / 100)) * item.AdvaDevicRhythm / 60
|
// if (count < item.producedCount && new Date().getTime() <= new Date(item.start_date).getTime()) {
|
// duration = duration * (count / item.producedCount) //好像注释掉就对了 待验证
|
// }
|
|
const obj = {
|
id: nanoid(),
|
type: 'task2',
|
text: '任务名称111',
|
partName: item.partName,
|
partCode: item.partCode,
|
start_date: new Date(item.start_date).getTime() < new Date().getTime() ? handleDatetime2(new Date()) : (item.start_date2 ? item.start_date2 : handleDatetime2(item.start_date)),
|
// end_date: handleDatetime2(item.end_date),
|
// end_date: item.end_date,
|
duration, // 代表的是进度条
|
checked: false,
|
progress: 0,
|
parent: item.parent,
|
saleOrder: item.saleOrder,
|
producedCount: count <= item.producedCount ? Math.round(count) : item.producedCount
|
// producedCount: count <= item.producedCount ? count : item.producedCount
|
}
|
this.canArrangeNumber += parseFloat(obj.producedCount)
|
if (Math.abs(this.canArrangeNumber - this.needArrangeNumber) === 1) {
|
obj.producedCount = obj.producedCount + (this.needArrangeNumber - this.canArrangeNumber)
|
}
|
|
newArr.push({ ...obj })
|
}
|
}
|
})
|
|
// for (let i = 0; i < this.allTasks.length; i++) {
|
//
|
// }
|
|
this.allTasks = [...this.allTasks, ...newArr]
|
|
console.log(JSON.parse(JSON.stringify(this.allTasks)), '888')
|
|
this.totalTasks = this.allTasks.length
|
this.updatePaginatedTasks()
|
this.renderGanttChart()
|
},
|
// 清空已排值
|
priorityMethodChange() {
|
this.canArrangeNumber = 0
|
this.allTasks = this.allTasks.filter(i => i.type !== 'task2')
|
this.totalTasks = this.allTasks.length
|
this.updatePaginatedTasks()
|
this.renderGanttChart()
|
}
|
}
|
}
|
|
</script>
|
|
<style>
|
body,
|
html {
|
width: 100%;
|
height: 100%;
|
margin: unset;
|
}
|
|
/*.gantt_task_cell {*/
|
/* background: rgba(5, 185, 100, .1);*/
|
/*}*/
|
|
/*!*非工作日*!*/
|
/*.weekend {*/
|
/* background: rgba(255, 255, 255, 0.1);*/
|
/*}*/
|
|
/*.row-completed {*/
|
/* background-color: #bee4be !important; !* 浅绿色 *!*/
|
/*}*/
|
|
/*.gantt_grid_head_cell[data-column-id="duration"],*/
|
/*.gantt_row .gantt_cell[data-column-name="duration"] {*/
|
/* background-color: #f5f5f5 !important;*/
|
/*}*/
|
|
/* 为任务条内部的进度条添加圆角,保持视觉统一 */
|
/*.gantt_task_progress {*/
|
/*border-radius: 10px !important; !* 建议与任务条半径一致 *!*/
|
/*}*/
|
|
/*.gantt_bar_task {*/
|
/*border-radius: 10px !important;*/
|
/*padding: 3px !important;*/
|
/*transform: scaleX(-1) !important;*/
|
/*}*/
|
|
/*.gantt_bar_project {*/
|
/*border-radius: 10px !important;*/
|
/*}*/
|
|
.taskCheckBox {
|
cursor: pointer;
|
}
|
|
/* 分页容器样式 */
|
.pagination-container {
|
margin-top: 10px;
|
display: flex;
|
justify-content: end;
|
padding: 10px 0;
|
background-color: #fff;
|
border-top: 1px solid #ebeef5;
|
}
|
|
#taskRange {
|
width: 80px;
|
height: 6px;
|
border-radius: 2px;
|
}
|
|
/* 隐藏滑块 */
|
#taskRange::-webkit-slider-thumb {
|
opacity: 0;
|
-webkit-appearance: none !important;
|
width: 1px !important;
|
height: 1px !important;
|
}
|
|
#taskRange::-moz-range-thumb {
|
opacity: 0;
|
width: 1px !important;
|
height: 1px !important;
|
}
|
|
#taskRange::-ms-thumb {
|
opacity: 0;
|
width: 1px !important;
|
height: 1px !important;
|
}
|
|
.task2Css {
|
width: 100%;
|
height: 100%;
|
margin-left: 0;
|
background-color: #ac96ff;
|
}
|
|
.task3Css {
|
width: 100%;
|
height: 100%;
|
margin-left: 0;
|
background-color: rgb(255, 145, 0);
|
}
|
|
.gantt_scale_cell {
|
border: none !important;
|
}
|
|
.gantt_task_scale {
|
margin-left: -35px;
|
}
|
|
.gantt_scale_line:last-child {
|
width: 105%;
|
}
|
|
/*.gantt_task_bg:first-child > .gantt_task_row:first-child > .gantt_last_cell {*/
|
/* border-top: 1px solid rgb(166, 166, 166);*/
|
/*}*/
|
</style>
|