feat:甘特图组件封装

pull/1/head
黄伟杰 1 month ago
parent dc9cf5eeea
commit b57ed07ff8

@ -3502,6 +3502,7 @@ export default {
detailBaogongRecordTabLabel: 'Report Records', detailBaogongRecordTabLabel: 'Report Records',
tableCodeColumn: 'Plan Code', tableCodeColumn: 'Plan Code',
tableTaskCodeColumn: 'Task Code',
tableProductColumn: 'Product', tableProductColumn: 'Product',
tableDeviceNameColumn: 'Device Name', tableDeviceNameColumn: 'Device Name',
tableFeedingPipelineColumn: 'Production Line', tableFeedingPipelineColumn: 'Production Line',

@ -3347,6 +3347,7 @@ export default {
detailBaogongRecordTabLabel: '报工记录', detailBaogongRecordTabLabel: '报工记录',
tableCodeColumn: '计划编码', tableCodeColumn: '计划编码',
tableTaskCodeColumn: '任务编码',
tableProductColumn: '产品', tableProductColumn: '产品',
tableDeviceNameColumn: '设备名称', tableDeviceNameColumn: '设备名称',
tableFeedingPipelineColumn: '生产线', tableFeedingPipelineColumn: '生产线',

@ -0,0 +1,853 @@
<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>
<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>
</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>
</div>
</div>
<el-dialog v-if="props.editable" 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 dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
defineOptions({ name: 'ScheduleGanttPanel' })
const props = withDefaults(
defineProps<{
scheduleList: any[]
editable?: boolean
height?: string
}>(),
{
editable: false,
height: '800px'
}
)
const message = useMessage()
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 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 || !props.editable,
_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 || !props.editable) 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 || !props.editable) 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 || !props.editable) 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 || !props.editable) 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) return
destroyGantt()
if (!previewScheduleList.value.length) {
activePreviewDevice.value = undefined
return
}
gantt.plugins({ tooltip: true })
gantt.config.readonly = !props.editable
gantt.config.drag_move = !!props.editable
gantt.config.drag_links = false
gantt.config.drag_progress = false
gantt.config.drag_resize = !!props.editable
gantt.config.order_branch = !!props.editable
gantt.config.order_branch_free = !!props.editable
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) =>
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'
}
},
{
name: 'duration',
label: '天数',
align: 'center',
width: 60,
template: (task: any) =>
props.editable && 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
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
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
})
ganttEventIds.value.push(clickEventId)
if (!props.editable) return
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(beforeDragEventId, beforeMoveEventId, afterMoveEventId, contextMenuEventId, dragEventId, updateEventId)
refreshPlanLinksByRowOrder()
}
onMounted(async () => {
await nextTick()
initGanttPreview()
})
watch(
() => props.scheduleList,
async () => {
await nextTick()
initGanttPreview()
},
{ deep: true }
)
watch(
() => props.editable,
async () => {
await nextTick()
initGanttPreview()
}
)
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;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_cell) {
text-overflow: clip;
}
.schedule-gantt-container :deep(.gantt_task_bg .gantt_task_cell) {
border-right-width: 2px;
}
.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;
}
</style>

@ -18,59 +18,46 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</ContentWrap> </ContentWrap>
<ContentWrap> <ContentWrap>
<div v-loading="loading" class="gantt-page-wrap"> <div v-loading="loading">
<div ref="ganttContainerRef" class="gantt-chart-container"></div> <ScheduleGanttPanel :schedule-list="scheduleList" :editable="false" height="calc(100vh - 320px)" />
<div class="gantt-detail-panel">
<div class="gantt-detail-title">设备信息</div>
<el-descriptions v-if="activeDevice" :column="1" border size="small">
<el-descriptions-item label="设备名称">{{ activeDevice.deviceName || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备编码">{{ activeDevice.deviceCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="计划条数">{{ activeDevice.plans?.length ?? 0 }}</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无设备计划" :image-size="80" />
<div class="gantt-plan-list-title">计划明细</div>
<div class="gantt-plan-list">
<div
v-for="(plan, index) in activeDevice?.plans ?? []"
:key="`${activeDevice?.deviceId}-${plan.planId}-${index}`"
class="gantt-plan-item"
>
<div>计划ID{{ plan.planId ?? '-' }}</div>
<div>产品{{ plan.productName ?? '-' }}</div>
<div>计划数量{{ plan.planNumber ?? '-' }}</div>
<div>开始{{ plan.planStartTimeStr || '-' }}</div>
<div>结束{{ plan.planEndTimeStr || '-' }}</div>
<div>最晚开工{{ plan.latestStartTimeStr || '-' }}</div>
</div>
</div>
</div>
</div> </div>
</ContentWrap> </ContentWrap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DevicePlanGanttRespVO, PlanApi } from '@/api/mes/plan' import { DevicePlanGanttRespVO, PlanApi } from '@/api/mes/plan'
import ScheduleGanttPanel from '@/views/mes/components/ScheduleGanttPanel.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
defineOptions({ name: 'MesGanttChart' }) defineOptions({ name: 'MesGanttChart' })
type DevicePlanView = DevicePlanGanttRespVO & { type UnifiedPlan = {
plans: (DevicePlanGanttRespVO['plans'][number] & { taskId: string | number
taskDetailId: string | number
taskCode: string
productCode: string
productName: string
planNumber: number
planStartTimeStr: string planStartTimeStr: string
planEndTimeStr: string planEndTimeStr: string
latestStartTimeStr: string latestStartTimeStr: string
})[] deliveryDateStr: string
scheduleDays: number
sourceType: 'HISTORY'
}
type UnifiedDevice = {
deviceId: string | number
deviceName: string
ratedCapacity?: number | string
plans: UnifiedPlan[]
} }
const loading = ref(false) const loading = ref(false)
const queryFormRef = ref() const queryFormRef = ref()
const ganttContainerRef = ref<HTMLDivElement>() const scheduleList = ref<UnifiedDevice[]>([])
const ganttEventIds = ref<string[]>([])
const scheduleList = ref<DevicePlanView[]>([])
const activeDevice = ref<DevicePlanView>()
const buildDefaultRange = () => { const buildDefaultRange = () => {
const start = dayjs().startOf('month').startOf('day') const start = dayjs().startOf('month').startOf('day')
@ -84,221 +71,42 @@ const queryParams = reactive({
endTime: queryRange.value[1] endTime: queryRange.value[1]
}) })
const getGlobalDateRange = (devices: DevicePlanView[]) => { const formatDetailDate = (value: string | number | null | undefined) => {
const allPlans = devices.flatMap((item) => item.plans ?? [])
const starts = allPlans.map((item) => dayjs(item.planStartTime).valueOf()).filter((item) => Number.isFinite(item))
const ends = allPlans.map((item) => dayjs(item.planEndTime).valueOf()).filter((item) => 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: string) => {
const date = dayjs(value) const date = dayjs(value)
if (!date.isValid()) return undefined if (!date.isValid()) return ''
return date.format('YYYY-MM-DD HH:mm')
}
const formatDetailDate = (value: string) => {
const date = dayjs(value)
if (!date.isValid()) return '-'
return date.format('YYYY-MM-DD HH:mm:ss') return date.format('YYYY-MM-DD HH:mm:ss')
} }
const getDeviceTaskRangeByChildren = (planTasks: { start_date: string; end_date: string }[]) => { const calcScheduleDays = (start: string, end: string) => {
const starts = planTasks.map((item) => dayjs(item.start_date)).filter((item) => item.isValid()) const startValue = dayjs(start)
const ends = planTasks.map((item) => dayjs(item.end_date)).filter((item) => item.isValid()) const endValue = dayjs(end)
if (!starts.length || !ends.length) return undefined if (!startValue.isValid() || !endValue.isValid()) return 1
const earliestStart = starts.reduce((min, current) => (current.valueOf() < min.valueOf() ? current : min)) return Math.max(endValue.endOf('day').diff(startValue.startOf('day'), 'day') + 1, 1)
const latestEnd = ends.reduce((max, current) => (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 buildGanttData = (devices: DevicePlanView[]) => {
const tasks: any[] = []
const links: any[] = []
let linkIndex = 1
devices.forEach((device) => {
const deviceId = `device-${device.deviceId}`
const plans = (device.plans ?? []).map((plan) => ({
...plan,
_start: dayjs(plan.planStartTime),
_end: dayjs(plan.planEndTime)
}))
const validPlans = plans
.filter((plan) => plan._start.isValid() && plan._end.isValid())
.sort((a, b) => a._start.valueOf() - b._start.valueOf())
const firstPlan = validPlans[0]
const deviceRange = getDeviceTaskRangeByChildren(
validPlans.map((item) => ({
start_date: item.planStartTime,
end_date: item.planEndTime
}))
)
tasks.push({
id: deviceId,
text: `${device.deviceName ?? '-'}`,
start_date: formatGanttDate(deviceRange?.start_date ? dayjs(deviceRange.start_date).toISOString() : firstPlan?.planStartTime),
end_date: formatGanttDate(deviceRange?.end_date ? dayjs(deviceRange.end_date).toISOString() : firstPlan?.planEndTime),
duration: deviceRange?.duration ?? 1,
parent: 0,
progress: 0,
open: true,
readonly: true,
_deviceData: device
})
let previousPlanTaskId: string | null = null
validPlans.forEach((plan, index) => {
const startDate = formatGanttDate(plan.planStartTime)
const endDate = formatGanttDate(plan.planEndTime)
const duration = Math.max(plan._end.endOf('day').diff(plan._start.startOf('day'), 'day') + 1, 1)
if (!startDate || !endDate) return
const planTaskId = `plan-${device.deviceId}-${plan.planId ?? index}-${index}`
tasks.push({
id: planTaskId,
text: `${plan.productName ?? '-'} / 数量: ${plan.planNumber ?? '-'}`,
start_date: startDate,
end_date: endDate,
duration,
parent: deviceId,
progress: 0,
readonly: true,
_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 initGantt = () => {
if (!ganttContainerRef.value) return
destroyGantt()
gantt.plugins({ tooltip: true })
gantt.config.readonly = true
gantt.config.drag_move = false
gantt.config.drag_links = false
gantt.config.drag_progress = false
gantt.config.drag_resize = false
gantt.config.order_branch = false
gantt.config.order_branch_free = false
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: 240
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 180,
template: (task: any) => formatDetailDate(task.start_date)
},
{
name: 'duration',
label: '天数',
align: 'center',
width: 70,
template: (task: any) => String(task.duration ?? 0)
} }
]
gantt.config.scales = [ const mapScheduleList = (list: DevicePlanGanttRespVO[]): UnifiedDevice[] =>
{ unit: 'month', step: 1, format: (date) => dayjs(date).format('YYYY年M月') }, (Array.isArray(list) ? list : []).map((device) => ({
{ unit: 'day', step: 1, format: (date) => dayjs(date).format('MM-DD') } deviceId: device?.deviceId ?? '',
] deviceName: device?.deviceName ?? '-',
plans: (device?.plans ?? []).map((plan, index) => {
gantt.templates.task_class = (_start, _end, task: any) => (task?._planData ? 'gantt-plan-task' : '') const current = plan as any
gantt.templates.tooltip_text = (start, end, task: any) => { const planStartTimeStr = formatDetailDate(current?.planStartTime)
const plan = task._planData const planEndTimeStr = formatDetailDate(current?.planEndTime)
if (plan) { return {
return ` taskId: current?.taskId ?? current?.planId ?? index,
<div>产品${plan.productName ?? '-'}</div> taskDetailId: current?.planId ?? index,
<div>数量${plan.planNumber ?? '-'}</div> taskCode: String(current?.planId ?? '-'),
<div>开始${dayjs(start).format('YYYY-MM-DD HH:mm:ss')}</div> productCode: String(current?.productCode ?? '-'),
<div>结束${dayjs(end).format('YYYY-MM-DD HH:mm:ss')}</div> productName: String(current?.productName ?? '-'),
` planNumber: Number(current?.planNumber ?? 0),
} planStartTimeStr,
const device = task._deviceData planEndTimeStr,
return ` latestStartTimeStr: formatDetailDate(current?.latestStartTime),
<div>设备${device?.deviceName ?? '-'}</div> deliveryDateStr: formatDetailDate(current?.deliveryDate),
<div>计划条数${device?.plans?.length ?? 0}</div> scheduleDays: calcScheduleDays(planStartTimeStr, planEndTimeStr),
` sourceType: 'HISTORY' as const
}
const globalRange = getGlobalDateRange(scheduleList.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 = buildGanttData(scheduleList.value)
gantt.parse(ganttData)
if (ganttData.data.length) {
activeDevice.value = ganttData.data[0]._deviceData
gantt.showDate(gantt.config.start_date)
} else {
activeDevice.value = undefined
} }
const clickEventId = gantt.attachEvent('onTaskClick', (id) => {
const task = gantt.getTask(id)
activeDevice.value = task?._deviceData
return true
}) })
ganttEventIds.value.push(clickEventId)
}
const mapScheduleList = (list: DevicePlanGanttRespVO[]) =>
(Array.isArray(list) ? list : []).map((device) => ({
...device,
plans: (device.plans ?? []).map((plan) => ({
...plan,
planStartTimeStr: formatDetailDate(plan.planStartTime),
planEndTimeStr: formatDetailDate(plan.planEndTime),
latestStartTimeStr: formatDetailDate(plan.latestStartTime)
}))
})) }))
const getList = async () => { const getList = async () => {
@ -309,8 +117,6 @@ const getList = async () => {
endTime: queryParams.endTime endTime: queryParams.endTime
}) })
scheduleList.value = mapScheduleList(data) scheduleList.value = mapScheduleList(data)
await nextTick()
initGantt()
} finally { } finally {
loading.value = false loading.value = false
} }
@ -334,74 +140,4 @@ const resetQuery = () => {
onMounted(() => { onMounted(() => {
handleQuery() handleQuery()
}) })
onBeforeUnmount(() => {
destroyGantt()
})
</script> </script>
<style scoped>
.gantt-page-wrap {
display: flex;
width: 100%;
gap: 12px;
}
.gantt-chart-container {
flex: 1;
min-width: 0;
height: calc(100vh - 320px);
min-height: 640px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
}
.gantt-detail-panel {
width: 320px;
flex: 0 0 320px;
}
.gantt-detail-title {
margin-bottom: 10px;
font-weight: 600;
}
.gantt-plan-list-title {
margin-top: 14px;
margin-bottom: 10px;
font-weight: 600;
}
.gantt-plan-list {
max-height: calc(100vh - 420px);
min-height: 240px;
overflow: auto;
border: 1px solid var(--el-border-color);
border-radius: 4px;
padding: 8px;
}
.gantt-plan-item {
padding: 8px;
border-radius: 4px;
background: var(--el-fill-color-light);
}
.gantt-plan-item + .gantt-plan-item {
margin-top: 8px;
}
.gantt-chart-container :deep(.gantt_grid_data .gantt_row) {
font-weight: 600;
}
.gantt-chart-container :deep(.gantt_task_line.gantt-plan-task) {
background: #409eff;
border-color: #409eff;
}
.gantt-chart-container :deep(.gantt_task_line.gantt-plan-task .gantt_task_content) {
color: #ffffff;
}
</style>

@ -1,74 +1,17 @@
<template> <template>
<Dialog v-model="previewVisible" title="排产甘特图预览" width="100%" align-center> <Dialog v-model="previewVisible" title="排产甘特图预览" width="100%" align-center>
<div class="schedule-preview-wrap"> <ScheduleGanttPanel :schedule-list="previewScheduleList" :editable="true" height="800px" />
<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> <template #footer>
<el-button type="primary" :loading="previewSaveLoading" @click="handlePreviewSave"></el-button> <el-button type="primary" :loading="previewSaveLoading" @click="handlePreviewSave"></el-button>
<el-button @click="previewVisible = false">关闭</el-button> <el-button @click="previewVisible = false">关闭</el-button>
</template> </template>
</Dialog> </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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PlanApi } from '@/api/mes/plan' import { PlanApi } from '@/api/mes/plan'
import ScheduleGanttPanel from '@/views/mes/components/ScheduleGanttPanel.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
defineOptions({ name: 'TaskSchedulePreviewDialog' }) defineOptions({ name: 'TaskSchedulePreviewDialog' })
@ -84,19 +27,6 @@ const emit = defineEmits<{
const message = useMessage() const message = useMessage()
const previewSaveLoading = ref(false) 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({ const previewVisible = computed({
get: () => props.modelValue, get: () => props.modelValue,
@ -104,634 +34,6 @@ const previewVisible = computed({
}) })
const previewScheduleList = computed(() => (Array.isArray(props.scheduleList) ? props.scheduleList : [])) 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 handlePreviewSave = async () => {
const createReqVOList = previewScheduleList.value.flatMap((device: any) => { const createReqVOList = previewScheduleList.value.flatMap((device: any) => {
@ -778,140 +80,4 @@ const handlePreviewSave = async () => {
previewSaveLoading.value = false 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> </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>

Loading…
Cancel
Save