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/components/ScheduleGanttPanel.vue

1206 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>
<div v-if="!editable" class="schedule-status-legend">
<div
v-for="item in sortedPlanStatusList"
:key="item.key"
class="legend-item"
:style="{ borderLeftColor: item.color }"
>
<span class="legend-color" :style="{ backgroundColor: item.color }"></span>
<span class="legend-label">{{ item.label }}</span>
</div>
</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>
<span
v-if="plan.planStatus && PLAN_STATUS_COLOR_MAP[plan.planStatus]"
class="plan-status-tag"
:style="{ backgroundColor: PLAN_STATUS_COLOR_MAP[plan.planStatus].color }"
>
{{ PLAN_STATUS_COLOR_MAP[plan.planStatus].label }}
</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-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>
<el-dialog v-if="props.editable" v-model="startDateEditorVisible" title="修改开始时间" width="360px" append-to-body>
<el-form label-width="80px">
<el-form-item label="开始时间">
<el-date-picker
v-model="startDateEditorValue"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择开始时间"
class="!w-full"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="startDateEditorVisible = false">取消</el-button>
<el-button type="primary" @click="handleStartDateEditorSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
defineOptions({ name: 'ScheduleGanttPanel' })
// 计划状态颜色映射表
const PLAN_STATUS_COLOR_MAP = {
1: { label: '已排产', color: '#409eff', textColor: '#ffffff', sort: 1 },
8: { label: '已开工', color: '#67c23a', textColor: '#ffffff', sort: 2 },
3: { label: '暂停', color: '#e6a23c', textColor: '#ffffff', sort: 3 },
4: { label: '待入库', color: '#f56c6c', textColor: '#ffffff', sort: 4 },
5: { label: '已入库', color: '#8e7cc3', textColor: '#ffffff', sort: 5 },
}
const sortedPlanStatusList = Object.entries(PLAN_STATUS_COLOR_MAP)
.map(([key, val]) => ({ key: Number(key), ...val }))
.sort((a, b) => a.sort - b.sort)
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 activePreviewTask = ref<any>()
const startDateEditorVisible = ref(false)
const startDateEditorTaskId = ref<string | number | null>(null)
const startDateEditorValue = ref('')
const tooltipCleanupFns = ref<(() => void)[]>([])
const ganttEventIds = ref<string[]>([])
const ganttSyncing = ref(false)
const taskAdjustDialogVisible = ref(false)
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 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 = () => {
cleanupTaskTooltips()
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
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 || !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
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 || !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 handleStartDateEditorSubmit = () => {
if (!startDateEditorTaskId.value || !startDateEditorValue.value) {
startDateEditorVisible.value = false
return
}
const task = gantt.getTask(startDateEditorTaskId.value)
if (!task?._planData) {
startDateEditorVisible.value = false
return
}
const newStart = dayjs(startDateEditorValue.value)
if (!newStart.isValid()) {
message.warning('请选择有效的时间')
return
}
const duration = Math.max(Number(task.duration) || 1, 1)
const newEnd = newStart.add(duration - 1, 'day').endOf('day')
ganttSyncing.value = true
try {
task.start_date = newStart.toDate()
task.end_date = newEnd.toDate()
task.duration = duration
syncPlanTimeFromTask(task)
normalizeDeviceChildren(task.parent)
refreshPlanLinksByRowOrder()
refreshTimelineRangeByTasks()
gantt.updateTask(task.id)
} finally {
ganttSyncing.value = false
}
startDateEditorVisible.value = false
}
const refreshTimelineRangeByTasks = () => {
let minStart = Number.POSITIVE_INFINITY
let maxEnd = Number.NEGATIVE_INFINITY
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 = !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" data-task-id="${task.id}">${formatGridDateText(task.start_date)}</span>`
: formatGridDateText(task.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 ''
// 当 editable 为 false 时,根据 planStatus 显示对应的颜色
if (!props.editable) {
const planStatus = task._planData?.planStatus
const statusMap: Record<number | string, string> = {
'1': 'schedule-plan-task-status-1',
1: 'schedule-plan-task-status-1',
'8': 'schedule-plan-task-status-8',
8: 'schedule-plan-task-status-8',
'3': 'schedule-plan-task-status-3',
3: 'schedule-plan-task-status-3',
'4': 'schedule-plan-task-status-4',
4: 'schedule-plan-task-status-4',
'5': 'schedule-plan-task-status-5',
5: 'schedule-plan-task-status-5',
'2': 'schedule-plan-task-status-2',
2: 'schedule-plan-task-status-2'
}
return statusMap[planStatus] || 'schedule-plan-task-default'
}
// 当 editable 为 true 时,使用原来的逻辑
const sourceType = String(task?._planData?.sourceType ?? '').toUpperCase()
return sourceType === '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)
// 强制刷新所有任务的样式,确保颜色正确应用
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
}
if (!props.editable) return true
const target = event?.target as HTMLElement | null
const editableNode = target?.closest('.gantt-inline-editor-trigger') as HTMLElement | null
const field = editableNode?.dataset?.field
if (!field || !task?._planData || String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return true
if (field === 'start_date') {
startDateEditorTaskId.value = id
startDateEditorValue.value = dayjs(task.start_date).format('YYYY-MM-DD HH:mm:ss')
startDateEditorVisible.value = true
return false
}
const inlineEditors = (gantt.ext as any)?.inlineEditors
if (inlineEditors?.startEdit) {
inlineEditors.startEdit(id, field)
return false
}
return true
})
ganttEventIds.value.push(clickEventId)
if (props.editable && ganttContainerRef.value) {
const gridClickHandler = (e: MouseEvent) => {
const target = e.target as HTMLElement
const trigger = target.closest('.gantt-inline-editor-trigger[data-field="start_date"]') as HTMLElement | null
if (!trigger) return
const taskId = trigger.getAttribute('data-task-id')
if (!taskId) return
try {
const task = gantt.getTask(taskId)
if (!task?._planData || String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return
e.stopPropagation()
startDateEditorTaskId.value = taskId
startDateEditorValue.value = dayjs(task.start_date).format('YYYY-MM-DD HH:mm:ss')
startDateEditorVisible.value = true
} catch {}
}
ganttContainerRef.value.addEventListener('click', gridClickHandler, true)
tooltipCleanupFns.value.push(() => {
ganttContainerRef.value?.removeEventListener('click', gridClickHandler, true)
})
}
if (!props.editable) return
const beforeDragEventId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
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-status-legend {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 8px;
margin-bottom: 12px;
padding: 8px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-left: 3px solid;
font-size: 12px;
}
.legend-color {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.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;
}
.plan-status-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 2px;
font-size: 12px;
color: #ffffff;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.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-status-1) {
background: #409eff !important;
border-color: #409eff !important;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-1 .gantt_task_content) {
color: #ffffff !important;
}
/* 已开工状态 - 绿色 */
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-8) {
background: #67c23a !important;
border-color: #67c23a !important;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-8 .gantt_task_content) {
color: #ffffff !important;
}
/* 暂停状态 - 橙色 */
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-3) {
background: #e6a23c !important;
border-color: #e6a23c !important;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-3 .gantt_task_content) {
color: #ffffff !important;
}
/* 待入库状态 - 红色 */
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-4) {
background: #f56c6c !important;
border-color: #f56c6c !important;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-4 .gantt_task_content) {
color: #ffffff !important;
}
/* 已入库状态 - 紫色 */
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-5) {
background: #8e7cc3 !important;
border-color: #8e7cc3 !important;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-5 .gantt_task_content) {
color: #ffffff !important;
}
/* 其他状态 - 深灰 */
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-2) {
background: #606266 !important;
border-color: #606266 !important;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-status-2 .gantt_task_content) {
color: #ffffff !important;
}
/* 默认状态 */
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-default) {
background: #909399;
border-color: #909399;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-default .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>