You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/mes/tasksummary/components/TaskSchedulePreviewDialog.vue

918 lines
32 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<Dialog v-model="previewVisible" title="排产甘特图预览" width="100%" align-center>
<div class="schedule-preview-wrap">
<div ref="ganttContainerRef" class="schedule-gantt-container"></div>
<div class="schedule-detail-panel">
<div class="schedule-detail-title">计划信息</div>
<el-descriptions :column="1" border size="small" v-if="activePreviewDevice">
<el-descriptions-item label="设备名称">{{ activePreviewDevice.deviceName }}</el-descriptions-item>
<el-descriptions-item label="设备ID">{{ activePreviewDevice.deviceId }}</el-descriptions-item>
<el-descriptions-item label="产能">{{ activePreviewDevice.ratedCapacity ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="计划条数">{{ activePreviewDevice.plans?.length ?? 0 }}</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无计划信息" :image-size="80" />
<div class="schedule-plan-list-title">计划明细</div>
<div class="schedule-plan-list">
<div
v-for="(plan, index) in activePreviewDevice?.plans ?? []"
:key="`${activePreviewDevice?.deviceId}-${plan.taskDetailId}-${index}`"
:class="['schedule-plan-item', { 'schedule-plan-item-active': plan.sourceType === 'CURRENT' }]"
>
<div class="schedule-plan-item-head">
<span class="schedule-plan-item-title">{{ plan.productCode ?? '-' }} / {{ plan.productName ?? '-' }}</span>
<!-- <el-tag v-if="plan.sourceType === 'CURRENT'" type="success" size="small">进行中</el-tag> -->
</div>
<div>任务明细ID{{ plan.taskDetailId ?? '-' }}</div>
<div>计划数量:{{ plan.planNumber ?? '-' }}</div>
<div>交货日期:{{ plan.deliveryDateStr ?? '-' }}</div>
<div>开始:{{ plan.planStartTimeStr || '-' }}</div>
<div>结束:{{ plan.planEndTimeStr || '-' }}</div>
<div>最晚开工:{{ plan.latestStartTimeStr || '-' }}</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button type="primary" :loading="previewSaveLoading" @click="handlePreviewSave">保存</el-button>
<el-button @click="previewVisible = false">关闭</el-button>
</template>
</Dialog>
<el-dialog v-model="taskAdjustDialogVisible" title="调整任务" width="420px" append-to-body>
<el-form label-width="90px">
<el-form-item label="设备">
<el-select v-model="taskAdjustForm.deviceTaskId" placeholder="请选择设备" class="!w-full">
<el-option v-for="item in previewDeviceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="taskAdjustForm.startDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择开始日期"
class="!w-full"
/>
</el-form-item>
<el-form-item label="天数">
<el-input-number v-model="taskAdjustForm.duration" :min="1" :max="365" class="!w-full" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="taskAdjustDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleTaskAdjustSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { PlanApi } from '@/api/mes/plan'
import dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
defineOptions({ name: 'TaskSchedulePreviewDialog' })
const props = defineProps<{
modelValue: boolean
scheduleList: any[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'saved'): void
}>()
const message = useMessage()
const previewSaveLoading = ref(false)
const ganttContainerRef = ref<HTMLDivElement>()
const activePreviewDevice = ref<any>()
const ganttEventIds = ref<string[]>([])
const ganttSyncing = ref(false)
const taskAdjustDialogVisible = ref(false)
const taskAdjustTaskId = ref<string | number | null>(null)
const taskAdjustForm = reactive({
deviceTaskId: '',
startDate: '',
duration: 1
})
const taskMoveFromParentMap = ref<Record<string, string | number | undefined>>({})
const taskDragDurationMap = ref<Record<string, number>>({})
const previewVisible = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value)
})
const previewScheduleList = computed(() => (Array.isArray(props.scheduleList) ? props.scheduleList : []))
const previewDeviceOptions = computed(() =>
previewScheduleList.value.map((device: any) => ({
label: device?.deviceName ?? '-',
value: `device-${device?.deviceId}`
}))
)
const getGlobalDateRange = (scheduleList: any[]) => {
const allPlans = scheduleList.flatMap((item: any) => item?.plans ?? [])
const starts = allPlans.map((item: any) => dayjs(item?.planStartTimeStr).valueOf()).filter((item: number) => Number.isFinite(item))
const ends = allPlans.map((item: any) => dayjs(item?.planEndTimeStr).valueOf()).filter((item: number) => Number.isFinite(item))
if (!starts.length || !ends.length) {
const now = Date.now()
return {
start: now,
end: now + 7 * 24 * 60 * 60 * 1000
}
}
return {
start: Math.min(...starts),
end: Math.max(...ends)
}
}
const formatGanttDate = (value: unknown) => {
const date = dayjs(value)
if (!date.isValid()) return undefined
return date.format('YYYY-MM-DD HH:mm')
}
const getDeviceTaskRangeByChildren = (planTasks: any[]) => {
const starts = planTasks.map((item: any) => dayjs(item?.start_date)).filter((item: any) => item.isValid())
const ends = planTasks.map((item: any) => dayjs(item?.end_date)).filter((item: any) => item.isValid())
if (!starts.length || !ends.length) return undefined
const earliestStart = starts.reduce((min: any, current: any) => (current.valueOf() < min.valueOf() ? current : min))
const latestEnd = ends.reduce((max: any, current: any) => (current.valueOf() > max.valueOf() ? current : max))
const duration = Math.max(latestEnd.endOf('day').diff(earliestStart.startOf('day'), 'day') + 1, 1)
return {
start_date: earliestStart.toDate(),
end_date: latestEnd.toDate(),
duration
}
}
const buildPreviewGanttData = (scheduleList: any[]) => {
const tasks: any[] = []
const links: any[] = []
let linkIndex = 1
scheduleList.forEach((device: any) => {
const deviceId = `device-${device.deviceId}`
const plans = (device?.plans ?? []).map((plan: any) => ({
...plan,
_start: dayjs(plan?.planStartTimeStr),
_end: dayjs(plan?.planEndTimeStr)
}))
const validPlans = plans
.filter((plan: any) => plan._start.isValid() && plan._end.isValid())
.sort((a: any, b: any) => a._start.valueOf() - b._start.valueOf())
const firstPlan = validPlans[0]
const deviceRange = getDeviceTaskRangeByChildren(
validPlans.map((item: any) => ({
start_date: item?._start,
end_date: item?._end
}))
)
tasks.push({
id: deviceId,
text: `${device.deviceName ?? '-'}`,
start_date: formatGanttDate(deviceRange?.start_date ?? firstPlan?._start),
end_date: formatGanttDate(deviceRange?.end_date ?? firstPlan?._end),
duration: deviceRange?.duration ?? 1,
parent: 0,
progress: 0,
open: true,
readonly: true,
deviceName: device.deviceName ?? '-',
_deviceData: device
})
let previousPlanTaskId: string | null = null
validPlans.forEach((plan: any, index: number) => {
const startDate = formatGanttDate(plan.planStartTimeStr)
const endDate = formatGanttDate(plan.planEndTimeStr)
const duration = Number(plan.scheduleDays) > 0 ? Number(plan.scheduleDays) : Math.max(plan._end.diff(plan._start, 'day') + 1, 1)
if (!startDate || !endDate) return
const isHistory = String(plan.sourceType ?? '').toUpperCase() === 'HISTORY'
const planTaskId = `plan-${device.deviceId}-${plan.taskDetailId ?? index}-${index}`
tasks.push({
id: planTaskId,
text: `${plan.productCode ?? '-'} / ${plan.productName ?? '-'} / ${plan.taskCode ?? '-'}`,
start_date: startDate,
end_date: endDate,
duration,
parent: deviceId,
progress: 0,
readonly: isHistory,
_planData: plan,
_deviceData: device
})
if (previousPlanTaskId) {
links.push({
id: `link-${linkIndex++}`,
source: previousPlanTaskId,
target: planTaskId,
type: '0'
})
}
previousPlanTaskId = planTaskId
})
})
return { data: tasks, links }
}
const destroyGantt = () => {
ganttEventIds.value.forEach((eventId) => gantt.detachEvent(eventId))
ganttEventIds.value = []
gantt.clearAll()
}
const formatGridDateText = (value: unknown) => {
const date = dayjs(value)
if (!date.isValid()) return '-'
return date.format('YYYY-MM-DD HH:mm:ss')
}
const formatTooltipDateTime = (value: unknown) => {
const date = dayjs(value)
if (!date.isValid()) return '-'
return date.format('YYYY-MM-DD HH:mm:ss')
}
const getDeviceTaskSummary = (task: any) => {
if (!task) {
return {
planCount: 0,
totalPlanNumber: 0,
earliestStart: '-',
latestEnd: '-'
}
}
const childTaskIds = gantt.getChildren(task.id)
const childTasks = (Array.isArray(childTaskIds) ? childTaskIds : [])
.map((childId: string | number) => gantt.getTask(childId))
.filter((child: any) => child?._planData)
if (!childTasks.length) {
return {
planCount: 0,
totalPlanNumber: 0,
earliestStart: '-',
latestEnd: '-'
}
}
const starts = childTasks.map((child: any) => dayjs(child.start_date).valueOf()).filter((val: number) => Number.isFinite(val))
const ends = childTasks.map((child: any) => dayjs(child.end_date).valueOf()).filter((val: number) => Number.isFinite(val))
const totalPlanNumber = childTasks.reduce((sum: number, child: any) => sum + Number(child?._planData?.planNumber ?? 0), 0)
return {
planCount: childTasks.length,
totalPlanNumber,
earliestStart: starts.length ? formatTooltipDateTime(Math.min(...starts)) : '-',
latestEnd: ends.length ? formatTooltipDateTime(Math.max(...ends)) : '-'
}
}
const buildTaskTooltipHtml = (task: any, start?: Date, end?: Date) => {
const plan = task?._planData
if (plan) {
return `
<div><b>任务明细</b></div>
<div>任务单:${plan.taskCode ?? '-'}</div>
<div>产品:${plan.productCode ?? '-'} / ${plan.productName ?? '-'}</div>
<div>明细ID${plan.taskDetailId ?? '-'}</div>
<div>计划数量:${plan.planNumber ?? '-'}</div>
<div>开始:${formatTooltipDateTime(start ?? task?.start_date)}</div>
<div>结束:${formatTooltipDateTime(end ?? task?.end_date)}</div>
<div>最晚开工:${formatTooltipDateTime(plan.latestStartTimeStr)}</div>
`
}
const device = task?._deviceData
const summary = getDeviceTaskSummary(task)
return `
<div><b>汇总</b></div>
<div>设备:${device?.deviceName ?? '-'}</div>
<div>任务明细条数:${summary.planCount}</div>
<div>计划总数:${summary.totalPlanNumber}</div>
<div>最早计划开始:${summary.earliestStart}</div>
<div>最晚计划结束:${summary.latestEnd}</div>
`
}
const getTaskByTooltipNode = (node: HTMLElement) => {
const holder =
node.closest('[task_id]') ||
node.closest('[data-task-id]') ||
node.closest('.gantt_task_line') ||
node
const taskId = holder?.getAttribute('task_id') || holder?.getAttribute('data-task-id')
if (!taskId) return undefined
try {
return gantt.getTask(taskId)
} catch {
return undefined
}
}
const initTaskTooltips = () => {
const tooltipsExt = (gantt.ext as any)?.tooltips
const tooltip = tooltipsExt?.tooltip
if (!tooltipsExt || !tooltip || !ganttContainerRef.value) return
const showTooltip = (event: MouseEvent, html: string) => {
tooltip.setContent(html)
tooltip.show(event)
const node = tooltip.getNode?.()
if (node) {
node.style.display = 'block'
node.style.visibility = 'visible'
node.style.opacity = '1'
node.style.zIndex = '10000'
node.style.pointerEvents = 'none'
}
}
tooltipsExt.detach('.gantt_task_line')
tooltipsExt.detach('.gantt_task_content')
tooltipsExt.attach({
selector: '.gantt_task_line,.gantt_task_content',
onmouseenter: (event: MouseEvent, node: HTMLElement) => {
const task = getTaskByTooltipNode(node)
if (!task) return
showTooltip(event, buildTaskTooltipHtml(task))
},
onmousemove: (event: MouseEvent, node: HTMLElement) => {
const task = getTaskByTooltipNode(node)
if (!task) return
showTooltip(event, buildTaskTooltipHtml(task))
},
onmouseleave: () => {
tooltip.hide()
}
})
}
const syncPlanTimeFromTask = (task: any) => {
if (!task?._planData) return
if (String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return
const start = dayjs(task.start_date)
const end = dayjs(task.end_date)
if (!start.isValid() || !end.isValid()) return
const duration = Math.max(Number(task.duration) || end.diff(start, 'day') + 1, 1)
task.duration = duration
task._planData.planStartTimeStr = start.format('YYYY-MM-DD HH:mm:ss')
task._planData.planEndTimeStr = end.format('YYYY-MM-DD HH:mm:ss')
task._planData.scheduleDays = duration
activePreviewDevice.value = task._deviceData
}
const syncDeviceTaskRangeFromChildren = (task: any) => {
if (!task?._planData) return
const deviceTaskId = task.parent
if (!deviceTaskId) return
const childTaskIds = gantt.getChildren(deviceTaskId)
if (!Array.isArray(childTaskIds) || !childTaskIds.length) return
const childTasks = childTaskIds.map((childId: string | number) => gantt.getTask(childId)).filter((item: any) => item?._planData)
if (!childTasks.length) return
const deviceRange = getDeviceTaskRangeByChildren(childTasks)
if (!deviceRange) return
const deviceTask = gantt.getTask(deviceTaskId)
deviceTask.start_date = deviceRange.start_date
deviceTask.end_date = deviceRange.end_date
deviceTask.duration = deviceRange.duration
gantt.updateTask(deviceTaskId)
}
const movePlanDataToDevice = (task: any, targetDeviceTaskId: string | number, sourceDeviceTaskId?: string | number) => {
if (!task?._planData) return
const targetDeviceTask = gantt.getTask(targetDeviceTaskId)
const targetDeviceData = targetDeviceTask?._deviceData
if (!targetDeviceData) return
const planIdentity = `${task?._planData?.taskId ?? ''}-${task?._planData?.taskDetailId ?? ''}`
const sourceDeviceTask = sourceDeviceTaskId ? gantt.getTask(sourceDeviceTaskId) : undefined
const sourceDeviceData = sourceDeviceTask?._deviceData
if (sourceDeviceData?.plans && Array.isArray(sourceDeviceData.plans)) {
sourceDeviceData.plans = sourceDeviceData.plans.filter((item: any) => {
const itemIdentity = `${item?.taskId ?? ''}-${item?.taskDetailId ?? ''}`
return itemIdentity !== planIdentity
})
}
if (!Array.isArray(targetDeviceData.plans)) {
targetDeviceData.plans = []
}
const targetExists = targetDeviceData.plans.some((item: any) => {
const itemIdentity = `${item?.taskId ?? ''}-${item?.taskDetailId ?? ''}`
return itemIdentity === planIdentity
})
if (!targetExists) {
targetDeviceData.plans.push(task._planData)
}
task._planData.deviceId = targetDeviceData.deviceId
task._planData.feedingPipeline = targetDeviceData.deviceId
task._deviceData = targetDeviceData
}
const normalizeDeviceChildren = (deviceTaskId: string | number, priorityTaskId?: string | number) => {
const childTaskIds = gantt.getChildren(deviceTaskId)
if (!Array.isArray(childTaskIds) || !childTaskIds.length) return
const childTasks = childTaskIds
.map((childId: string | number) => gantt.getTask(childId))
.filter((item: any) => item?._planData)
.sort((a: any, b: any) => {
const startDiff = dayjs(a.start_date).valueOf() - dayjs(b.start_date).valueOf()
if (startDiff !== 0) return startDiff
if (priorityTaskId !== undefined && String(a.id) === String(priorityTaskId)) return -1
if (priorityTaskId !== undefined && String(b.id) === String(priorityTaskId)) return 1
return 0
})
childTasks.forEach((item: any) => {
syncPlanTimeFromTask(item)
gantt.updateTask(item.id)
})
const fakePlanTask = { _planData: true, parent: deviceTaskId }
syncDeviceTaskRangeFromChildren(fakePlanTask)
}
const refreshPlanLinksByRowOrder = () => {
const allLinks = gantt.getLinks()
allLinks.forEach((item: any) => gantt.deleteLink(item.id))
let linkIndex = 1
const deviceTaskIds = gantt.getChildren(0)
deviceTaskIds.forEach((deviceTaskId: string | number) => {
const childTaskIds = gantt.getChildren(deviceTaskId).filter((childId: string | number) => {
const task = gantt.getTask(childId)
return !!task?._planData
})
let previousTaskId: string | number | null = null
childTaskIds.forEach((taskId: string | number) => {
if (previousTaskId !== null) {
gantt.addLink({
id: `link-${linkIndex++}`,
source: previousTaskId,
target: taskId,
type: '0'
})
}
previousTaskId = taskId
})
})
}
const getDeviceInsertIndex = (deviceTaskId: string | number, startDate: dayjs.Dayjs) => {
const childTaskIds = gantt.getChildren(deviceTaskId)
if (!Array.isArray(childTaskIds) || !childTaskIds.length) return 0
const targetStart = startDate.startOf('day').valueOf()
for (let index = 0; index < childTaskIds.length; index += 1) {
const childTask = gantt.getTask(childTaskIds[index])
if (!childTask?._planData) continue
const childStart = dayjs(childTask.start_date).startOf('day').valueOf()
if (childStart >= targetStart) return index
}
return childTaskIds.length
}
const applyTaskAdjust = (task: any, targetDeviceTaskId: string | number, startDate: string, durationValue: number) => {
const targetDeviceTask = gantt.getTask(targetDeviceTaskId)
if (!targetDeviceTask || targetDeviceTask?._planData) return
const sourceDeviceTaskId = task.parent
const nextStart = dayjs(startDate).startOf('day')
const duration = Math.max(Number(durationValue) || 1, 1)
task.parent = targetDeviceTaskId
task.start_date = nextStart.toDate()
task.end_date = nextStart.add(duration - 1, 'day').endOf('day').toDate()
task.duration = duration
movePlanDataToDevice(task, targetDeviceTaskId, sourceDeviceTaskId)
const targetIndex = getDeviceInsertIndex(targetDeviceTaskId, nextStart)
gantt.moveTask(task.id, targetIndex, targetDeviceTaskId)
gantt.updateTask(task.id)
syncPlanTimeFromTask(task)
normalizeDeviceChildren(targetDeviceTaskId, task.id)
if (sourceDeviceTaskId && sourceDeviceTaskId !== targetDeviceTaskId) {
normalizeDeviceChildren(sourceDeviceTaskId)
}
refreshPlanLinksByRowOrder()
refreshTimelineRangeByTasks()
}
const openTaskAdjustDialog = (task: any) => {
if (!task?._planData) return
taskAdjustTaskId.value = task.id
taskAdjustForm.deviceTaskId = String(task.parent ?? '')
taskAdjustForm.startDate = dayjs(task.start_date).format('YYYY-MM-DD')
taskAdjustForm.duration = Math.max(Number(task.duration) || 1, 1)
taskAdjustDialogVisible.value = true
}
const handleTaskAdjustSubmit = () => {
if (!taskAdjustTaskId.value) return
if (!taskAdjustForm.deviceTaskId || !taskAdjustForm.startDate) {
message.warning('请完善设备和开始日期')
return
}
const task = gantt.getTask(taskAdjustTaskId.value)
if (!task?._planData) return
ganttSyncing.value = true
try {
applyTaskAdjust(task, taskAdjustForm.deviceTaskId, taskAdjustForm.startDate, taskAdjustForm.duration)
} finally {
ganttSyncing.value = false
}
taskAdjustDialogVisible.value = false
}
const refreshTimelineRangeByTasks = () => {
let minStart = Number.POSITIVE_INFINITY
let maxEnd = Number.NEGATIVE_INFINITY
gantt.eachTask((task: any) => {
if (!task?._planData) return
const start = dayjs(task.start_date).valueOf()
const end = dayjs(task.end_date).valueOf()
if (!Number.isFinite(start) || !Number.isFinite(end)) return
minStart = Math.min(minStart, start)
maxEnd = Math.max(maxEnd, end)
})
if (!Number.isFinite(minStart) || !Number.isFinite(maxEnd)) return
const nextStartDate = dayjs(minStart).startOf('day').toDate()
const nextEndDate = dayjs(maxEnd).endOf('day').toDate()
const currentStart = dayjs(gantt.config.start_date).valueOf()
const currentEnd = dayjs(gantt.config.end_date).valueOf()
const nextStart = dayjs(nextStartDate).valueOf()
const nextEnd = dayjs(nextEndDate).valueOf()
if (currentStart === nextStart && currentEnd === nextEnd) return
gantt.config.start_date = nextStartDate
gantt.config.end_date = nextEndDate
gantt.render()
}
const initGanttPreview = () => {
if (!ganttContainerRef.value || !previewScheduleList.value.length) return
destroyGantt()
gantt.plugins({ tooltip: true })
gantt.config.readonly = false
gantt.config.drag_move = true
gantt.config.drag_links = false
gantt.config.drag_progress = false
gantt.config.drag_resize = true
gantt.config.order_branch = true
gantt.config.order_branch_free = true
gantt.config.details_on_dblclick = false
gantt.config.show_progress = false
gantt.config.row_height = 40
gantt.config.scale_height = 44
gantt.config.xml_date = '%Y-%m-%d %H:%i'
gantt.config.task_height = 24
gantt.config.columns = [
{
name: 'text',
label: '任务名称',
tree: true,
width: '*',
min_width: 200
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 210,
template: (task: any) =>
task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
? `<span class="gantt-inline-editor-trigger" data-field="start_date">${formatGridDateText(task.start_date)}</span>`
: formatGridDateText(task.start_date),
editor: {
type: 'date',
map_to: 'start_date'
}
},
{
name: 'duration',
label: '天数',
align: 'center',
width: 60,
template: (task: any) =>
task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
? `<span class="gantt-inline-editor-trigger" data-field="duration">${task.duration ?? 0}</span>`
: String(task.duration ?? 0),
editor: {
type: 'number',
map_to: 'duration',
min: 1
}
}
]
gantt.config.scales = [
{ unit: 'month', step: 1, format: (date) => dayjs(date).format('YYYY年M月') },
{ unit: 'day', step: 1, format: (date) => dayjs(date).format('MM-DD') }
]
gantt.templates.tooltip_text = (start, end, task: any) => buildTaskTooltipHtml(task, start, end)
gantt.templates.task_class = (_start, _end, task: any) => {
if (!task?._planData) return ''
return String(task?._planData?.sourceType ?? '').toUpperCase() === 'HISTORY' ? 'schedule-plan-task-history' : 'schedule-plan-task'
}
const globalRange = getGlobalDateRange(previewScheduleList.value)
gantt.config.start_date = dayjs(globalRange.start).startOf('day').toDate()
gantt.config.end_date = dayjs(globalRange.end).endOf('day').toDate()
gantt.init(ganttContainerRef.value)
const ganttData = buildPreviewGanttData(previewScheduleList.value)
gantt.parse(ganttData)
initTaskTooltips()
if (ganttData.data.length) {
activePreviewDevice.value = ganttData.data[0]._deviceData
gantt.showDate(gantt.config.start_date)
}
const clickEventId = gantt.attachEvent('onTaskClick', (id, event: MouseEvent) => {
const task = gantt.getTask(id)
activePreviewDevice.value = task?._deviceData
const target = event?.target as HTMLElement | null
const editableNode = target?.closest('.gantt-inline-editor-trigger') as HTMLElement | null
const field = editableNode?.dataset?.field
const inlineEditors = (gantt.ext as any)?.inlineEditors
if (field && task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT' && inlineEditors?.startEdit) {
inlineEditors.startEdit(id, field)
return false
}
return true
})
const beforeDragEventId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
const task = gantt.getTask(id)
if (!task?._planData) return false
taskDragDurationMap.value[String(id)] = Math.max(Number(task.duration) || 1, 1)
return String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
})
const beforeMoveEventId = gantt.attachEvent('onBeforeTaskMove', (id, parent) => {
const task = gantt.getTask(id)
if (!task?._planData) return false
if (String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return false
const targetTask = gantt.getTask(parent)
if (!targetTask || targetTask?._planData) return false
taskMoveFromParentMap.value[String(id)] = task.parent
return true
})
const afterMoveEventId = gantt.attachEvent('onAfterTaskMove', (id, parent) => {
const task = gantt.getTask(id)
if (!task?._planData) return
const sourceParent = taskMoveFromParentMap.value[String(id)]
delete taskMoveFromParentMap.value[String(id)]
if (!sourceParent && sourceParent !== 0) return
if (sourceParent === parent) {
refreshPlanLinksByRowOrder()
return
}
if (ganttSyncing.value) return
ganttSyncing.value = true
try {
movePlanDataToDevice(task, parent, sourceParent)
syncPlanTimeFromTask(task)
normalizeDeviceChildren(parent)
normalizeDeviceChildren(sourceParent)
refreshPlanLinksByRowOrder()
refreshTimelineRangeByTasks()
} finally {
ganttSyncing.value = false
}
})
const contextMenuEventId = gantt.attachEvent('onContextMenu', (taskId, _linkId, event: MouseEvent) => {
const task = gantt.getTask(taskId)
if (!task?._planData) return true
if (String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return false
event.preventDefault()
openTaskAdjustDialog(task)
return false
})
const dragEventId = gantt.attachEvent('onAfterTaskDrag', (id, mode) => {
if (ganttSyncing.value) return
const task = gantt.getTask(id)
const taskId = String(id)
const originalDuration = taskDragDurationMap.value[taskId]
delete taskDragDurationMap.value[taskId]
ganttSyncing.value = true
try {
if (mode === gantt.config.drag_mode.move && Number(originalDuration) > 0) {
task.duration = originalDuration
}
syncPlanTimeFromTask(task)
normalizeDeviceChildren(task.parent)
refreshPlanLinksByRowOrder()
refreshTimelineRangeByTasks()
} finally {
ganttSyncing.value = false
}
})
const updateEventId = gantt.attachEvent('onAfterTaskUpdate', (id) => {
if (ganttSyncing.value) return
const task = gantt.getTask(id)
ganttSyncing.value = true
try {
syncPlanTimeFromTask(task)
normalizeDeviceChildren(task.parent)
refreshPlanLinksByRowOrder()
refreshTimelineRangeByTasks()
gantt.refreshTask(id)
} finally {
ganttSyncing.value = false
}
})
ganttEventIds.value.push(
clickEventId,
beforeDragEventId,
beforeMoveEventId,
afterMoveEventId,
contextMenuEventId,
dragEventId,
updateEventId
)
refreshPlanLinksByRowOrder()
}
const handlePreviewSave = async () => {
const createReqVOList = previewScheduleList.value.flatMap((device: any) => {
const plans = Array.isArray(device?.plans) ? device.plans : []
return plans
.filter((plan: any) => String(plan?.sourceType ?? '').toUpperCase() === 'CURRENT')
.map((plan: any) => {
const startValue = dayjs(plan?.planStartTimeStr)
const endValue = dayjs(plan?.planEndTimeStr)
const deliveryDateValue = dayjs(plan?.deliveryDateStr)
const latestStartValue = dayjs(plan?.latestStartTimeStr)
return {
productId: plan?.productId,
taskId: plan?.taskId,
taskDetailId: plan?.taskDetailId,
planNumber: Number(plan?.planNumber ?? 0),
finishNumber: Number(plan?.finishNumber ?? 0),
isCode: typeof plan?.isCode === 'boolean' ? plan.isCode : true,
isPreProduction: Number(plan?.isPreProduction ?? 0),
planStartTime: startValue.isValid() ? startValue.valueOf() : undefined,
planEndTime: endValue.isValid() ? endValue.valueOf() : undefined,
reyaNumber: Number(plan?.reyaNumber ?? plan?.planNumber ?? 0),
workerId: plan?.workerId,
feedingPipeline: plan?.feedingPipeline ?? device?.deviceId,
deviceId: device?.deviceId ?? plan?.deviceId,
deliveryDate: deliveryDateValue.isValid() ? deliveryDateValue.valueOf() : undefined,
latestStartTime: latestStartValue.isValid() ? latestStartValue.valueOf() : undefined
}
})
})
if (!createReqVOList.length) {
message.warning('暂无可保存的计划数据')
return
}
previewSaveLoading.value = true
try {
await PlanApi.createBatch({ createReqVOList })
message.success('排产计划保存成功')
previewVisible.value = false
emit('saved')
} finally {
previewSaveLoading.value = false
}
}
watch(
() => props.modelValue,
async (visible) => {
if (visible) {
activePreviewDevice.value = previewScheduleList.value[0]
await nextTick()
initGanttPreview()
return
}
destroyGantt()
}
)
watch(
() => props.scheduleList,
async () => {
if (!props.modelValue) return
activePreviewDevice.value = previewScheduleList.value[0]
await nextTick()
initGanttPreview()
},
{ deep: true }
)
onBeforeUnmount(() => {
destroyGantt()
})
</script>
<style scoped>
.schedule-preview-wrap {
display: flex;
width: 100%;
gap: 12px;
}
.schedule-gantt-container {
flex: 1;
min-width: 0;
height: 800px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
}
.schedule-detail-panel {
width: 280px;
flex: 0 0 280px;
}
.schedule-detail-title {
margin-bottom: 10px;
font-weight: 600;
}
.schedule-plan-list-title {
margin-top: 14px;
margin-bottom: 10px;
font-weight: 600;
}
.schedule-plan-list {
overflow: auto;
max-height: 55vh;
border: 1px solid var(--el-border-color);
border-radius: 4px;
padding: 8px;
}
.schedule-plan-item {
padding: 8px;
border-radius: 4px;
background: var(--el-fill-color-light);
}
.schedule-plan-item-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 4px;
}
.schedule-plan-item-title {
font-weight: 600;
color: var(--el-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.schedule-plan-item-active {
border: 1px solid var(--el-color-success-light-5);
background: var(--el-color-success-light-9);
}
.schedule-plan-item + .schedule-plan-item {
margin-top: 8px;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_row) {
font-weight: 600;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_cell) {
text-overflow: clip;
}
.schedule-gantt-container :deep(.gantt-inline-editor-trigger) {
display: inline-block;
width: 100%;
overflow: visible;
cursor: pointer;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task) {
background: #67c23a;
border-color: #67c23a;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task .gantt_task_content) {
color: #ffffff;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history) {
background: #909399;
border-color: #909399;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history .gantt_task_content) {
color: #ffffff;
}
:deep(.gantt_tooltip) {
z-index: 5000 !important;
}
.schedule-custom-tooltip {
display: none;
}
</style>