|
|
|
|
@ -3,31 +3,33 @@
|
|
|
|
|
<div ref="ganttContainerRef" class="schedule-gantt-container" :style="{ height }"></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>
|
|
|
|
|
<template v-if="activePreviewDevice">
|
|
|
|
|
<el-descriptions :column="1" border size="small">
|
|
|
|
|
<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>
|
|
|
|
|
<div class="schedule-plan-list-title">计划明细</div>
|
|
|
|
|
<div class="schedule-plan-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(plan, index) in activePreviewTask ? [activePreviewTask] : (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>
|
|
|
|
|
</div>
|
|
|
|
|
<div>计划编码:{{ plan.taskCode ?? '-' }}</div>
|
|
|
|
|
<div>计划数量:{{ plan.planNumber ?? '-' }}</div>
|
|
|
|
|
<div>交货日期:{{ plan.deliveryDateStr ?? '-' }}</div>
|
|
|
|
|
<div>开始:{{ plan.planStartTimeStr || '-' }}</div>
|
|
|
|
|
<div>结束:{{ plan.planEndTimeStr || '-' }}</div>
|
|
|
|
|
<div>最晚开工:{{ plan.latestStartTimeStr || '-' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>计划编码:{{ plan.taskCode ?? '-' }}</div>
|
|
|
|
|
<div>计划数量:{{ plan.planNumber ?? '-' }}</div>
|
|
|
|
|
<div>交货日期:{{ plan.deliveryDateStr ?? '-' }}</div>
|
|
|
|
|
<div>开始:{{ plan.planStartTimeStr || '-' }}</div>
|
|
|
|
|
<div>结束:{{ plan.planEndTimeStr || '-' }}</div>
|
|
|
|
|
<div>最晚开工:{{ plan.latestStartTimeStr || '-' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-empty v-else description="暂无计划信息" :image-size="80" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@ -56,6 +58,24 @@
|
|
|
|
|
<el-button type="primary" @click="handleTaskAdjustSubmit">确定</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<el-dialog v-if="props.editable" v-model="startDateEditorVisible" title="修改开始时间" width="360px" append-to-body>
|
|
|
|
|
<el-form label-width="80px">
|
|
|
|
|
<el-form-item label="开始时间">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="startDateEditorValue"
|
|
|
|
|
type="datetime"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
placeholder="请选择开始时间"
|
|
|
|
|
class="!w-full"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<el-button @click="startDateEditorVisible = false">取消</el-button>
|
|
|
|
|
<el-button type="primary" @click="handleStartDateEditorSubmit">确定</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
@ -80,6 +100,11 @@ const props = withDefaults(
|
|
|
|
|
const message = useMessage()
|
|
|
|
|
const ganttContainerRef = ref<HTMLDivElement>()
|
|
|
|
|
const activePreviewDevice = ref<any>()
|
|
|
|
|
const activePreviewTask = ref<any>()
|
|
|
|
|
const startDateEditorVisible = ref(false)
|
|
|
|
|
const startDateEditorTaskId = ref<string | number | null>(null)
|
|
|
|
|
const startDateEditorValue = ref('')
|
|
|
|
|
const tooltipCleanupFns = ref<(() => void)[]>([])
|
|
|
|
|
const ganttEventIds = ref<string[]>([])
|
|
|
|
|
const ganttSyncing = ref(false)
|
|
|
|
|
const taskAdjustDialogVisible = ref(false)
|
|
|
|
|
@ -225,6 +250,8 @@ const cleanupTaskTooltips = () => {
|
|
|
|
|
node.style.visibility = 'hidden'
|
|
|
|
|
node.style.opacity = '0'
|
|
|
|
|
}
|
|
|
|
|
tooltipCleanupFns.value.forEach((fn) => fn())
|
|
|
|
|
tooltipCleanupFns.value = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const destroyGantt = () => {
|
|
|
|
|
@ -315,7 +342,28 @@ const initTaskTooltips = () => {
|
|
|
|
|
const tooltipsExt = (gantt.ext as any)?.tooltips
|
|
|
|
|
const tooltip = tooltipsExt?.tooltip
|
|
|
|
|
if (!tooltipsExt || !tooltip || !ganttContainerRef.value) return
|
|
|
|
|
|
|
|
|
|
let tooltipHideTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
|
|
|
|
const forceHideTooltip = () => {
|
|
|
|
|
if (tooltipHideTimer) {
|
|
|
|
|
clearTimeout(tooltipHideTimer)
|
|
|
|
|
tooltipHideTimer = null
|
|
|
|
|
}
|
|
|
|
|
tooltip.hide()
|
|
|
|
|
const node = tooltip.getNode?.()
|
|
|
|
|
if (node) {
|
|
|
|
|
node.style.display = 'none'
|
|
|
|
|
node.style.visibility = 'hidden'
|
|
|
|
|
node.style.opacity = '0'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const showTooltip = (event: MouseEvent, html: string) => {
|
|
|
|
|
if (tooltipHideTimer) {
|
|
|
|
|
clearTimeout(tooltipHideTimer)
|
|
|
|
|
tooltipHideTimer = null
|
|
|
|
|
}
|
|
|
|
|
tooltip.setContent(html)
|
|
|
|
|
tooltip.show(event)
|
|
|
|
|
const node = tooltip.getNode?.()
|
|
|
|
|
@ -327,6 +375,7 @@ const initTaskTooltips = () => {
|
|
|
|
|
node.style.pointerEvents = 'none'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tooltipsExt.detach('.gantt_task_line')
|
|
|
|
|
tooltipsExt.detach('.gantt_task_content')
|
|
|
|
|
tooltipsExt.attach({
|
|
|
|
|
@ -342,7 +391,38 @@ const initTaskTooltips = () => {
|
|
|
|
|
showTooltip(event, buildTaskTooltipHtml(task))
|
|
|
|
|
},
|
|
|
|
|
onmouseleave: () => {
|
|
|
|
|
tooltip.hide()
|
|
|
|
|
forceHideTooltip()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const containerEl = ganttContainerRef.value
|
|
|
|
|
const containerMouseLeaveHandler = () => {
|
|
|
|
|
forceHideTooltip()
|
|
|
|
|
}
|
|
|
|
|
containerEl.addEventListener('mouseleave', containerMouseLeaveHandler)
|
|
|
|
|
|
|
|
|
|
const documentMouseMoveHandler = (e: MouseEvent) => {
|
|
|
|
|
const target = e.target as HTMLElement
|
|
|
|
|
if (!target) return
|
|
|
|
|
const isOverTask = target.closest('.gantt_task_line') || target.closest('.gantt_task_content')
|
|
|
|
|
if (!isOverTask) {
|
|
|
|
|
if (tooltipHideTimer) clearTimeout(tooltipHideTimer)
|
|
|
|
|
tooltipHideTimer = setTimeout(forceHideTooltip, 150)
|
|
|
|
|
} else {
|
|
|
|
|
if (tooltipHideTimer) {
|
|
|
|
|
clearTimeout(tooltipHideTimer)
|
|
|
|
|
tooltipHideTimer = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener('mousemove', documentMouseMoveHandler)
|
|
|
|
|
|
|
|
|
|
tooltipCleanupFns.value.push(() => {
|
|
|
|
|
containerEl.removeEventListener('mouseleave', containerMouseLeaveHandler)
|
|
|
|
|
document.removeEventListener('mousemove', documentMouseMoveHandler)
|
|
|
|
|
if (tooltipHideTimer) {
|
|
|
|
|
clearTimeout(tooltipHideTimer)
|
|
|
|
|
tooltipHideTimer = null
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
@ -359,6 +439,9 @@ const syncPlanTimeFromTask = (task: any) => {
|
|
|
|
|
task._planData.planEndTimeStr = end.format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
task._planData.scheduleDays = duration
|
|
|
|
|
activePreviewDevice.value = task._deviceData
|
|
|
|
|
if (activePreviewTask.value && String(activePreviewTask.value.taskDetailId) === String(task._planData.taskDetailId)) {
|
|
|
|
|
activePreviewTask.value = task._planData
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const syncDeviceTaskRangeFromChildren = (task: any) => {
|
|
|
|
|
@ -515,6 +598,39 @@ const handleTaskAdjustSubmit = () => {
|
|
|
|
|
taskAdjustDialogVisible.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleStartDateEditorSubmit = () => {
|
|
|
|
|
if (!startDateEditorTaskId.value || !startDateEditorValue.value) {
|
|
|
|
|
startDateEditorVisible.value = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const task = gantt.getTask(startDateEditorTaskId.value)
|
|
|
|
|
if (!task?._planData) {
|
|
|
|
|
startDateEditorVisible.value = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const newStart = dayjs(startDateEditorValue.value)
|
|
|
|
|
if (!newStart.isValid()) {
|
|
|
|
|
message.warning('请选择有效的时间')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const duration = Math.max(Number(task.duration) || 1, 1)
|
|
|
|
|
const newEnd = newStart.add(duration - 1, 'day').endOf('day')
|
|
|
|
|
ganttSyncing.value = true
|
|
|
|
|
try {
|
|
|
|
|
task.start_date = newStart.toDate()
|
|
|
|
|
task.end_date = newEnd.toDate()
|
|
|
|
|
task.duration = duration
|
|
|
|
|
syncPlanTimeFromTask(task)
|
|
|
|
|
normalizeDeviceChildren(task.parent)
|
|
|
|
|
refreshPlanLinksByRowOrder()
|
|
|
|
|
refreshTimelineRangeByTasks()
|
|
|
|
|
gantt.updateTask(task.id)
|
|
|
|
|
} finally {
|
|
|
|
|
ganttSyncing.value = false
|
|
|
|
|
}
|
|
|
|
|
startDateEditorVisible.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const refreshTimelineRangeByTasks = () => {
|
|
|
|
|
let minStart = Number.POSITIVE_INFINITY
|
|
|
|
|
let maxEnd = Number.NEGATIVE_INFINITY
|
|
|
|
|
@ -546,10 +662,11 @@ const initGanttPreview = () => {
|
|
|
|
|
destroyGantt()
|
|
|
|
|
if (!previewScheduleList.value.length) {
|
|
|
|
|
activePreviewDevice.value = undefined
|
|
|
|
|
activePreviewTask.value = undefined
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gantt.plugins({ tooltip: true })
|
|
|
|
|
gantt.plugins({ tooltip: true, inline_edit: true,undo: true })
|
|
|
|
|
gantt.config.readonly = !props.editable
|
|
|
|
|
gantt.config.drag_move = !!props.editable
|
|
|
|
|
gantt.config.drag_links = false
|
|
|
|
|
@ -579,12 +696,8 @@ const initGanttPreview = () => {
|
|
|
|
|
width: 210,
|
|
|
|
|
template: (task: any) =>
|
|
|
|
|
props.editable && 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'
|
|
|
|
|
}
|
|
|
|
|
? `<span class="gantt-inline-editor-trigger" data-field="start_date" data-task-id="${task.id}">${formatGridDateText(task.start_date)}</span>`
|
|
|
|
|
: formatGridDateText(task.start_date)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'duration',
|
|
|
|
|
@ -617,6 +730,7 @@ const initGanttPreview = () => {
|
|
|
|
|
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)
|
|
|
|
|
@ -625,18 +739,39 @@ const initGanttPreview = () => {
|
|
|
|
|
|
|
|
|
|
if (ganttData.data.length) {
|
|
|
|
|
activePreviewDevice.value = ganttData.data[0]._deviceData
|
|
|
|
|
gantt.showDate(gantt.config.start_date)
|
|
|
|
|
const today = dayjs().startOf('day').toDate()
|
|
|
|
|
const pos = gantt.posFromDate(today)
|
|
|
|
|
const scrollState = gantt.getScrollState()
|
|
|
|
|
if (scrollState && pos >= 0) {
|
|
|
|
|
const halfWidth = scrollState.inner_width / 3
|
|
|
|
|
gantt.scrollTo(Math.max(0, pos - halfWidth), 0)
|
|
|
|
|
} else {
|
|
|
|
|
gantt.showDate(today)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clickEventId = gantt.attachEvent('onTaskClick', (id, event: MouseEvent) => {
|
|
|
|
|
const task = gantt.getTask(id)
|
|
|
|
|
activePreviewDevice.value = task?._deviceData
|
|
|
|
|
if (task?._planData) {
|
|
|
|
|
activePreviewTask.value = task._planData
|
|
|
|
|
activePreviewDevice.value = task._deviceData
|
|
|
|
|
} else {
|
|
|
|
|
activePreviewTask.value = undefined
|
|
|
|
|
activePreviewDevice.value = task?._deviceData
|
|
|
|
|
}
|
|
|
|
|
if (!props.editable) return true
|
|
|
|
|
const target = event?.target as HTMLElement | null
|
|
|
|
|
const editableNode = target?.closest('.gantt-inline-editor-trigger') as HTMLElement | null
|
|
|
|
|
const field = editableNode?.dataset?.field
|
|
|
|
|
if (!field || !task?._planData || String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return true
|
|
|
|
|
if (field === 'start_date') {
|
|
|
|
|
startDateEditorTaskId.value = id
|
|
|
|
|
startDateEditorValue.value = dayjs(task.start_date).format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
startDateEditorVisible.value = true
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const inlineEditors = (gantt.ext as any)?.inlineEditors
|
|
|
|
|
if (field && task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT' && inlineEditors?.startEdit) {
|
|
|
|
|
if (inlineEditors?.startEdit) {
|
|
|
|
|
inlineEditors.startEdit(id, field)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
@ -644,6 +779,28 @@ const initGanttPreview = () => {
|
|
|
|
|
})
|
|
|
|
|
ganttEventIds.value.push(clickEventId)
|
|
|
|
|
|
|
|
|
|
if (props.editable && ganttContainerRef.value) {
|
|
|
|
|
const gridClickHandler = (e: MouseEvent) => {
|
|
|
|
|
const target = e.target as HTMLElement
|
|
|
|
|
const trigger = target.closest('.gantt-inline-editor-trigger[data-field="start_date"]') as HTMLElement | null
|
|
|
|
|
if (!trigger) return
|
|
|
|
|
const taskId = trigger.getAttribute('data-task-id')
|
|
|
|
|
if (!taskId) return
|
|
|
|
|
try {
|
|
|
|
|
const task = gantt.getTask(taskId)
|
|
|
|
|
if (!task?._planData || String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
startDateEditorTaskId.value = taskId
|
|
|
|
|
startDateEditorValue.value = dayjs(task.start_date).format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
startDateEditorVisible.value = true
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
ganttContainerRef.value.addEventListener('click', gridClickHandler, true)
|
|
|
|
|
tooltipCleanupFns.value.push(() => {
|
|
|
|
|
ganttContainerRef.value?.removeEventListener('click', gridClickHandler, true)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!props.editable) return
|
|
|
|
|
|
|
|
|
|
const beforeDragEventId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
|
|
|
|
|
|