|
|
<template>
|
|
|
<div class="schedule-preview-wrap">
|
|
|
<div ref="ganttContainerRef" class="schedule-gantt-container" :style="{ height }"></div>
|
|
|
<div class="schedule-detail-panel">
|
|
|
<div class="schedule-detail-title">计划信息</div>
|
|
|
<template v-if="activePreviewDevice">
|
|
|
<el-descriptions :column="1" border size="small">
|
|
|
<el-descriptions-item label="设备名称">{{ activePreviewDevice.deviceName }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="设备ID">{{ activePreviewDevice.deviceId }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="产能">{{ activePreviewDevice.ratedCapacity ?? '-' }}</el-descriptions-item>
|
|
|
<el-descriptions-item v-if="'dailyAverageValue' in activePreviewDevice" label="每日报工平均值">{{ activePreviewDevice.dailyAverageValue ?? '-' }}</el-descriptions-item>
|
|
|
<el-descriptions-item v-if="'dataCollectionCapacity' in activePreviewDevice" label="数据采集产能">{{ activePreviewDevice.dataCollectionCapacity ?? '-' }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="计划条数">{{ activePreviewDevice.plans?.length ?? 0 }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
<div class="schedule-plan-list-title">计划明细</div>
|
|
|
<div class="schedule-plan-list">
|
|
|
<div
|
|
|
v-for="(plan, index) in activePreviewTask ? [activePreviewTask] : (activePreviewDevice?.plans ?? [])"
|
|
|
:key="`${activePreviewDevice?.deviceId}-${plan.taskDetailId}-${index}`"
|
|
|
:class="['schedule-plan-item', { 'schedule-plan-item-active': plan.sourceType === 'CURRENT' }]"
|
|
|
>
|
|
|
<div class="schedule-plan-item-head">
|
|
|
<span class="schedule-plan-item-title">{{ plan.productCode ?? '-' }} / {{ plan.productName ?? '-' }}</span>
|
|
|
</div>
|
|
|
<div>计划编码:{{ plan.taskCode ?? '-' }}</div>
|
|
|
<div>计划数量:{{ plan.planNumber ?? '-' }}</div>
|
|
|
<div>交货日期:{{ plan.deliveryDateStr ?? '-' }}</div>
|
|
|
<div>开始:{{ plan.planStartTimeStr || '-' }}</div>
|
|
|
<div>结束:{{ plan.planEndTimeStr || '-' }}</div>
|
|
|
<div>最晚开工:{{ plan.latestStartTimeStr || '-' }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<el-empty v-else description="暂无计划信息" :image-size="80" />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<el-dialog v-model="taskAdjustDialogVisible" title="调整任务" width="420px" append-to-body>
|
|
|
<el-form label-width="110px">
|
|
|
<el-form-item label="设备">
|
|
|
<el-select v-model="taskAdjustForm.deviceTaskId" placeholder="请选择设备" class="!w-full">
|
|
|
<el-option v-for="item in previewDeviceOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="计划开始日期">
|
|
|
<el-date-picker
|
|
|
v-model="taskAdjustForm.startDate"
|
|
|
type="datetime"
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
placeholder="请选择计划开始日期"
|
|
|
class="!w-full"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="计划结束日期">
|
|
|
<el-date-picker
|
|
|
v-model="taskAdjustForm.endDate"
|
|
|
type="datetime"
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
placeholder="请选择计划结束日期"
|
|
|
class="!w-full"
|
|
|
/>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<el-button @click="taskAdjustDialogVisible = false">取消</el-button>
|
|
|
<el-button type="primary" @click="handleTaskAdjustSubmit">确定</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import dayjs from 'dayjs'
|
|
|
import { gantt } from 'dhtmlx-gantt'
|
|
|
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
|
|
|
|
|
|
defineOptions({ name: 'ScheduleGanttPanelEditable' })
|
|
|
|
|
|
const props = withDefaults(
|
|
|
defineProps<{
|
|
|
scheduleList: any[]
|
|
|
height?: string
|
|
|
}>(),
|
|
|
{
|
|
|
height: '800px'
|
|
|
}
|
|
|
)
|
|
|
|
|
|
const message = useMessage()
|
|
|
const ganttContainerRef = ref<HTMLDivElement>()
|
|
|
const activePreviewDevice = ref<any>()
|
|
|
const activePreviewTask = ref<any>()
|
|
|
const tooltipCleanupFns = ref<(() => void)[]>([])
|
|
|
const ganttEventIds = ref<string[]>([])
|
|
|
const ganttSyncing = ref(false)
|
|
|
const taskAdjustDialogVisible = ref(false)
|
|
|
const taskAdjustTaskId = ref<string | number | null>(null)
|
|
|
const taskAdjustForm = reactive({
|
|
|
deviceTaskId: '',
|
|
|
startDate: '',
|
|
|
endDate: ''
|
|
|
})
|
|
|
const taskMoveFromParentMap = ref<Record<string, string | number | undefined>>({})
|
|
|
const taskDragDurationMap = ref<Record<string, number>>({})
|
|
|
|
|
|
const previewScheduleList = computed(() => (Array.isArray(props.scheduleList) ? props.scheduleList : []))
|
|
|
const previewDeviceOptions = computed(() =>
|
|
|
previewScheduleList.value.map((device: any) => ({
|
|
|
label: device?.deviceName ?? '-',
|
|
|
value: `device-${device?.deviceId}`
|
|
|
}))
|
|
|
)
|
|
|
|
|
|
const hasCurrentPlan = (device: any) =>
|
|
|
(device?.plans ?? []).some((plan: any) => String(plan.sourceType ?? '').toUpperCase() === 'CURRENT')
|
|
|
|
|
|
const getGanttScheduleList = () =>
|
|
|
previewScheduleList.value.filter((device: any) => hasCurrentPlan(device))
|
|
|
|
|
|
const getGlobalDateRange = (scheduleList: any[]) => {
|
|
|
const allPlans = scheduleList.flatMap((item: any) => item?.plans ?? [])
|
|
|
const starts = allPlans.map((item: any) => dayjs(item?.planStartTimeStr).valueOf()).filter((item: number) => Number.isFinite(item))
|
|
|
const ends = allPlans.map((item: any) => dayjs(item?.planEndTimeStr).valueOf()).filter((item: number) => Number.isFinite(item))
|
|
|
if (!starts.length || !ends.length) {
|
|
|
const now = Date.now()
|
|
|
return {
|
|
|
start: now,
|
|
|
end: now + 7 * 24 * 60 * 60 * 1000
|
|
|
}
|
|
|
}
|
|
|
return {
|
|
|
start: Math.min(...starts),
|
|
|
end: Math.max(...ends)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const formatGanttDate = (value: unknown) => {
|
|
|
const date = dayjs(value)
|
|
|
if (!date.isValid()) return undefined
|
|
|
return date.format('YYYY-MM-DD HH:mm')
|
|
|
}
|
|
|
|
|
|
const getDeviceTaskRangeByChildren = (planTasks: any[]) => {
|
|
|
const starts = planTasks.map((item: any) => dayjs(item?.start_date)).filter((item: any) => item.isValid())
|
|
|
const ends = planTasks.map((item: any) => dayjs(item?.end_date)).filter((item: any) => item.isValid())
|
|
|
if (!starts.length || !ends.length) return undefined
|
|
|
const earliestStart = starts.reduce((min: any, current: any) => (current.valueOf() < min.valueOf() ? current : min))
|
|
|
const latestEnd = ends.reduce((max: any, current: any) => (current.valueOf() > max.valueOf() ? current : max))
|
|
|
const duration = Math.max(latestEnd.endOf('day').diff(earliestStart.startOf('day'), 'day') + 1, 1)
|
|
|
return {
|
|
|
start_date: earliestStart.toDate(),
|
|
|
end_date: latestEnd.toDate(),
|
|
|
duration
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const buildPreviewGanttData = (scheduleList: any[]) => {
|
|
|
const tasks: any[] = []
|
|
|
const links: any[] = []
|
|
|
let linkIndex = 1
|
|
|
|
|
|
scheduleList.forEach((device: any) => {
|
|
|
const deviceId = `device-${device.deviceId}`
|
|
|
const plans = (device?.plans ?? []).map((plan: any) => ({
|
|
|
...plan,
|
|
|
_start: dayjs(plan?.planStartTimeStr),
|
|
|
_end: dayjs(plan?.planEndTimeStr)
|
|
|
}))
|
|
|
const validPlans = plans
|
|
|
.filter((plan: any) => plan._start.isValid() && plan._end.isValid())
|
|
|
.sort((a: any, b: any) => a._start.valueOf() - b._start.valueOf())
|
|
|
const firstPlan = validPlans[0]
|
|
|
const deviceRange = getDeviceTaskRangeByChildren(
|
|
|
validPlans.map((item: any) => ({
|
|
|
start_date: item?._start,
|
|
|
end_date: item?._end
|
|
|
}))
|
|
|
)
|
|
|
|
|
|
tasks.push({
|
|
|
id: deviceId,
|
|
|
text: `${device.deviceName ?? '-'}`,
|
|
|
start_date: formatGanttDate(deviceRange?.start_date ?? firstPlan?._start),
|
|
|
end_date: formatGanttDate(deviceRange?.end_date ?? firstPlan?._end),
|
|
|
duration: deviceRange?.duration ?? 1,
|
|
|
parent: 0,
|
|
|
progress: 0,
|
|
|
open: true,
|
|
|
readonly: true,
|
|
|
deviceName: device.deviceName ?? '-',
|
|
|
_deviceData: device
|
|
|
})
|
|
|
|
|
|
let previousPlanTaskId: string | null = null
|
|
|
validPlans.forEach((plan: any, index: number) => {
|
|
|
const startDate = formatGanttDate(plan.planStartTimeStr)
|
|
|
const endDate = formatGanttDate(plan.planEndTimeStr)
|
|
|
const duration = Number(plan.scheduleDays) > 0 ? Number(plan.scheduleDays) : Math.max(plan._end.diff(plan._start, 'day') + 1, 1)
|
|
|
if (!startDate || !endDate) return
|
|
|
const isHistory = String(plan.sourceType ?? '').toUpperCase() === 'HISTORY'
|
|
|
const planTaskId = `plan-${device.deviceId}-${plan.taskDetailId ?? index}-${index}`
|
|
|
tasks.push({
|
|
|
id: planTaskId,
|
|
|
text: `${plan.productCode ?? '-'} / ${plan.productName ?? '-'} / ${plan.taskCode ?? '-'}`,
|
|
|
start_date: startDate,
|
|
|
end_date: endDate,
|
|
|
duration,
|
|
|
parent: deviceId,
|
|
|
progress: 0,
|
|
|
readonly: isHistory,
|
|
|
_planData: plan,
|
|
|
_deviceData: device
|
|
|
})
|
|
|
if (previousPlanTaskId) {
|
|
|
links.push({
|
|
|
id: `link-${linkIndex++}`,
|
|
|
source: previousPlanTaskId,
|
|
|
target: planTaskId,
|
|
|
type: '0'
|
|
|
})
|
|
|
}
|
|
|
previousPlanTaskId = planTaskId
|
|
|
})
|
|
|
})
|
|
|
|
|
|
return { data: tasks, links }
|
|
|
}
|
|
|
|
|
|
const cleanupTaskTooltips = () => {
|
|
|
const tooltipsExt = (gantt.ext as any)?.tooltips
|
|
|
const tooltip = tooltipsExt?.tooltip
|
|
|
if (tooltipsExt?.detach) {
|
|
|
tooltipsExt.detach('.gantt_task_line')
|
|
|
tooltipsExt.detach('.gantt_task_content')
|
|
|
}
|
|
|
if (tooltip?.hide) {
|
|
|
tooltip.hide()
|
|
|
}
|
|
|
const node = tooltip?.getNode?.()
|
|
|
if (node) {
|
|
|
node.style.display = 'none'
|
|
|
node.style.visibility = 'hidden'
|
|
|
node.style.opacity = '0'
|
|
|
}
|
|
|
tooltipCleanupFns.value.forEach((fn) => fn())
|
|
|
tooltipCleanupFns.value = []
|
|
|
}
|
|
|
|
|
|
const destroyGantt = () => {
|
|
|
try {
|
|
|
cleanupTaskTooltips()
|
|
|
} catch {}
|
|
|
try {
|
|
|
ganttEventIds.value.forEach((eventId) => {
|
|
|
try {
|
|
|
gantt.detachEvent(eventId)
|
|
|
} catch {}
|
|
|
})
|
|
|
} catch {}
|
|
|
ganttEventIds.value = []
|
|
|
try {
|
|
|
gantt.clearAll()
|
|
|
} catch {}
|
|
|
}
|
|
|
|
|
|
const formatGridDateText = (value: unknown) => {
|
|
|
const date = dayjs(value)
|
|
|
if (!date.isValid()) return '-'
|
|
|
return date.format('YYYY-MM-DD HH:mm:ss')
|
|
|
}
|
|
|
const formatTooltipDateTime = (value: unknown) => {
|
|
|
const date = dayjs(value)
|
|
|
if (!date.isValid()) return '-'
|
|
|
return date.format('YYYY-MM-DD HH:mm:ss')
|
|
|
}
|
|
|
const getDeviceTaskSummary = (task: any) => {
|
|
|
if (!task) {
|
|
|
return {
|
|
|
planCount: 0,
|
|
|
totalPlanNumber: 0,
|
|
|
earliestStart: '-',
|
|
|
latestEnd: '-'
|
|
|
}
|
|
|
}
|
|
|
const childTaskIds = gantt.getChildren(task.id)
|
|
|
const childTasks = (Array.isArray(childTaskIds) ? childTaskIds : [])
|
|
|
.map((childId: string | number) => gantt.getTask(childId))
|
|
|
.filter((child: any) => child?._planData)
|
|
|
if (!childTasks.length) {
|
|
|
return {
|
|
|
planCount: 0,
|
|
|
totalPlanNumber: 0,
|
|
|
earliestStart: '-',
|
|
|
latestEnd: '-'
|
|
|
}
|
|
|
}
|
|
|
const starts = childTasks.map((child: any) => dayjs(child.start_date).valueOf()).filter((val: number) => Number.isFinite(val))
|
|
|
const ends = childTasks.map((child: any) => dayjs(child.end_date).valueOf()).filter((val: number) => Number.isFinite(val))
|
|
|
const totalPlanNumber = childTasks.reduce((sum: number, child: any) => sum + Number(child?._planData?.planNumber ?? 0), 0)
|
|
|
return {
|
|
|
planCount: childTasks.length,
|
|
|
totalPlanNumber,
|
|
|
earliestStart: starts.length ? formatTooltipDateTime(Math.min(...starts)) : '-',
|
|
|
latestEnd: ends.length ? formatTooltipDateTime(Math.max(...ends)) : '-'
|
|
|
}
|
|
|
}
|
|
|
const buildTaskTooltipHtml = (task: any, start?: Date, end?: Date) => {
|
|
|
const plan = task?._planData
|
|
|
if (plan) {
|
|
|
return `
|
|
|
<div><b>任务明细</b></div>
|
|
|
<div>任务单:${plan.taskCode ?? '-'}</div>
|
|
|
<div>产品:${plan.productCode ?? '-'} / ${plan.productName ?? '-'}</div>
|
|
|
<div>明细ID:${plan.taskDetailId ?? '-'}</div>
|
|
|
<div>计划数量:${plan.planNumber ?? '-'}</div>
|
|
|
<div>开始:${formatTooltipDateTime(start ?? task?.start_date)}</div>
|
|
|
<div>结束:${formatTooltipDateTime(end ?? task?.end_date)}</div>
|
|
|
<div>最晚开工:${formatTooltipDateTime(plan.latestStartTimeStr)}</div>
|
|
|
`
|
|
|
}
|
|
|
const device = task?._deviceData
|
|
|
const summary = getDeviceTaskSummary(task)
|
|
|
return `
|
|
|
<div><b>汇总</b></div>
|
|
|
<div>设备:${device?.deviceName ?? '-'}</div>
|
|
|
<div>任务明细条数:${summary.planCount}</div>
|
|
|
<div>计划总数:${summary.totalPlanNumber}</div>
|
|
|
<div>最早计划开始:${summary.earliestStart}</div>
|
|
|
<div>最晚计划结束:${summary.latestEnd}</div>
|
|
|
`
|
|
|
}
|
|
|
const getTaskByTooltipNode = (node: HTMLElement) => {
|
|
|
const holder = node.closest('[task_id]') || node.closest('[data-task-id]') || node.closest('.gantt_task_line') || node
|
|
|
const taskId = holder?.getAttribute('task_id') || holder?.getAttribute('data-task-id')
|
|
|
if (!taskId) return undefined
|
|
|
try {
|
|
|
return gantt.getTask(taskId)
|
|
|
} catch {
|
|
|
return undefined
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const initTaskTooltips = () => {
|
|
|
const tooltipsExt = (gantt.ext as any)?.tooltips
|
|
|
const tooltip = tooltipsExt?.tooltip
|
|
|
if (!tooltipsExt || !tooltip || !ganttContainerRef.value) return
|
|
|
|
|
|
let tooltipHideTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
const forceHideTooltip = () => {
|
|
|
if (tooltipHideTimer) {
|
|
|
clearTimeout(tooltipHideTimer)
|
|
|
tooltipHideTimer = null
|
|
|
}
|
|
|
tooltip.hide()
|
|
|
const node = tooltip.getNode?.()
|
|
|
if (node) {
|
|
|
node.style.display = 'none'
|
|
|
node.style.visibility = 'hidden'
|
|
|
node.style.opacity = '0'
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const showTooltip = (event: MouseEvent, html: string) => {
|
|
|
if (tooltipHideTimer) {
|
|
|
clearTimeout(tooltipHideTimer)
|
|
|
tooltipHideTimer = null
|
|
|
}
|
|
|
tooltip.setContent(html)
|
|
|
tooltip.show(event)
|
|
|
const node = tooltip.getNode?.()
|
|
|
if (node) {
|
|
|
node.style.display = 'block'
|
|
|
node.style.visibility = 'visible'
|
|
|
node.style.opacity = '1'
|
|
|
node.style.zIndex = '10000'
|
|
|
node.style.pointerEvents = 'none'
|
|
|
}
|
|
|
}
|
|
|
|
|
|
tooltipsExt.detach('.gantt_task_line')
|
|
|
tooltipsExt.detach('.gantt_task_content')
|
|
|
tooltipsExt.attach({
|
|
|
selector: '.gantt_task_line,.gantt_task_content',
|
|
|
onmouseenter: (event: MouseEvent, node: HTMLElement) => {
|
|
|
const task = getTaskByTooltipNode(node)
|
|
|
if (!task) return
|
|
|
showTooltip(event, buildTaskTooltipHtml(task))
|
|
|
},
|
|
|
onmousemove: (event: MouseEvent, node: HTMLElement) => {
|
|
|
const task = getTaskByTooltipNode(node)
|
|
|
if (!task) return
|
|
|
showTooltip(event, buildTaskTooltipHtml(task))
|
|
|
},
|
|
|
onmouseleave: () => {
|
|
|
forceHideTooltip()
|
|
|
}
|
|
|
})
|
|
|
|
|
|
const containerEl = ganttContainerRef.value
|
|
|
const containerMouseLeaveHandler = () => {
|
|
|
forceHideTooltip()
|
|
|
}
|
|
|
containerEl.addEventListener('mouseleave', containerMouseLeaveHandler)
|
|
|
|
|
|
const documentMouseMoveHandler = (e: MouseEvent) => {
|
|
|
const target = e.target as HTMLElement
|
|
|
if (!target) return
|
|
|
const isOverTask = target.closest('.gantt_task_line') || target.closest('.gantt_task_content')
|
|
|
if (!isOverTask) {
|
|
|
if (tooltipHideTimer) clearTimeout(tooltipHideTimer)
|
|
|
tooltipHideTimer = setTimeout(forceHideTooltip, 150)
|
|
|
} else {
|
|
|
if (tooltipHideTimer) {
|
|
|
clearTimeout(tooltipHideTimer)
|
|
|
tooltipHideTimer = null
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
document.addEventListener('mousemove', documentMouseMoveHandler)
|
|
|
|
|
|
tooltipCleanupFns.value.push(() => {
|
|
|
containerEl.removeEventListener('mouseleave', containerMouseLeaveHandler)
|
|
|
document.removeEventListener('mousemove', documentMouseMoveHandler)
|
|
|
if (tooltipHideTimer) {
|
|
|
clearTimeout(tooltipHideTimer)
|
|
|
tooltipHideTimer = null
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
const syncPlanTimeFromTask = (task: any) => {
|
|
|
if (!task?._planData) return
|
|
|
if (String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return
|
|
|
const start = dayjs(task.start_date)
|
|
|
const end = dayjs(task.end_date)
|
|
|
if (!start.isValid() || !end.isValid()) return
|
|
|
const duration = Math.max(Number(task.duration) || end.diff(start, 'day') + 1, 1)
|
|
|
task.duration = duration
|
|
|
task._planData.planStartTimeStr = start.format('YYYY-MM-DD HH:mm:ss')
|
|
|
task._planData.planEndTimeStr = end.format('YYYY-MM-DD HH:mm:ss')
|
|
|
task._planData.scheduleDays = duration
|
|
|
activePreviewDevice.value = task._deviceData
|
|
|
if (activePreviewTask.value && String(activePreviewTask.value.taskDetailId) === String(task._planData.taskDetailId)) {
|
|
|
activePreviewTask.value = task._planData
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const syncDeviceTaskRangeFromChildren = (task: any) => {
|
|
|
if (!task?._planData) return
|
|
|
const deviceTaskId = task.parent
|
|
|
if (!deviceTaskId) return
|
|
|
const childTaskIds = gantt.getChildren(deviceTaskId)
|
|
|
if (!Array.isArray(childTaskIds) || !childTaskIds.length) return
|
|
|
const childTasks = childTaskIds.map((childId: string | number) => gantt.getTask(childId)).filter((item: any) => item?._planData)
|
|
|
if (!childTasks.length) return
|
|
|
const deviceRange = getDeviceTaskRangeByChildren(childTasks)
|
|
|
if (!deviceRange) return
|
|
|
const deviceTask = gantt.getTask(deviceTaskId)
|
|
|
deviceTask.start_date = deviceRange.start_date
|
|
|
deviceTask.end_date = deviceRange.end_date
|
|
|
deviceTask.duration = deviceRange.duration
|
|
|
gantt.updateTask(deviceTaskId)
|
|
|
}
|
|
|
|
|
|
const movePlanDataToDevice = (task: any, targetDeviceTaskId: string | number, sourceDeviceTaskId?: string | number) => {
|
|
|
if (!task?._planData) return
|
|
|
const targetDeviceTask = gantt.getTask(targetDeviceTaskId)
|
|
|
const targetDeviceData = targetDeviceTask?._deviceData
|
|
|
if (!targetDeviceData) return
|
|
|
const planIdentity = `${task?._planData?.taskId ?? ''}-${task?._planData?.taskDetailId ?? ''}`
|
|
|
const sourceDeviceTask = sourceDeviceTaskId ? gantt.getTask(sourceDeviceTaskId) : undefined
|
|
|
const sourceDeviceData = sourceDeviceTask?._deviceData
|
|
|
if (sourceDeviceData?.plans && Array.isArray(sourceDeviceData.plans)) {
|
|
|
sourceDeviceData.plans = sourceDeviceData.plans.filter((item: any) => {
|
|
|
const itemIdentity = `${item?.taskId ?? ''}-${item?.taskDetailId ?? ''}`
|
|
|
return itemIdentity !== planIdentity
|
|
|
})
|
|
|
}
|
|
|
if (!Array.isArray(targetDeviceData.plans)) {
|
|
|
targetDeviceData.plans = []
|
|
|
}
|
|
|
const targetExists = targetDeviceData.plans.some((item: any) => {
|
|
|
const itemIdentity = `${item?.taskId ?? ''}-${item?.taskDetailId ?? ''}`
|
|
|
return itemIdentity === planIdentity
|
|
|
})
|
|
|
if (!targetExists) {
|
|
|
targetDeviceData.plans.push(task._planData)
|
|
|
}
|
|
|
task._planData.deviceId = targetDeviceData.deviceId
|
|
|
task._planData.feedingPipeline = targetDeviceData.deviceId
|
|
|
task._deviceData = targetDeviceData
|
|
|
}
|
|
|
|
|
|
const normalizeDeviceChildren = (deviceTaskId: string | number, priorityTaskId?: string | number) => {
|
|
|
try {
|
|
|
gantt.getTask(deviceTaskId)
|
|
|
} catch {
|
|
|
return
|
|
|
}
|
|
|
const childTaskIds = gantt.getChildren(deviceTaskId)
|
|
|
if (!Array.isArray(childTaskIds) || !childTaskIds.length) return
|
|
|
const childTasks = childTaskIds
|
|
|
.map((childId: string | number) => gantt.getTask(childId))
|
|
|
.filter((item: any) => item?._planData)
|
|
|
.sort((a: any, b: any) => {
|
|
|
const startDiff = dayjs(a.start_date).valueOf() - dayjs(b.start_date).valueOf()
|
|
|
if (startDiff !== 0) return startDiff
|
|
|
if (priorityTaskId !== undefined && String(a.id) === String(priorityTaskId)) return -1
|
|
|
if (priorityTaskId !== undefined && String(b.id) === String(priorityTaskId)) return 1
|
|
|
return 0
|
|
|
})
|
|
|
childTasks.forEach((item: any) => {
|
|
|
syncPlanTimeFromTask(item)
|
|
|
gantt.updateTask(item.id)
|
|
|
})
|
|
|
const fakePlanTask = { _planData: true, parent: deviceTaskId }
|
|
|
syncDeviceTaskRangeFromChildren(fakePlanTask)
|
|
|
}
|
|
|
|
|
|
const refreshPlanLinksByRowOrder = () => {
|
|
|
try {
|
|
|
const allLinks = gantt.getLinks()
|
|
|
allLinks.forEach((item: any) => gantt.deleteLink(item.id))
|
|
|
let linkIndex = 1
|
|
|
const deviceTaskIds = gantt.getChildren(0)
|
|
|
deviceTaskIds.forEach((deviceTaskId: string | number) => {
|
|
|
try {
|
|
|
const childTaskIds = gantt.getChildren(deviceTaskId).filter((childId: string | number) => {
|
|
|
const task = gantt.getTask(childId)
|
|
|
return !!task?._planData
|
|
|
})
|
|
|
let previousTaskId: string | number | null = null
|
|
|
childTaskIds.forEach((taskId: string | number) => {
|
|
|
if (previousTaskId !== null) {
|
|
|
gantt.addLink({
|
|
|
id: `link-${linkIndex++}`,
|
|
|
source: previousTaskId,
|
|
|
target: taskId,
|
|
|
type: '0'
|
|
|
})
|
|
|
}
|
|
|
previousTaskId = taskId
|
|
|
})
|
|
|
} catch {}
|
|
|
})
|
|
|
} catch {}
|
|
|
}
|
|
|
|
|
|
const getDeviceInsertIndex = (deviceTaskId: string | number, startDate: dayjs.Dayjs) => {
|
|
|
const childTaskIds = gantt.getChildren(deviceTaskId)
|
|
|
if (!Array.isArray(childTaskIds) || !childTaskIds.length) return 0
|
|
|
const targetStart = startDate.startOf('day').valueOf()
|
|
|
for (let index = 0; index < childTaskIds.length; index += 1) {
|
|
|
const childTask = gantt.getTask(childTaskIds[index])
|
|
|
if (!childTask?._planData) continue
|
|
|
const childStart = dayjs(childTask.start_date).startOf('day').valueOf()
|
|
|
if (childStart >= targetStart) return index
|
|
|
}
|
|
|
return childTaskIds.length
|
|
|
}
|
|
|
|
|
|
const addDeviceToGantt = (device: any, excludePlanIdentity?: string) => {
|
|
|
const deviceId = `device-${device.deviceId}`
|
|
|
try {
|
|
|
gantt.getTask(deviceId)
|
|
|
return
|
|
|
} catch {}
|
|
|
const plans = (device?.plans ?? []).map((plan: any) => ({
|
|
|
...plan,
|
|
|
_start: dayjs(plan?.planStartTimeStr),
|
|
|
_end: dayjs(plan?.planEndTimeStr)
|
|
|
}))
|
|
|
const validPlans = plans
|
|
|
.filter((plan: any) => plan._start.isValid() && plan._end.isValid())
|
|
|
.sort((a: any, b: any) => a._start.valueOf() - b._start.valueOf())
|
|
|
const firstPlan = validPlans[0]
|
|
|
const deviceRange = getDeviceTaskRangeByChildren(
|
|
|
validPlans.map((item: any) => ({ start_date: item?._start, end_date: item?._end }))
|
|
|
)
|
|
|
gantt.addTask({
|
|
|
id: deviceId,
|
|
|
text: `${device.deviceName ?? '-'}`,
|
|
|
start_date: formatGanttDate(deviceRange?.start_date ?? firstPlan?._start),
|
|
|
end_date: formatGanttDate(deviceRange?.end_date ?? firstPlan?._end),
|
|
|
duration: deviceRange?.duration ?? 1,
|
|
|
parent: 0,
|
|
|
progress: 0,
|
|
|
open: true,
|
|
|
readonly: true,
|
|
|
deviceName: device.deviceName ?? '-',
|
|
|
_deviceData: device
|
|
|
})
|
|
|
validPlans.forEach((plan: any, index: number) => {
|
|
|
const planIdentity = `${plan?.taskId ?? ''}-${plan?.taskDetailId ?? ''}`
|
|
|
if (excludePlanIdentity && planIdentity === excludePlanIdentity) return
|
|
|
const startDate = formatGanttDate(plan.planStartTimeStr)
|
|
|
const endDate = formatGanttDate(plan.planEndTimeStr)
|
|
|
if (!startDate || !endDate) return
|
|
|
const duration = Number(plan.scheduleDays) > 0 ? Number(plan.scheduleDays) : Math.max(plan._end.diff(plan._start, 'day') + 1, 1)
|
|
|
const isHistory = String(plan.sourceType ?? '').toUpperCase() === 'HISTORY'
|
|
|
const planTaskId = `plan-${device.deviceId}-${plan.taskDetailId ?? index}-${index}`
|
|
|
gantt.addTask({
|
|
|
id: planTaskId,
|
|
|
text: `${plan.productCode ?? '-'} / ${plan.productName ?? '-'} / ${plan.taskCode ?? '-'}`,
|
|
|
start_date: startDate,
|
|
|
end_date: endDate,
|
|
|
duration,
|
|
|
parent: deviceId,
|
|
|
progress: 0,
|
|
|
readonly: isHistory,
|
|
|
_planData: plan,
|
|
|
_deviceData: device
|
|
|
})
|
|
|
})
|
|
|
}
|
|
|
|
|
|
const removeDeviceFromGanttIfNoCurrent = (deviceTaskId: string | number) => {
|
|
|
try {
|
|
|
const deviceTask = gantt.getTask(deviceTaskId)
|
|
|
if (!deviceTask || deviceTask?._planData) return
|
|
|
const childTaskIds = gantt.getChildren(deviceTaskId)
|
|
|
const hasCurrent = (Array.isArray(childTaskIds) ? childTaskIds : []).some((childId: string | number) => {
|
|
|
const childTask = gantt.getTask(childId)
|
|
|
return childTask?._planData && String(childTask._planData.sourceType ?? '').toUpperCase() === 'CURRENT'
|
|
|
})
|
|
|
if (!hasCurrent) {
|
|
|
gantt.deleteTask(String(deviceTaskId))
|
|
|
}
|
|
|
} catch {}
|
|
|
}
|
|
|
|
|
|
const applyTaskAdjust = (task: any, targetDeviceTaskId: string | number, startDate: string, endDate: string) => {
|
|
|
const sourceDeviceTaskId = task.parent
|
|
|
const nextStart = dayjs(startDate)
|
|
|
const nextEnd = dayjs(endDate)
|
|
|
const duration = Math.max(nextEnd.diff(nextStart, 'day') + 1, 1)
|
|
|
const planIdentity = `${task?._planData?.taskId ?? ''}-${task?._planData?.taskDetailId ?? ''}`
|
|
|
|
|
|
const targetDeviceData = previewScheduleList.value.find((d: any) => `device-${d.deviceId}` === targetDeviceTaskId)
|
|
|
if (!targetDeviceData) return
|
|
|
|
|
|
const sourceDeviceData = task._deviceData
|
|
|
if (sourceDeviceData?.plans && Array.isArray(sourceDeviceData.plans)) {
|
|
|
sourceDeviceData.plans = sourceDeviceData.plans.filter((item: any) => {
|
|
|
const itemId = `${item?.taskId ?? ''}-${item?.taskDetailId ?? ''}`
|
|
|
return itemId !== planIdentity
|
|
|
})
|
|
|
}
|
|
|
if (!Array.isArray(targetDeviceData.plans)) {
|
|
|
targetDeviceData.plans = []
|
|
|
}
|
|
|
const targetExists = targetDeviceData.plans.some((item: any) => {
|
|
|
const itemId = `${item?.taskId ?? ''}-${item?.taskDetailId ?? ''}`
|
|
|
return itemId === planIdentity
|
|
|
})
|
|
|
if (!targetExists) {
|
|
|
targetDeviceData.plans.push(task._planData)
|
|
|
}
|
|
|
task._planData.deviceId = targetDeviceData.deviceId
|
|
|
task._planData.feedingPipeline = targetDeviceData.deviceId
|
|
|
task._deviceData = targetDeviceData
|
|
|
|
|
|
let targetDeviceInGantt = true
|
|
|
try {
|
|
|
gantt.getTask(targetDeviceTaskId)
|
|
|
} catch {
|
|
|
targetDeviceInGantt = false
|
|
|
}
|
|
|
|
|
|
if (!targetDeviceInGantt) {
|
|
|
addDeviceToGantt(targetDeviceData, planIdentity)
|
|
|
}
|
|
|
|
|
|
task.parent = targetDeviceTaskId
|
|
|
task.start_date = nextStart.toDate()
|
|
|
task.end_date = nextEnd.toDate()
|
|
|
task.duration = duration
|
|
|
const targetIndex = getDeviceInsertIndex(targetDeviceTaskId, nextStart)
|
|
|
gantt.moveTask(task.id, targetIndex, targetDeviceTaskId)
|
|
|
gantt.updateTask(task.id)
|
|
|
syncPlanTimeFromTask(task)
|
|
|
normalizeDeviceChildren(targetDeviceTaskId, task.id)
|
|
|
if (sourceDeviceTaskId && sourceDeviceTaskId !== targetDeviceTaskId) {
|
|
|
removeDeviceFromGanttIfNoCurrent(sourceDeviceTaskId)
|
|
|
normalizeDeviceChildren(sourceDeviceTaskId)
|
|
|
}
|
|
|
refreshPlanLinksByRowOrder()
|
|
|
refreshTimelineRangeByTasks()
|
|
|
gantt.render()
|
|
|
}
|
|
|
|
|
|
const openTaskAdjustDialog = (task: any) => {
|
|
|
if (!task?._planData) return
|
|
|
taskAdjustTaskId.value = task.id
|
|
|
taskAdjustForm.deviceTaskId = String(task.parent ?? '')
|
|
|
taskAdjustForm.startDate = dayjs(task.start_date).format('YYYY-MM-DD HH:mm:ss')
|
|
|
taskAdjustForm.endDate = dayjs(task.end_date).format('YYYY-MM-DD HH:mm:ss')
|
|
|
taskAdjustDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
const handleTaskAdjustSubmit = () => {
|
|
|
if (!taskAdjustTaskId.value) return
|
|
|
if (!taskAdjustForm.deviceTaskId || !taskAdjustForm.startDate || !taskAdjustForm.endDate) {
|
|
|
message.warning('请完善设备、计划开始日期和计划结束日期')
|
|
|
return
|
|
|
}
|
|
|
const task = gantt.getTask(taskAdjustTaskId.value)
|
|
|
if (!task?._planData) return
|
|
|
ganttSyncing.value = true
|
|
|
try {
|
|
|
applyTaskAdjust(task, taskAdjustForm.deviceTaskId, taskAdjustForm.startDate, taskAdjustForm.endDate)
|
|
|
} finally {
|
|
|
ganttSyncing.value = false
|
|
|
}
|
|
|
taskAdjustDialogVisible.value = false
|
|
|
}
|
|
|
|
|
|
const refreshTimelineRangeByTasks = () => {
|
|
|
let minStart = Number.POSITIVE_INFINITY
|
|
|
let maxEnd = Number.NEGATIVE_INFINITY
|
|
|
gantt.eachTask((task: any) => {
|
|
|
if (!task?._planData) return
|
|
|
const start = dayjs(task.start_date).valueOf()
|
|
|
const end = dayjs(task.end_date).valueOf()
|
|
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return
|
|
|
minStart = Math.min(minStart, start)
|
|
|
maxEnd = Math.max(maxEnd, end)
|
|
|
})
|
|
|
if (!Number.isFinite(minStart) || !Number.isFinite(maxEnd)) return
|
|
|
|
|
|
const nextStartDate = dayjs(minStart).startOf('day').toDate()
|
|
|
const nextEndDate = dayjs(maxEnd).endOf('day').toDate()
|
|
|
const currentStart = dayjs(gantt.config.start_date).valueOf()
|
|
|
const currentEnd = dayjs(gantt.config.end_date).valueOf()
|
|
|
const nextStart = dayjs(nextStartDate).valueOf()
|
|
|
const nextEnd = dayjs(nextEndDate).valueOf()
|
|
|
if (currentStart === nextStart && currentEnd === nextEnd) return
|
|
|
|
|
|
gantt.config.start_date = nextStartDate
|
|
|
gantt.config.end_date = nextEndDate
|
|
|
gantt.render()
|
|
|
}
|
|
|
|
|
|
const initGanttPreview = () => {
|
|
|
if (!ganttContainerRef.value) return
|
|
|
destroyGantt()
|
|
|
if (!previewScheduleList.value.length) {
|
|
|
activePreviewDevice.value = undefined
|
|
|
activePreviewTask.value = undefined
|
|
|
return
|
|
|
}
|
|
|
|
|
|
gantt.plugins({ tooltip: true, inline_edit: true,undo: true })
|
|
|
|
|
|
// 编辑相关配置
|
|
|
gantt.config.readonly = false // 是否只读模式
|
|
|
gantt.config.drag_move = true // 是否允许拖动任务
|
|
|
gantt.config.drag_links = false // 是否允许拖动链接
|
|
|
gantt.config.drag_progress = false // 是否允许拖动进度
|
|
|
gantt.config.drag_resize = true // 是否允许调整任务大小
|
|
|
gantt.config.order_branch = true // 是否允许分支内排序
|
|
|
gantt.config.order_branch_free = true // 是否允许跨分支排序
|
|
|
gantt.config.details_on_dblclick = false // 双击是否显示详情
|
|
|
gantt.config.show_progress = false // 是否显示进度条
|
|
|
|
|
|
// 布局相关配置
|
|
|
gantt.config.row_height = 48 // 任务行高度
|
|
|
gantt.config.scale_height = 70 // 时间轴高度
|
|
|
gantt.config.xml_date = '%Y-%m-%d %H:%i' // 日期格式
|
|
|
gantt.config.task_height = 36 // 任务条高度
|
|
|
gantt.config.min_column_width = 80 // 最小列宽
|
|
|
gantt.config.column_width = 90 // 默认列宽
|
|
|
gantt.config.bar_height = 34 // 任务条实际高度
|
|
|
gantt.config.resize_step = 15 // 调整步长
|
|
|
|
|
|
gantt.config.columns = [
|
|
|
{
|
|
|
name: 'text',
|
|
|
label: '任务名称',
|
|
|
tree: true,
|
|
|
width: '*',
|
|
|
min_width: 200
|
|
|
},
|
|
|
{
|
|
|
name: 'start_date',
|
|
|
label: '计划时间',
|
|
|
align: 'center',
|
|
|
width: 210,
|
|
|
template: (task: any) =>
|
|
|
task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
|
|
|
? `<span class="gantt-inline-editor-trigger" data-field="start_date" data-task-id="${task.id}">${formatGridDateText(task.start_date)}</span>`
|
|
|
: formatGridDateText(task.start_date)
|
|
|
}
|
|
|
]
|
|
|
|
|
|
gantt.config.scales = [
|
|
|
{ unit: 'month', step: 1, format: (date) => dayjs(date).format('YYYY年M月') },
|
|
|
{ unit: 'day', step: 1, format: (date) => dayjs(date).format('MM-DD') }
|
|
|
]
|
|
|
|
|
|
gantt.templates.tooltip_text = (start, end, task: any) => buildTaskTooltipHtml(task, start, end)
|
|
|
gantt.templates.task_class = (_start, _end, task: any) => {
|
|
|
if (!task?._planData) return ''
|
|
|
const sourceType = String(task?._planData?.sourceType ?? '').toUpperCase()
|
|
|
return sourceType === 'HISTORY' ? 'schedule-plan-task-history' : 'schedule-plan-task'
|
|
|
}
|
|
|
|
|
|
const ganttScheduleData = getGanttScheduleList()
|
|
|
const globalRange = getGlobalDateRange(ganttScheduleData)
|
|
|
gantt.config.start_date = dayjs(globalRange.start).startOf('day').toDate()
|
|
|
gantt.config.end_date = dayjs(globalRange.end).endOf('day').toDate()
|
|
|
|
|
|
gantt.init(ganttContainerRef.value)
|
|
|
|
|
|
const ganttData = buildPreviewGanttData(ganttScheduleData)
|
|
|
gantt.parse(ganttData)
|
|
|
|
|
|
// 强制刷新所有任务的样式,确保颜色正确应用
|
|
|
gantt.eachTask((task: any) => {
|
|
|
gantt.refreshTask(task.id)
|
|
|
})
|
|
|
|
|
|
initTaskTooltips()
|
|
|
|
|
|
if (ganttData.data.length) {
|
|
|
activePreviewDevice.value = ganttData.data[0]._deviceData
|
|
|
const today = dayjs().startOf('day').toDate()
|
|
|
const pos = gantt.posFromDate(today)
|
|
|
const scrollState = gantt.getScrollState()
|
|
|
if (scrollState && pos >= 0) {
|
|
|
const halfWidth = scrollState.inner_width / 3
|
|
|
gantt.scrollTo(Math.max(0, pos - halfWidth), 0)
|
|
|
} else {
|
|
|
gantt.showDate(today)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const clickEventId = gantt.attachEvent('onTaskClick', (id, event: MouseEvent) => {
|
|
|
const task = gantt.getTask(id)
|
|
|
if (task?._planData) {
|
|
|
activePreviewTask.value = task._planData
|
|
|
activePreviewDevice.value = task._deviceData
|
|
|
} else {
|
|
|
activePreviewTask.value = undefined
|
|
|
activePreviewDevice.value = task?._deviceData
|
|
|
}
|
|
|
const target = event?.target as HTMLElement | null
|
|
|
const editableNode = target?.closest('.gantt-inline-editor-trigger') as HTMLElement | null
|
|
|
const field = editableNode?.dataset?.field
|
|
|
if (!field || !task?._planData || String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return true
|
|
|
if (field === 'start_date') {
|
|
|
openTaskAdjustDialog(task)
|
|
|
return false
|
|
|
}
|
|
|
const inlineEditors = (gantt.ext as any)?.inlineEditors
|
|
|
if (inlineEditors?.startEdit) {
|
|
|
inlineEditors.startEdit(id, field)
|
|
|
return false
|
|
|
}
|
|
|
return true
|
|
|
})
|
|
|
ganttEventIds.value.push(clickEventId)
|
|
|
|
|
|
if (ganttContainerRef.value) {
|
|
|
const gridClickHandler = (e: MouseEvent) => {
|
|
|
const target = e.target as HTMLElement
|
|
|
const trigger = target.closest('.gantt-inline-editor-trigger[data-field="start_date"]') as HTMLElement | null
|
|
|
if (!trigger) return
|
|
|
const taskId = trigger.getAttribute('data-task-id')
|
|
|
if (!taskId) return
|
|
|
try {
|
|
|
const task = gantt.getTask(taskId)
|
|
|
if (!task?._planData || String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return
|
|
|
e.stopPropagation()
|
|
|
openTaskAdjustDialog(task)
|
|
|
} catch {}
|
|
|
}
|
|
|
ganttContainerRef.value.addEventListener('click', gridClickHandler, true)
|
|
|
tooltipCleanupFns.value.push(() => {
|
|
|
ganttContainerRef.value?.removeEventListener('click', gridClickHandler, true)
|
|
|
})
|
|
|
}
|
|
|
|
|
|
const beforeDragEventId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
|
|
|
const task = gantt.getTask(id)
|
|
|
if (!task?._planData) return false
|
|
|
taskDragDurationMap.value[String(id)] = Math.max(Number(task.duration) || 1, 1)
|
|
|
return String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
|
|
|
})
|
|
|
|
|
|
const beforeMoveEventId = gantt.attachEvent('onBeforeTaskMove', (id, parent) => {
|
|
|
const task = gantt.getTask(id)
|
|
|
if (!task?._planData) return false
|
|
|
if (String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return false
|
|
|
const targetTask = gantt.getTask(parent)
|
|
|
if (!targetTask || targetTask?._planData) return false
|
|
|
taskMoveFromParentMap.value[String(id)] = task.parent
|
|
|
return true
|
|
|
})
|
|
|
|
|
|
const afterMoveEventId = gantt.attachEvent('onAfterTaskMove', (id, parent) => {
|
|
|
const task = gantt.getTask(id)
|
|
|
if (!task?._planData) return
|
|
|
const sourceParent = taskMoveFromParentMap.value[String(id)]
|
|
|
delete taskMoveFromParentMap.value[String(id)]
|
|
|
if (!sourceParent && sourceParent !== 0) return
|
|
|
if (sourceParent === parent) {
|
|
|
refreshPlanLinksByRowOrder()
|
|
|
return
|
|
|
}
|
|
|
if (ganttSyncing.value) return
|
|
|
ganttSyncing.value = true
|
|
|
try {
|
|
|
movePlanDataToDevice(task, parent, sourceParent)
|
|
|
syncPlanTimeFromTask(task)
|
|
|
normalizeDeviceChildren(parent)
|
|
|
normalizeDeviceChildren(sourceParent)
|
|
|
removeDeviceFromGanttIfNoCurrent(sourceParent)
|
|
|
refreshPlanLinksByRowOrder()
|
|
|
refreshTimelineRangeByTasks()
|
|
|
} finally {
|
|
|
ganttSyncing.value = false
|
|
|
}
|
|
|
})
|
|
|
|
|
|
const contextMenuEventId = gantt.attachEvent('onContextMenu', (taskId, _linkId, event: MouseEvent) => {
|
|
|
const task = gantt.getTask(taskId)
|
|
|
if (!task?._planData) return true
|
|
|
if (String(task?._planData?.sourceType ?? '').toUpperCase() !== 'CURRENT') return false
|
|
|
event.preventDefault()
|
|
|
openTaskAdjustDialog(task)
|
|
|
return false
|
|
|
})
|
|
|
|
|
|
const dragEventId = gantt.attachEvent('onAfterTaskDrag', (id, mode) => {
|
|
|
if (ganttSyncing.value) return
|
|
|
const task = gantt.getTask(id)
|
|
|
const taskId = String(id)
|
|
|
const originalDuration = taskDragDurationMap.value[taskId]
|
|
|
delete taskDragDurationMap.value[taskId]
|
|
|
ganttSyncing.value = true
|
|
|
try {
|
|
|
if (mode === gantt.config.drag_mode.move && Number(originalDuration) > 0) {
|
|
|
task.duration = originalDuration
|
|
|
}
|
|
|
syncPlanTimeFromTask(task)
|
|
|
normalizeDeviceChildren(task.parent)
|
|
|
refreshPlanLinksByRowOrder()
|
|
|
refreshTimelineRangeByTasks()
|
|
|
} finally {
|
|
|
ganttSyncing.value = false
|
|
|
}
|
|
|
})
|
|
|
|
|
|
const updateEventId = gantt.attachEvent('onAfterTaskUpdate', (id) => {
|
|
|
if (ganttSyncing.value) return
|
|
|
const task = gantt.getTask(id)
|
|
|
ganttSyncing.value = true
|
|
|
try {
|
|
|
syncPlanTimeFromTask(task)
|
|
|
normalizeDeviceChildren(task.parent)
|
|
|
refreshPlanLinksByRowOrder()
|
|
|
refreshTimelineRangeByTasks()
|
|
|
gantt.refreshTask(id)
|
|
|
} finally {
|
|
|
ganttSyncing.value = false
|
|
|
}
|
|
|
})
|
|
|
|
|
|
ganttEventIds.value.push(beforeDragEventId, beforeMoveEventId, afterMoveEventId, contextMenuEventId, dragEventId, updateEventId)
|
|
|
refreshPlanLinksByRowOrder()
|
|
|
}
|
|
|
|
|
|
onMounted(async () => {
|
|
|
await nextTick()
|
|
|
initGanttPreview()
|
|
|
})
|
|
|
|
|
|
watch(
|
|
|
() => props.scheduleList,
|
|
|
async () => {
|
|
|
if (ganttSyncing.value) return
|
|
|
await nextTick()
|
|
|
if (ganttSyncing.value) return
|
|
|
initGanttPreview()
|
|
|
},
|
|
|
{ deep: true }
|
|
|
)
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
destroyGantt()
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.schedule-preview-wrap {
|
|
|
display: flex;
|
|
|
width: 100%;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
border-radius: 4px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.schedule-detail-panel {
|
|
|
width: 280px;
|
|
|
flex: 0 0 280px;
|
|
|
}
|
|
|
|
|
|
.schedule-detail-title {
|
|
|
margin-bottom: 10px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.schedule-plan-list-title {
|
|
|
margin-top: 14px;
|
|
|
margin-bottom: 10px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.schedule-plan-list {
|
|
|
overflow: auto;
|
|
|
max-height: 55vh;
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
border-radius: 4px;
|
|
|
padding: 8px;
|
|
|
}
|
|
|
|
|
|
.schedule-plan-item {
|
|
|
padding: 8px;
|
|
|
border-radius: 4px;
|
|
|
background: var(--el-fill-color-light);
|
|
|
}
|
|
|
.schedule-plan-item-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 8px;
|
|
|
margin-bottom: 4px;
|
|
|
}
|
|
|
.schedule-plan-item-title {
|
|
|
font-weight: 600;
|
|
|
color: var(--el-text-color-primary);
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.schedule-plan-item-active {
|
|
|
border: 1px solid var(--el-color-success-light-5);
|
|
|
background: var(--el-color-success-light-9);
|
|
|
}
|
|
|
|
|
|
.schedule-plan-item + .schedule-plan-item {
|
|
|
margin-top: 8px;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_grid_data .gantt_row) {
|
|
|
font-weight: 600;
|
|
|
background: linear-gradient(180deg, #f8fafc 0%, #f0f4f8 100%);
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
height: 48px;
|
|
|
display: flex;
|
|
|
align-items: flex-end;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_grid_data .gantt_row:nth-child(even)) {
|
|
|
background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_grid_data .gantt_row:hover) {
|
|
|
background: linear-gradient(180deg, #f3f4f6 0%, #e5e7eb 100%) !important;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_grid_data .gantt_cell) {
|
|
|
text-overflow: clip;
|
|
|
font-size: 13px;
|
|
|
color: #4b5563;
|
|
|
border-right: 1px solid #e5e7eb;
|
|
|
padding: 0 12px;
|
|
|
line-height: 36px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: flex-start;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_scale_line) {
|
|
|
border-bottom: 2px solid #d1d5db;
|
|
|
background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
margin-bottom: 2px;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_scale_line:last-child) {
|
|
|
margin-bottom: 0;
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_scale_cell) {
|
|
|
font-size: 14px;
|
|
|
font-weight: 600;
|
|
|
color: #374151;
|
|
|
border-right: 1px solid #e5e7eb;
|
|
|
padding: 0 8px;
|
|
|
text-align: center;
|
|
|
background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
height: 35px;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_scale_line:last-child .gantt_scale_cell) {
|
|
|
font-size: 13px;
|
|
|
font-weight: 500;
|
|
|
color: #6b7280;
|
|
|
background: linear-gradient(180deg, #f9fafb 0%, #f3f4f6 100%);
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
height: 35px;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_bg) {
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_bg .gantt_task_cell) {
|
|
|
border-right: 1px solid #f3f4f6;
|
|
|
border-bottom: 1px solid #f3f4f6;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_bg .gantt_task_row:nth-child(even)) {
|
|
|
background: #f9fafb;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_bg .gantt_task_row:hover) {
|
|
|
background: #f3f4f6 !important;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_line) {
|
|
|
border-radius: 10px;
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
border: none;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_content) {
|
|
|
font-size: 12px;
|
|
|
font-weight: 500;
|
|
|
color: #ffffff;
|
|
|
padding: 0 12px;
|
|
|
line-height: 32px;
|
|
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_inline-editor-trigger) {
|
|
|
display: inline-block;
|
|
|
width: 100%;
|
|
|
overflow: visible;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task) {
|
|
|
background: #67c23a;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task .gantt_task_content) {
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history) {
|
|
|
background: #9ca3af;
|
|
|
opacity: 0.7;
|
|
|
}
|
|
|
|
|
|
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history .gantt_task_content) {
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
|
|
|
:deep(.gantt_tooltip) {
|
|
|
z-index: 5000 !important;
|
|
|
background: rgba(255, 255, 255, 0.95) !important;
|
|
|
border: 1px solid #e5e7eb !important;
|
|
|
border-radius: 8px !important;
|
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
|
|
padding: 12px !important;
|
|
|
font-size: 13px !important;
|
|
|
line-height: 1.5 !important;
|
|
|
}
|
|
|
|
|
|
:deep(.gantt_tooltip) div {
|
|
|
margin-bottom: 6px !important;
|
|
|
}
|
|
|
|
|
|
:deep(.gantt_tooltip) div:last-child {
|
|
|
margin-bottom: 0 !important;
|
|
|
}
|
|
|
|
|
|
:deep(.gantt_tree_content) {
|
|
|
line-height: 50px;
|
|
|
}
|
|
|
</style>
|