You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/mes/tasksummary/components/ScheduleGanttPanelEditable.vue

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>