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/ScheduleGanttPanelEditable.vue

1207 lines
41 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>
<div class="schedule-preview-wrap">
<div ref="ganttContainerRef" class="schedule-gantt-container" :style="{ height }"></div>
<div class="schedule-detail-panel">
<div class="schedule-detail-title">计划信息</div>
<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 v-if="'dailyAverageValue' in activePreviewDevice" label="每日报工平均值">{{ activePreviewDevice.dailyAverageValue ?? '-' }}</el-descriptions-item>
<el-descriptions-item v-if="'dataCollectionCapacity' in activePreviewDevice" label="数据采集产能">{{ activePreviewDevice.dataCollectionCapacity ?? '-' }}</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>
</template>
<el-empty v-else description="暂无计划信息" :image-size="80" />
</div>
</div>
<el-dialog v-model="taskAdjustDialogVisible" title="调整任务" width="420px" append-to-body>
<el-form label-width="110px">
<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="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择计划开始日期"
class="!w-full"
/>
</el-form-item>
<el-form-item label="计划结束日期">
<el-date-picker
v-model="taskAdjustForm.endDate"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择计划结束日期"
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 dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
defineOptions({ name: 'ScheduleGanttPanelEditable' })
const props = withDefaults(
defineProps<{
scheduleList: any[]
height?: string
}>(),
{
height: '800px'
}
)
const message = useMessage()
const ganttContainerRef = ref<HTMLDivElement>()
const activePreviewDevice = ref<any>()
const activePreviewTask = ref<any>()
const tooltipCleanupFns = ref<(() => void)[]>([])
const ganttEventIds = ref<string[]>([])
const ganttSyncing = ref(false)
const taskAdjustDialogVisible = ref(false)
const taskAdjustTaskId = ref<string | number | null>(null)
const taskAdjustForm = reactive({
deviceTaskId: '',
startDate: '',
endDate: ''
})
const taskMoveFromParentMap = ref<Record<string, string | number | undefined>>({})
const taskDragDurationMap = ref<Record<string, number>>({})
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 hasCurrentPlan = (device: any) =>
(device?.plans ?? []).some((plan: any) => String(plan.sourceType ?? '').toUpperCase() === 'CURRENT')
const getGanttScheduleList = () =>
previewScheduleList.value.filter((device: any) => hasCurrentPlan(device))
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 cleanupTaskTooltips = () => {
const tooltipsExt = (gantt.ext as any)?.tooltips
const tooltip = tooltipsExt?.tooltip
if (tooltipsExt?.detach) {
tooltipsExt.detach('.gantt_task_line')
tooltipsExt.detach('.gantt_task_content')
}
if (tooltip?.hide) {
tooltip.hide()
}
const node = tooltip?.getNode?.()
if (node) {
node.style.display = 'none'
node.style.visibility = 'hidden'
node.style.opacity = '0'
}
tooltipCleanupFns.value.forEach((fn) => fn())
tooltipCleanupFns.value = []
}
const destroyGantt = () => {
try {
cleanupTaskTooltips()
} catch {}
try {
ganttEventIds.value.forEach((eventId) => {
try {
gantt.detachEvent(eventId)
} catch {}
})
} catch {}
ganttEventIds.value = []
try {
gantt.clearAll()
} catch {}
}
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
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?.()
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: () => {
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
}
})
}
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
if (activePreviewTask.value && String(activePreviewTask.value.taskDetailId) === String(task._planData.taskDetailId)) {
activePreviewTask.value = task._planData
}
}
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) => {
try {
gantt.getTask(deviceTaskId)
} catch {
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)
.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 = () => {
try {
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) => {
try {
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
})
} catch {}
})
} catch {}
}
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 addDeviceToGantt = (device: any, excludePlanIdentity?: string) => {
const deviceId = `device-${device.deviceId}`
try {
gantt.getTask(deviceId)
return
} catch {}
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 }))
)
gantt.addTask({
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
})
validPlans.forEach((plan: any, index: number) => {
const planIdentity = `${plan?.taskId ?? ''}-${plan?.taskDetailId ?? ''}`
if (excludePlanIdentity && planIdentity === excludePlanIdentity) return
const startDate = formatGanttDate(plan.planStartTimeStr)
const endDate = formatGanttDate(plan.planEndTimeStr)
if (!startDate || !endDate) return
const duration = Number(plan.scheduleDays) > 0 ? Number(plan.scheduleDays) : Math.max(plan._end.diff(plan._start, 'day') + 1, 1)
const isHistory = String(plan.sourceType ?? '').toUpperCase() === 'HISTORY'
const planTaskId = `plan-${device.deviceId}-${plan.taskDetailId ?? index}-${index}`
gantt.addTask({
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
})
})
}
const removeDeviceFromGanttIfNoCurrent = (deviceTaskId: string | number) => {
try {
const deviceTask = gantt.getTask(deviceTaskId)
if (!deviceTask || deviceTask?._planData) return
const childTaskIds = gantt.getChildren(deviceTaskId)
const hasCurrent = (Array.isArray(childTaskIds) ? childTaskIds : []).some((childId: string | number) => {
const childTask = gantt.getTask(childId)
return childTask?._planData && String(childTask._planData.sourceType ?? '').toUpperCase() === 'CURRENT'
})
if (!hasCurrent) {
gantt.deleteTask(String(deviceTaskId))
}
} catch {}
}
const applyTaskAdjust = (task: any, targetDeviceTaskId: string | number, startDate: string, endDate: string) => {
const sourceDeviceTaskId = task.parent
const nextStart = dayjs(startDate)
const nextEnd = dayjs(endDate)
const duration = Math.max(nextEnd.diff(nextStart, 'day') + 1, 1)
const planIdentity = `${task?._planData?.taskId ?? ''}-${task?._planData?.taskDetailId ?? ''}`
const targetDeviceData = previewScheduleList.value.find((d: any) => `device-${d.deviceId}` === targetDeviceTaskId)
if (!targetDeviceData) return
const sourceDeviceData = task._deviceData
if (sourceDeviceData?.plans && Array.isArray(sourceDeviceData.plans)) {
sourceDeviceData.plans = sourceDeviceData.plans.filter((item: any) => {
const itemId = `${item?.taskId ?? ''}-${item?.taskDetailId ?? ''}`
return itemId !== planIdentity
})
}
if (!Array.isArray(targetDeviceData.plans)) {
targetDeviceData.plans = []
}
const targetExists = targetDeviceData.plans.some((item: any) => {
const itemId = `${item?.taskId ?? ''}-${item?.taskDetailId ?? ''}`
return itemId === planIdentity
})
if (!targetExists) {
targetDeviceData.plans.push(task._planData)
}
task._planData.deviceId = targetDeviceData.deviceId
task._planData.feedingPipeline = targetDeviceData.deviceId
task._deviceData = targetDeviceData
let targetDeviceInGantt = true
try {
gantt.getTask(targetDeviceTaskId)
} catch {
targetDeviceInGantt = false
}
if (!targetDeviceInGantt) {
addDeviceToGantt(targetDeviceData, planIdentity)
}
task.parent = targetDeviceTaskId
task.start_date = nextStart.toDate()
task.end_date = nextEnd.toDate()
task.duration = duration
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) {
removeDeviceFromGanttIfNoCurrent(sourceDeviceTaskId)
normalizeDeviceChildren(sourceDeviceTaskId)
}
refreshPlanLinksByRowOrder()
refreshTimelineRangeByTasks()
gantt.render()
}
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 HH:mm:ss')
taskAdjustForm.endDate = dayjs(task.end_date).format('YYYY-MM-DD HH:mm:ss')
taskAdjustDialogVisible.value = true
}
const handleTaskAdjustSubmit = () => {
if (!taskAdjustTaskId.value) return
if (!taskAdjustForm.deviceTaskId || !taskAdjustForm.startDate || !taskAdjustForm.endDate) {
message.warning('请完善设备、计划开始日期和计划结束日期')
return
}
const task = gantt.getTask(taskAdjustTaskId.value)
if (!task?._planData) return
ganttSyncing.value = true
try {
applyTaskAdjust(task, taskAdjustForm.deviceTaskId, taskAdjustForm.startDate, taskAdjustForm.endDate)
} 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) return
destroyGantt()
if (!previewScheduleList.value.length) {
activePreviewDevice.value = undefined
activePreviewTask.value = undefined
return
}
gantt.plugins({ tooltip: true, inline_edit: true,undo: 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 = 48 // 任务行高度
gantt.config.scale_height = 70 // 时间轴高度
gantt.config.xml_date = '%Y-%m-%d %H:%i' // 日期格式
gantt.config.task_height = 36 // 任务条高度
gantt.config.min_column_width = 80 // 最小列宽
gantt.config.column_width = 90 // 默认列宽
gantt.config.bar_height = 34 // 任务条实际高度
gantt.config.resize_step = 15 // 调整步长
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" data-task-id="${task.id}">${formatGridDateText(task.start_date)}</span>`
: formatGridDateText(task.start_date)
}
]
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 ''
const sourceType = String(task?._planData?.sourceType ?? '').toUpperCase()
return sourceType === 'HISTORY' ? 'schedule-plan-task-history' : 'schedule-plan-task'
}
const ganttScheduleData = getGanttScheduleList()
const globalRange = getGlobalDateRange(ganttScheduleData)
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(ganttScheduleData)
gantt.parse(ganttData)
// 强制刷新所有任务的样式,确保颜色正确应用
gantt.eachTask((task: any) => {
gantt.refreshTask(task.id)
})
initTaskTooltips()
if (ganttData.data.length) {
activePreviewDevice.value = ganttData.data[0]._deviceData
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)
if (task?._planData) {
activePreviewTask.value = task._planData
activePreviewDevice.value = task._deviceData
} else {
activePreviewTask.value = undefined
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
if (!field || !task?._planData || String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return true
if (field === 'start_date') {
openTaskAdjustDialog(task)
return false
}
const inlineEditors = (gantt.ext as any)?.inlineEditors
if (inlineEditors?.startEdit) {
inlineEditors.startEdit(id, field)
return false
}
return true
})
ganttEventIds.value.push(clickEventId)
if (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()
openTaskAdjustDialog(task)
} catch {}
}
ganttContainerRef.value.addEventListener('click', gridClickHandler, true)
tooltipCleanupFns.value.push(() => {
ganttContainerRef.value?.removeEventListener('click', gridClickHandler, 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)
removeDeviceFromGanttIfNoCurrent(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(beforeDragEventId, beforeMoveEventId, afterMoveEventId, contextMenuEventId, dragEventId, updateEventId)
refreshPlanLinksByRowOrder()
}
onMounted(async () => {
await nextTick()
initGanttPreview()
})
watch(
() => props.scheduleList,
async () => {
if (ganttSyncing.value) return
await nextTick()
if (ganttSyncing.value) return
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;
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;
background: linear-gradient(180deg, #f8fafc 0%, #f0f4f8 100%);
border-bottom: 1px solid #e2e8f0;
height: 48px;
display: flex;
align-items: flex-end;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_row:nth-child(even)) {
background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_row:hover) {
background: linear-gradient(180deg, #f3f4f6 0%, #e5e7eb 100%) !important;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_cell) {
text-overflow: clip;
font-size: 13px;
color: #4b5563;
border-right: 1px solid #e5e7eb;
padding: 0 12px;
line-height: 36px;
display: flex;
align-items: center;
justify-content: flex-start;
}
.schedule-gantt-container :deep(.gantt_scale_line) {
border-bottom: 2px solid #d1d5db;
background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
display: flex;
align-items: center;
margin-bottom: 2px;
}
.schedule-gantt-container :deep(.gantt_scale_line:last-child) {
margin-bottom: 0;
border-bottom: 1px solid #e5e7eb;
}
.schedule-gantt-container :deep(.gantt_scale_cell) {
font-size: 14px;
font-weight: 600;
color: #374151;
border-right: 1px solid #e5e7eb;
padding: 0 8px;
text-align: center;
background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
display: flex;
align-items: center;
justify-content: center;
height: 35px;
}
.schedule-gantt-container :deep(.gantt_scale_line:last-child .gantt_scale_cell) {
font-size: 13px;
font-weight: 500;
color: #6b7280;
background: linear-gradient(180deg, #f9fafb 0%, #f3f4f6 100%);
border-bottom: 1px solid #e5e7eb;
height: 35px;
}
.schedule-gantt-container :deep(.gantt_task_bg) {
background: #ffffff;
}
.schedule-gantt-container :deep(.gantt_task_bg .gantt_task_cell) {
border-right: 1px solid #f3f4f6;
border-bottom: 1px solid #f3f4f6;
}
.schedule-gantt-container :deep(.gantt_task_bg .gantt_task_row:nth-child(even)) {
background: #f9fafb;
}
.schedule-gantt-container :deep(.gantt_task_bg .gantt_task_row:hover) {
background: #f3f4f6 !important;
}
.schedule-gantt-container :deep(.gantt_task_line) {
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: none;
}
.schedule-gantt-container :deep(.gantt_task_content) {
font-size: 12px;
font-weight: 500;
color: #ffffff;
padding: 0 12px;
line-height: 32px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.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;
}
.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: #9ca3af;
opacity: 0.7;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history .gantt_task_content) {
color: #ffffff;
}
:deep(.gantt_tooltip) {
z-index: 5000 !important;
background: rgba(255, 255, 255, 0.95) !important;
border: 1px solid #e5e7eb !important;
border-radius: 8px !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
padding: 12px !important;
font-size: 13px !important;
line-height: 1.5 !important;
}
:deep(.gantt_tooltip) div {
margin-bottom: 6px !important;
}
:deep(.gantt_tooltip) div:last-child {
margin-bottom: 0 !important;
}
:deep(.gantt_tree_content) {
line-height: 50px;
}
</style>