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.
865 lines
27 KiB
Vue
865 lines
27 KiB
Vue
<template>
|
|
<div class="schedule-preview-wrap">
|
|
<div class="gantt-main-area">
|
|
<div ref="ganttContainerRef" class="schedule-gantt-container" :style="{ height }"></div>
|
|
</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 v-if="activePreviewDevice.capacityType !== undefined" label="产能来源">{{ getCapacityTypeLabel(activePreviewDevice.capacityType) }}</el-descriptions-item>
|
|
<el-descriptions-item v-if="activePreviewDevice.capacityType === 1" 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.planId}-${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>
|
|
<template v-if="activePreviewDevice.capacityType !== 1">
|
|
<div>产能:{{ plan.ratedCapacity ?? '-' }}</div>
|
|
<div>产能来源:{{ getCapacityTypeLabel(plan.capacityType) }}</div>
|
|
</template>
|
|
<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'
|
|
import { getDictOptions } from '@/utils/dict'
|
|
|
|
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 ganttEventIds = ref<string[]>([])
|
|
const ganttSyncing = ref(false)
|
|
const taskAdjustDialogVisible = ref(false)
|
|
const taskAdjustForm = reactive({
|
|
deviceTaskId: '',
|
|
startDate: '',
|
|
endDate: ''
|
|
})
|
|
const editingPlanIdentity = ref<string | null>(null)
|
|
|
|
const getCapacityTypeLabel = (value: any) => {
|
|
if (value === undefined || value === null) return '-'
|
|
const options = getDictOptions('capacity_sources')
|
|
const found = options.find((opt) => Number(opt.value) === Number(value))
|
|
return found?.label ?? String(value)
|
|
}
|
|
|
|
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 buildPreviewGanttData = (scheduleList: any[]) => {
|
|
const tasks: any[] = []
|
|
|
|
scheduleList.forEach((device: any) => {
|
|
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())
|
|
|
|
if (!validPlans.length) return
|
|
|
|
const allStarts = validPlans.map((p: any) => p._start.valueOf())
|
|
const allEnds = validPlans.map((p: any) => p._end.valueOf())
|
|
const earliestStart = dayjs(Math.min(...allStarts))
|
|
const latestEnd = dayjs(Math.max(...allEnds))
|
|
const totalDays = Math.max(latestEnd.endOf('day').diff(earliestStart.startOf('day'), 'day') + 1, 1)
|
|
|
|
tasks.push({
|
|
id: `device-${device.deviceId}`,
|
|
text: device.deviceName ?? '-',
|
|
start_date: formatGanttDate(earliestStart),
|
|
end_date: formatGanttDate(latestEnd),
|
|
duration: totalDays,
|
|
parent: 0,
|
|
progress: 0,
|
|
readonly: true,
|
|
deviceName: device.deviceName ?? '-',
|
|
totalDays,
|
|
_deviceData: device,
|
|
_validPlans: validPlans
|
|
})
|
|
})
|
|
|
|
return { data: tasks, links: [] }
|
|
}
|
|
|
|
const destroyGantt = () => {
|
|
ganttEventIds.value.forEach((eventId) => {
|
|
try { gantt.detachEvent(eventId) } catch {}
|
|
})
|
|
ganttEventIds.value = []
|
|
try { gantt.clearAll() } catch {}
|
|
}
|
|
|
|
const formatTooltipDateTime = (value: unknown) => {
|
|
const date = dayjs(value)
|
|
if (!date.isValid()) return '-'
|
|
return date.format('YYYY-MM-DD HH:mm:ss')
|
|
}
|
|
|
|
const clearCustomPlanBars = () => {
|
|
if (!ganttContainerRef.value) return
|
|
const existing = ganttContainerRef.value.querySelectorAll('.custom-plan-bar')
|
|
existing.forEach((el) => el.remove())
|
|
}
|
|
|
|
const buildPlanBarTooltipHtml = (plan: any, device: any) => {
|
|
return `
|
|
<div><b>任务明细</b></div>
|
|
<div>设备:${device?.deviceName ?? '-'}</div>
|
|
<div>任务单:${plan.taskCode ?? '-'}</div>
|
|
<div>产品:${plan.productCode ?? '-'} / ${plan.productName ?? '-'}</div>
|
|
<div>计划数量:${plan.planNumber ?? '-'}</div>
|
|
<div>开始:${formatTooltipDateTime(plan.planStartTimeStr)}</div>
|
|
<div>结束:${formatTooltipDateTime(plan.planEndTimeStr)}</div>
|
|
<div>最晚开工:${formatTooltipDateTime(plan.latestStartTimeStr)}</div>
|
|
`
|
|
}
|
|
|
|
const makePlanIdentity = (plan: any) =>
|
|
`${plan?.taskId ?? ''}-${plan?.taskDetailId ?? ''}-${plan?.planId ?? ''}`
|
|
|
|
const updatePlanTime = (plan: any, newStart: dayjs.Dayjs, newEnd: dayjs.Dayjs) => {
|
|
plan.planStartTimeStr = newStart.format('YYYY-MM-DD HH:mm:ss')
|
|
plan.planEndTimeStr = newEnd.format('YYYY-MM-DD HH:mm:ss')
|
|
plan.planStartTime = newStart.valueOf()
|
|
plan.planEndTime = newEnd.valueOf()
|
|
plan.scheduleDays = Math.max(newEnd.diff(newStart, 'day') + 1, 1)
|
|
}
|
|
|
|
const movePlanToDevice = (plan: any, sourceDevice: any, targetDevice: any) => {
|
|
if (sourceDevice?.deviceId === targetDevice?.deviceId) return
|
|
const identity = makePlanIdentity(plan)
|
|
if (sourceDevice?.plans && Array.isArray(sourceDevice.plans)) {
|
|
sourceDevice.plans = sourceDevice.plans.filter((item: any) => makePlanIdentity(item) !== identity)
|
|
}
|
|
if (!Array.isArray(targetDevice.plans)) {
|
|
targetDevice.plans = []
|
|
}
|
|
const exists = targetDevice.plans.some((item: any) => makePlanIdentity(item) === identity)
|
|
if (!exists) {
|
|
targetDevice.plans.push(plan)
|
|
}
|
|
plan.deviceId = targetDevice.deviceId
|
|
plan.feedingPipeline = targetDevice.deviceId
|
|
}
|
|
|
|
const renderCustomPlanBars = () => {
|
|
clearCustomPlanBars()
|
|
if (!ganttContainerRef.value) return
|
|
|
|
const taskRows = ganttContainerRef.value.querySelectorAll('.gantt_task_row')
|
|
taskRows.forEach((row: Element) => {
|
|
const taskId = row.getAttribute('task_id')
|
|
if (!taskId) return
|
|
const task = gantt.getTask(taskId)
|
|
if (!task?._validPlans) return
|
|
|
|
const plans = task._validPlans as any[]
|
|
const rowHeight = gantt.config.row_height
|
|
const barHeight = Math.min(gantt.config.bar_height, rowHeight - 8)
|
|
const barTop = (rowHeight - barHeight) / 2
|
|
|
|
plans.forEach((plan: any) => {
|
|
const planStart = dayjs(plan.planStartTimeStr)
|
|
const planEnd = dayjs(plan.planEndTimeStr)
|
|
if (!planStart.isValid() || !planEnd.isValid()) return
|
|
|
|
const leftPos = gantt.posFromDate(planStart.toDate())
|
|
const rightPos = gantt.posFromDate(planEnd.toDate())
|
|
const width = Math.max(rightPos - leftPos, 4)
|
|
const isCurrent = String(plan.sourceType ?? '').toUpperCase() === 'CURRENT'
|
|
|
|
const bar = document.createElement('div')
|
|
bar.className = 'custom-plan-bar'
|
|
if (isCurrent) {
|
|
bar.classList.add('custom-plan-bar-current')
|
|
}
|
|
bar.style.cssText = `
|
|
position: absolute;
|
|
left: ${leftPos}px;
|
|
top: ${barTop}px;
|
|
width: ${width}px;
|
|
height: ${barHeight}px;
|
|
border-radius: 6px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
cursor: ${isCurrent ? 'grab' : 'pointer'};
|
|
z-index: 2;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 8px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: #ffffff;
|
|
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
box-sizing: border-box;
|
|
`
|
|
|
|
if (isCurrent) {
|
|
bar.style.backgroundColor = '#67c23a'
|
|
} else {
|
|
const planStatus = plan.planStatus
|
|
const statusColors: Record<number, string> = {
|
|
1: '#3b82f6',
|
|
8: '#10b981',
|
|
3: '#f59e0b',
|
|
4: '#f56c6c',
|
|
5: '#8e7cc3'
|
|
}
|
|
bar.style.backgroundColor = statusColors[planStatus] || '#6b7280'
|
|
}
|
|
|
|
bar.textContent = `${plan.productCode ?? '-'} / ${plan.productName ?? '-'}`
|
|
|
|
bar.setAttribute('data-plan-id', String(plan.planId ?? ''))
|
|
bar.setAttribute('data-device-id', String(task._deviceData?.deviceId ?? ''))
|
|
|
|
bar.addEventListener('click', (e) => {
|
|
e.stopPropagation()
|
|
activePreviewTask.value = plan
|
|
activePreviewDevice.value = task._deviceData
|
|
})
|
|
|
|
bar.addEventListener('mouseenter', (e) => {
|
|
const tooltipsExt = (gantt.ext as any)?.tooltips
|
|
const tooltip = tooltipsExt?.tooltip
|
|
if (!tooltip) return
|
|
const html = buildPlanBarTooltipHtml(plan, task._deviceData)
|
|
tooltip.setContent(html)
|
|
tooltip.show(e)
|
|
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'
|
|
}
|
|
})
|
|
|
|
bar.addEventListener('mouseleave', () => {
|
|
const tooltipsExt = (gantt.ext as any)?.tooltips
|
|
const tooltip = tooltipsExt?.tooltip
|
|
if (tooltip) { tooltip.hide() }
|
|
})
|
|
|
|
bar.addEventListener('contextmenu', (e) => {
|
|
if (!isCurrent) { e.preventDefault(); return }
|
|
e.preventDefault()
|
|
openTaskAdjustDialogForPlan(plan, task._deviceData)
|
|
})
|
|
|
|
if (isCurrent) {
|
|
attachPlanBarDrag(bar, plan, task)
|
|
attachPlanBarResize(bar, plan, task)
|
|
}
|
|
|
|
row.appendChild(bar)
|
|
})
|
|
})
|
|
}
|
|
|
|
const findOriginalPlan = (planIdentity: string) => {
|
|
for (const device of previewScheduleList.value) {
|
|
const plan = (device?.plans ?? []).find(
|
|
(p: any) => makePlanIdentity(p) === planIdentity
|
|
)
|
|
if (plan) return { plan, device }
|
|
}
|
|
return null
|
|
}
|
|
|
|
const attachPlanBarDrag = (bar: HTMLElement, plan: any, task: any) => {
|
|
let startX = 0
|
|
let startLeft = 0
|
|
let dragging = false
|
|
let moved = false
|
|
|
|
const onMouseDown = (e: MouseEvent) => {
|
|
if (e.button !== 0) return
|
|
const target = e.target as HTMLElement
|
|
if (target.classList.contains('custom-plan-resize-handle')) return
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
startX = e.clientX
|
|
startLeft = bar.offsetLeft
|
|
dragging = true
|
|
moved = false
|
|
bar.style.cursor = 'grabbing'
|
|
bar.style.zIndex = '10'
|
|
bar.style.opacity = '0.85'
|
|
document.addEventListener('mousemove', onMouseMove)
|
|
document.addEventListener('mouseup', onMouseUp)
|
|
}
|
|
|
|
const onMouseMove = (e: MouseEvent) => {
|
|
if (!dragging) return
|
|
const dx = e.clientX - startX
|
|
if (Math.abs(dx) < 3 && !moved) return
|
|
moved = true
|
|
const newLeft = Math.max(0, startLeft + dx)
|
|
bar.style.left = `${newLeft}px`
|
|
}
|
|
|
|
const onMouseUp = (e: MouseEvent) => {
|
|
document.removeEventListener('mousemove', onMouseMove)
|
|
document.removeEventListener('mouseup', onMouseUp)
|
|
dragging = false
|
|
bar.style.cursor = 'grab'
|
|
bar.style.zIndex = '2'
|
|
bar.style.opacity = '1'
|
|
|
|
if (!moved) return
|
|
moved = false
|
|
|
|
const dx = e.clientX - startX
|
|
const newLeft = Math.max(0, startLeft + dx)
|
|
const newStartDate = gantt.dateFromPos(newLeft)
|
|
const planDuration = dayjs(plan.planEndTimeStr).diff(dayjs(plan.planStartTimeStr), 'millisecond')
|
|
const newEndDate = new Date(newStartDate.getTime() + planDuration)
|
|
|
|
const newStart = dayjs(newStartDate)
|
|
const newEnd = dayjs(newEndDate)
|
|
|
|
const identity = makePlanIdentity(plan)
|
|
const original = findOriginalPlan(identity)
|
|
if (original) {
|
|
updatePlanTime(original.plan, newStart, newEnd)
|
|
} else {
|
|
updatePlanTime(plan, newStart, newEnd)
|
|
}
|
|
|
|
const targetDeviceEl = findDeviceRowAtPosition(bar, newLeft)
|
|
if (targetDeviceEl) {
|
|
const targetDeviceId = targetDeviceEl.getAttribute('data-device-id')
|
|
const targetDevice = previewScheduleList.value.find((d: any) => String(d.deviceId) === targetDeviceId)
|
|
if (targetDevice && targetDevice.deviceId !== task._deviceData?.deviceId) {
|
|
const sourceDevice = original?.device ?? task._deviceData
|
|
movePlanToDevice(original?.plan ?? plan, sourceDevice, targetDevice)
|
|
}
|
|
}
|
|
|
|
editingPlanIdentity.value = identity
|
|
initGanttPreview()
|
|
}
|
|
|
|
bar.addEventListener('mousedown', onMouseDown)
|
|
}
|
|
|
|
const findDeviceRowAtPosition = (bar: HTMLElement, left: number) => {
|
|
const barRect = bar.getBoundingClientRect()
|
|
const centerY = barRect.top + barRect.height / 2
|
|
const centerX = barRect.left + left + barRect.width / 2
|
|
const elAtCenter = document.elementFromPoint(centerX, centerY)
|
|
if (!elAtCenter) return null
|
|
const row = elAtCenter.closest('.gantt_task_row') as HTMLElement
|
|
if (!row) return null
|
|
const taskId = row.getAttribute('task_id')
|
|
if (!taskId) return null
|
|
const ganttTask = gantt.getTask(taskId)
|
|
if (!ganttTask?._deviceData) return null
|
|
return { getAttribute: (attr: string) => String(ganttTask._deviceData.deviceId ?? '') }
|
|
}
|
|
|
|
const attachPlanBarResize = (bar: HTMLElement, plan: any, task: any) => {
|
|
const leftHandle = document.createElement('div')
|
|
leftHandle.className = 'custom-plan-resize-handle custom-plan-resize-left'
|
|
leftHandle.style.cssText = `
|
|
position: absolute; left: 0; top: 0; width: 8px; height: 100%;
|
|
cursor: w-resize; z-index: 3;
|
|
`
|
|
const rightHandle = document.createElement('div')
|
|
rightHandle.className = 'custom-plan-resize-handle custom-plan-resize-right'
|
|
rightHandle.style.cssText = `
|
|
position: absolute; right: 0; top: 0; width: 8px; height: 100%;
|
|
cursor: e-resize; z-index: 3;
|
|
`
|
|
bar.appendChild(leftHandle)
|
|
bar.appendChild(rightHandle)
|
|
|
|
const attachResize = (handle: HTMLElement, isLeft: boolean) => {
|
|
let startX = 0
|
|
let startLeft = 0
|
|
let startWidth = 0
|
|
let resizing = false
|
|
|
|
const onMouseDown = (e: MouseEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
startX = e.clientX
|
|
startLeft = bar.offsetLeft
|
|
startWidth = bar.offsetWidth
|
|
resizing = true
|
|
bar.style.zIndex = '10'
|
|
document.addEventListener('mousemove', onMouseMove)
|
|
document.addEventListener('mouseup', onMouseUp)
|
|
}
|
|
|
|
const onMouseMove = (e: MouseEvent) => {
|
|
if (!resizing) return
|
|
const dx = e.clientX - startX
|
|
if (isLeft) {
|
|
const newLeft = Math.max(0, startLeft + dx)
|
|
const newWidth = Math.max(20, startWidth - dx)
|
|
bar.style.left = `${newLeft}px`
|
|
bar.style.width = `${newWidth}px`
|
|
} else {
|
|
const newWidth = Math.max(20, startWidth + dx)
|
|
bar.style.width = `${newWidth}px`
|
|
}
|
|
}
|
|
|
|
const onMouseUp = () => {
|
|
document.removeEventListener('mousemove', onMouseMove)
|
|
document.removeEventListener('mouseup', onMouseUp)
|
|
if (!resizing) return
|
|
resizing = false
|
|
bar.style.zIndex = '2'
|
|
|
|
const newLeft = parseFloat(bar.style.left) || startLeft
|
|
const newWidth = parseFloat(bar.style.width) || startWidth
|
|
const newStartDate = gantt.dateFromPos(newLeft)
|
|
const newEndDate = gantt.dateFromPos(newLeft + newWidth)
|
|
|
|
const identity = makePlanIdentity(plan)
|
|
const original = findOriginalPlan(identity)
|
|
if (original) {
|
|
updatePlanTime(original.plan, dayjs(newStartDate), dayjs(newEndDate))
|
|
} else {
|
|
updatePlanTime(plan, dayjs(newStartDate), dayjs(newEndDate))
|
|
}
|
|
editingPlanIdentity.value = identity
|
|
initGanttPreview()
|
|
}
|
|
|
|
handle.addEventListener('mousedown', onMouseDown)
|
|
}
|
|
|
|
attachResize(leftHandle, true)
|
|
attachResize(rightHandle, false)
|
|
}
|
|
|
|
const openTaskAdjustDialogForPlan = (plan: any, device: any) => {
|
|
taskAdjustForm.deviceTaskId = `device-${device?.deviceId ?? ''}`
|
|
taskAdjustForm.startDate = plan.planStartTimeStr || ''
|
|
taskAdjustForm.endDate = plan.planEndTimeStr || ''
|
|
editingPlanIdentity.value = makePlanIdentity(plan)
|
|
taskAdjustDialogVisible.value = true
|
|
}
|
|
|
|
const handleTaskAdjustSubmit = () => {
|
|
if (!taskAdjustForm.deviceTaskId || !taskAdjustForm.startDate || !taskAdjustForm.endDate) {
|
|
message.warning('请完善设备、计划开始日期和计划结束日期')
|
|
return
|
|
}
|
|
if (!editingPlanIdentity.value) return
|
|
|
|
const targetDevice = previewScheduleList.value.find(
|
|
(d: any) => `device-${d.deviceId}` === taskAdjustForm.deviceTaskId
|
|
)
|
|
if (!targetDevice) return
|
|
|
|
let foundPlan: any = null
|
|
let sourceDevice: any = null
|
|
for (const device of previewScheduleList.value) {
|
|
const plan = (device?.plans ?? []).find(
|
|
(p: any) => makePlanIdentity(p) === editingPlanIdentity.value
|
|
)
|
|
if (plan) {
|
|
foundPlan = plan
|
|
sourceDevice = device
|
|
break
|
|
}
|
|
}
|
|
if (!foundPlan) return
|
|
|
|
const newStart = dayjs(taskAdjustForm.startDate)
|
|
const newEnd = dayjs(taskAdjustForm.endDate)
|
|
if (!newStart.isValid() || !newEnd.isValid()) {
|
|
message.warning('请选择有效的时间')
|
|
return
|
|
}
|
|
if (newEnd.isBefore(newStart)) {
|
|
message.warning('结束时间不能早于开始时间')
|
|
return
|
|
}
|
|
|
|
updatePlanTime(foundPlan, newStart, newEnd)
|
|
if (sourceDevice?.deviceId !== targetDevice.deviceId) {
|
|
movePlanToDevice(foundPlan, sourceDevice, targetDevice)
|
|
}
|
|
|
|
taskAdjustDialogVisible.value = false
|
|
editingPlanIdentity.value = makePlanIdentity(foundPlan)
|
|
initGanttPreview()
|
|
}
|
|
|
|
const initGanttPreview = () => {
|
|
if (!ganttContainerRef.value) return
|
|
destroyGantt()
|
|
clearCustomPlanBars()
|
|
|
|
const ganttScheduleData = getGanttScheduleList()
|
|
if (!ganttScheduleData.length) {
|
|
activePreviewDevice.value = undefined
|
|
activePreviewTask.value = undefined
|
|
return
|
|
}
|
|
|
|
gantt.plugins({ tooltip: true })
|
|
|
|
gantt.config.tooltip_timeout = 0
|
|
gantt.templates.tooltip_text = () => ''
|
|
|
|
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 = 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: false,
|
|
width: '*',
|
|
min_width: 100,
|
|
template: (task: any) => task.deviceName ?? task.text ?? '-'
|
|
},
|
|
{
|
|
name: 'duration',
|
|
label: '天数',
|
|
align: 'center',
|
|
width: 80,
|
|
template: (task: any) => String(task.totalDays ?? task.duration ?? 0)
|
|
}
|
|
]
|
|
|
|
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.task_class = (_start, _end, task: any) => {
|
|
if (task._validPlans) return 'schedule-device-row'
|
|
return ''
|
|
}
|
|
|
|
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)
|
|
|
|
if (ganttData.data.length) {
|
|
const restoreDevice = editingPlanIdentity.value
|
|
? ganttScheduleData.find((d: any) =>
|
|
(d?.plans ?? []).some((p: any) => makePlanIdentity(p) === editingPlanIdentity.value)
|
|
)
|
|
: null
|
|
activePreviewDevice.value = restoreDevice || 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) => {
|
|
const ganttTask = gantt.getTask(id)
|
|
if (ganttTask?._deviceData) {
|
|
activePreviewTask.value = undefined
|
|
activePreviewDevice.value = ganttTask._deviceData
|
|
}
|
|
nextTick(() => {
|
|
renderCustomPlanBars()
|
|
})
|
|
return true
|
|
})
|
|
ganttEventIds.value.push(clickEventId)
|
|
|
|
nextTick(() => {
|
|
renderCustomPlanBars()
|
|
})
|
|
|
|
const renderEventId = gantt.attachEvent('onGanttRender', () => {
|
|
nextTick(() => {
|
|
renderCustomPlanBars()
|
|
})
|
|
})
|
|
ganttEventIds.value.push(renderEventId)
|
|
}
|
|
|
|
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%;
|
|
height: 100%;
|
|
gap: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.gantt-main-area {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.schedule-gantt-container {
|
|
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_task_line.schedule-device-row) {
|
|
visibility: hidden;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.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: 4px;
|
|
}
|
|
|
|
:deep(.gantt_tooltip) div:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
:deep(.gantt_tooltip) b {
|
|
font-size: 14px;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
</style>
|