feat:甘特图弹框抽离组件

pull/1/head
黄伟杰 1 month ago
parent f7a7f529d9
commit a124030f49

@ -8,8 +8,9 @@ VITE_DEV=true
# 线上环境
# VITE_BASE_URL='https://besure.ngsk.tech:7001'
# 本地联调
# VITE_BASE_URL='http://192.168.5.107:48081'
VITE_BASE_URL='http://localhost:48081'
VITE_BASE_URL='http://192.168.5.167:48081'
# VITE_BASE_URL='http://192.168.5.164:48081'
# VITE_BASE_URL='http://192.168.5.5:48081'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务

@ -159,40 +159,11 @@
</template>
</Dialog>
<Dialog v-model="previewVisible" title="排产甘特图预览" width="100%" align-center>
<div class="schedule-preview-wrap">
<div id="gantt_here" ref="ganttContainerRef" class="schedule-gantt-container"></div>
<div class="schedule-detail-panel">
<div class="schedule-detail-title">计划信息</div>
<el-descriptions :column="1" border size="small" v-if="activePreviewDevice">
<el-descriptions-item label="设备名称">{{ activePreviewDevice.deviceName }}</el-descriptions-item>
<el-descriptions-item label="设备ID">{{ activePreviewDevice.deviceId }}</el-descriptions-item>
<el-descriptions-item label="产能">{{ activePreviewDevice.ratedCapacity ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="计划条数">{{ activePreviewDevice.plans?.length ?? 0 }}</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无计划信息" :image-size="80" />
<div class="schedule-plan-list-title">计划明细</div>
<div class="schedule-plan-list">
<div
v-for="(plan, index) in activePreviewDevice?.plans ?? []"
:key="`${activePreviewDevice?.deviceId}-${plan.taskDetailId}-${index}`"
class="schedule-plan-item"
>
<div>任务明细ID{{ plan.taskDetailId ?? '-' }}</div>
<div>计划数量{{ plan.planNumber ?? '-' }}</div>
<div>交货日期{{ plan.deliveryDateStr ?? '-' }}</div>
<div>开始{{ plan.planStartTimeStr || '-' }}</div>
<div>结束{{ plan.planEndTimeStr || '-' }}</div>
<div>最晚开工{{ plan.latestStartTimeStr || '-' }}</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button type="primary" :loading="previewSaveLoading" @click="handlePreviewSave"></el-button>
<el-button @click="previewVisible = false">关闭</el-button>
</template>
</Dialog>
<TaskSchedulePreviewDialog
v-model="previewVisible"
:schedule-list="previewScheduleList"
@saved="handlePreviewSaved"
/>
<ItemNeedIndex ref="itemNeedRef" />
</template>
@ -200,12 +171,9 @@
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { TaskApi } from '@/api/mes/task'
import { PlanApi } from '@/api/mes/plan'
import ItemNeedIndex from '@/views/mes/bom/ItemNeedIndex.vue'
import { dateFormatter2 } from '@/utils/formatTime'
import dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import TaskSchedulePreviewDialog from './TaskSchedulePreviewDialog.vue'
defineOptions({ name: 'TaskScheduleDialog' })
@ -217,18 +185,15 @@ const dialogVisible = ref(false)
const taskLoading = ref(false)
const detailLoading = ref(false)
const submitLoading = ref(false)
const previewSaveLoading = ref(false)
const previewVisible = ref(false)
const itemNeedRef = ref()
const taskTableRef = ref()
const detailTableRef = ref()
const ganttContainerRef = ref<HTMLDivElement>()
const previewScheduleList = ref<any[]>([])
const activePreviewDevice = ref<any>()
const ganttEventIds = ref<string[]>([])
const scheduleRuleOptions = [
{ label: '订单优先级', value: 2 },
{ label: '产品类别顺序', value: 3 },
{ label: '订单交期优先', value: 4 }
]
const capacityTypeOptions = [
@ -482,315 +447,10 @@ const openDetailCreatePlan = (_row: any) => {
message.info('请在任务单汇总明细列表中使用“新增计划”功能')
}
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[] = []
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 lastPlan = validPlans[validPlans.length - 1]
const parentDuration =
firstPlan && lastPlan ? Math.max(lastPlan._end.endOf('day').diff(firstPlan._start.startOf('day'), 'day') + 1, 1) : 1
tasks.push({
id: deviceId,
text: `${device.deviceName ?? '-'}`,
start_date: formatGanttDate(firstPlan?._start),
end_date: formatGanttDate(lastPlan?._end),
duration: parentDuration,
parent: 0,
progress: 0,
open: true,
deviceName: device.deviceName ?? '-',
_deviceData: device
})
let previousPlanTaskId: string | null = null
validPlans.forEach((plan: any, index: number) => {
const startDate = formatGanttDate(plan.planStartTimeStr)
const endDate = formatGanttDate(plan.planEndTimeStr)
const duration = Number(plan.scheduleDays) > 0 ? Number(plan.scheduleDays) : Math.max(plan._end.diff(plan._start, 'day') + 1, 1)
if (!startDate || !endDate) return
const isHistory = String(plan.sourceType ?? '').toUpperCase() === 'HISTORY'
const planTaskId = `plan-${device.deviceId}-${plan.taskDetailId ?? index}-${index}`
tasks.push({
id: planTaskId,
text: `${plan.productCode ?? '-'} / ${plan.productName ?? '-'} / ${plan.taskCode ?? '-'}`,
start_date: startDate,
end_date: endDate,
duration,
parent: deviceId,
progress: 0,
readonly: isHistory,
_planData: plan,
_deviceData: device
})
if (previousPlanTaskId) {
links.push({
id: `link-${linkIndex++}`,
source: previousPlanTaskId,
target: planTaskId,
type: '0'
})
}
previousPlanTaskId = planTaskId
})
})
return { data: tasks, links }
}
const destroyGantt = () => {
ganttEventIds.value.forEach((eventId) => gantt.detachEvent(eventId))
ganttEventIds.value = []
gantt.clearAll()
}
const formatGridDateText = (value: unknown) => {
const date = dayjs(value)
if (!date.isValid()) return '-'
return date.format('YYYY-MM-DD HH:mm:ss')
}
const 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
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 = Math.max(end.endOf('day').diff(start.startOf('day'), 'day') + 1, 1)
activePreviewDevice.value = task._deviceData
}
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').subtract(1, 'day').toDate()
const nextEndDate = dayjs(maxEnd).endOf('day').add(1, 'day').toDate()
const currentStart = dayjs(gantt.config.start_date).valueOf()
const currentEnd = dayjs(gantt.config.end_date).valueOf()
const nextStart = dayjs(nextStartDate).valueOf()
const nextEnd = dayjs(nextEndDate).valueOf()
if (currentStart === nextStart && currentEnd === nextEnd) return
gantt.config.start_date = nextStartDate
gantt.config.end_date = nextEndDate
gantt.render()
}
const initGanttPreview = () => {
if (!ganttContainerRef.value || !previewScheduleList.value.length) return
destroyGantt()
gantt.plugins({ tooltip: true }) //
gantt.config.readonly = false // false
gantt.config.drag_move = true //
gantt.config.drag_links = false // 线
gantt.config.drag_progress = false //
gantt.config.drag_resize = true //
gantt.config.details_on_dblclick = false //
gantt.config.show_progress = false //
gantt.config.row_height = 40 //
gantt.config.scale_height = 44 //
gantt.config.xml_date = '%Y-%m-%d %H:%i' // parse start_date
gantt.config.task_height = 24 //
gantt.config.columns = [
{
name: 'text',
label: '任务名称',
tree: true,
width: '*',
min_width: 200
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 210,
template: (task: any) =>
task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
? `<span class="gantt-inline-editor-trigger" data-field="start_date">${formatGridDateText(task.start_date)}</span>`
: formatGridDateText(task.start_date),
editor: {
type: 'date',
map_to: 'start_date'
}
},
{
name: 'duration',
label: '天数',
align: 'center',
width: 60,
template: (task: any) =>
task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
? `<span class="gantt-inline-editor-trigger" data-field="duration">${task.duration ?? 0}</span>`
: String(task.duration ?? 0),
editor: {
type: 'number',
map_to: 'duration',
min: 1
}
}
]
gantt.config.scales = [
{ unit: 'month', step: 1, format: (date) => dayjs(date).format('YYYY年M月') },
{ unit: 'day', step: 1, format: (date) => dayjs(date).format('MM-DD') }
]
gantt.templates.tooltip_text = (start, end, task: any) => {
const plan = task._planData
if (plan) {
return `
<div>产品名称${plan.productName ?? '-'}</div>
<div>计划数量${plan.planNumber ?? '-'}</div>
<div>开始${dayjs(start).format('YYYY-MM-DD HH:mm:ss')}</div>
<div>结束${dayjs(end).format('YYYY-MM-DD HH:mm:ss')}</div>
`
}
const device = task._deviceData
const plans = device?.plans ?? []
return `
<div>设备${device?.deviceName ?? '-'}</div>
<div>计划条数${plans.length}</div>
`
}
gantt.templates.task_class = (_start, _end, task: any) => {
if (!task?._planData) return ''
return String(task?._planData?.sourceType ?? '').toUpperCase() === 'HISTORY' ? 'schedule-plan-task-history' : 'schedule-plan-task'
}
const globalRange = getGlobalDateRange(previewScheduleList.value)
gantt.config.start_date = dayjs(globalRange.start).startOf('day').toDate()
gantt.config.end_date = dayjs(globalRange.end).endOf('day').toDate()
gantt.init('gantt_here')
const ganttData = buildPreviewGanttData(previewScheduleList.value)
gantt.parse(ganttData)
if (ganttData.data.length) {
activePreviewDevice.value = ganttData.data[0]._deviceData
gantt.showDate(gantt.config.start_date)
}
const clickEventId = gantt.attachEvent('onTaskClick', (id, event: MouseEvent) => {
const task = gantt.getTask(id)
activePreviewDevice.value = task?._deviceData
const target = event?.target as HTMLElement | null
const editableNode = target?.closest('.gantt-inline-editor-trigger') as HTMLElement | null
const field = editableNode?.dataset?.field
const inlineEditors = (gantt.ext as any)?.inlineEditors
if (field && task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT' && inlineEditors?.startEdit) {
inlineEditors.startEdit(id, field)
return false
}
return true
})
const dragEventId = gantt.attachEvent('onAfterTaskDrag', (id) => {
syncPlanTimeFromTask(gantt.getTask(id))
refreshTimelineRangeByTasks()
})
const updateEventId = gantt.attachEvent('onAfterTaskUpdate', (id) => {
syncPlanTimeFromTask(gantt.getTask(id))
refreshTimelineRangeByTasks()
gantt.refreshTask(id)
})
ganttEventIds.value.push(clickEventId, dragEventId, updateEventId)
}
const handlePreviewSave = async () => {
const createReqVOList = previewScheduleList.value.flatMap((device: any) => {
const plans = Array.isArray(device?.plans) ? device.plans : []
return plans
.filter((plan: any) => String(plan?.sourceType ?? '').toUpperCase() === 'CURRENT')
.map((plan: any) => {
const startValue = dayjs(plan?.planStartTimeStr)
const endValue = dayjs(plan?.planEndTimeStr)
const deliveryDateValue = dayjs(plan?.deliveryDateStr)
const latestStartValue = dayjs(plan?.latestStartTimeStr)
return {
productId: plan?.productId,
taskId: plan?.taskId,
taskDetailId: plan?.taskDetailId,
planNumber: Number(plan?.planNumber ?? 0),
finishNumber: Number(plan?.finishNumber ?? 0),
isCode: typeof plan?.isCode === 'boolean' ? plan.isCode : true,
isPreProduction: Number(plan?.isPreProduction ?? 0),
planStartTime: startValue.isValid() ? startValue.valueOf() : undefined,
planEndTime: endValue.isValid() ? endValue.valueOf() : undefined,
reyaNumber: Number(plan?.reyaNumber ?? plan?.planNumber ?? 0),
workerId: plan?.workerId,
feedingPipeline: plan?.feedingPipeline ?? device?.deviceId,
deviceId: device?.deviceId ?? plan?.deviceId,
deliveryDate: deliveryDateValue.isValid() ? deliveryDateValue.valueOf() : undefined,
LatestStartTime: latestStartValue.isValid() ? latestStartValue.valueOf() : undefined,
}
})
})
if (!createReqVOList.length) {
message.warning('暂无可保存的计划数据')
return
}
previewSaveLoading.value = true
try {
await PlanApi.createBatch({ createReqVOList })
message.success('排产计划保存成功')
previewVisible.value = false
dialogVisible.value = false
await loadTaskList()
emit('success')
} finally {
previewSaveLoading.value = false
}
const handlePreviewSaved = async () => {
dialogVisible.value = false
await loadTaskList()
emit('success')
}
const handleSubmit = async () => {
@ -845,7 +505,6 @@ const handleSubmit = async () => {
? (scheduleResult as any).data
: []
previewScheduleList.value = scheduleData
activePreviewDevice.value = previewScheduleList.value[0]
message.success('排产已提交')
previewVisible.value = true
emit('success')
@ -859,102 +518,5 @@ const open = async () => {
await loadTaskList()
}
watch(previewVisible, async (visible) => {
if (visible) {
await nextTick()
initGanttPreview()
return
}
destroyGantt()
})
onBeforeUnmount(() => {
destroyGantt()
})
defineExpose({ open })
</script>
<style scoped>
.schedule-preview-wrap {
display: flex;
width: 100%;
gap: 12px;
}
.schedule-gantt-container {
flex: 1;
min-width: 0;
height: 800px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
}
.schedule-detail-panel {
width: 280px;
flex: 0 0 280px;
}
.schedule-detail-title {
margin-bottom: 10px;
font-weight: 600;
}
.schedule-plan-list-title {
margin-top: 14px;
margin-bottom: 10px;
font-weight: 600;
}
.schedule-plan-list {
/* max-height: 480px; */
overflow: auto;
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 + .schedule-plan-item {
margin-top: 8px;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_row) {
font-weight: 600;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_cell) {
text-overflow: clip;
}
.schedule-gantt-container :deep(.gantt-inline-editor-trigger) {
display: inline-block;
width: 100%;
overflow: visible;
cursor: pointer;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task) {
background: #67c23a;
border-color: #67c23a;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task .gantt_task_content) {
color: #ffffff;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history) {
background: #909399;
border-color: #909399;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history .gantt_task_content) {
color: #ffffff;
}
</style>

@ -0,0 +1,491 @@
<template>
<Dialog v-model="previewVisible" title="排产甘特图预览" width="100%" align-center>
<div class="schedule-preview-wrap">
<div ref="ganttContainerRef" class="schedule-gantt-container"></div>
<div class="schedule-detail-panel">
<div class="schedule-detail-title">计划信息</div>
<el-descriptions :column="1" border size="small" v-if="activePreviewDevice">
<el-descriptions-item label="设备名称">{{ activePreviewDevice.deviceName }}</el-descriptions-item>
<el-descriptions-item label="设备ID">{{ activePreviewDevice.deviceId }}</el-descriptions-item>
<el-descriptions-item label="产能">{{ activePreviewDevice.ratedCapacity ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="计划条数">{{ activePreviewDevice.plans?.length ?? 0 }}</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无计划信息" :image-size="80" />
<div class="schedule-plan-list-title">计划明细</div>
<div class="schedule-plan-list">
<div
v-for="(plan, index) in activePreviewDevice?.plans ?? []"
:key="`${activePreviewDevice?.deviceId}-${plan.taskDetailId}-${index}`"
class="schedule-plan-item"
>
<div>任务明细ID{{ plan.taskDetailId ?? '-' }}</div>
<div>计划数量{{ plan.planNumber ?? '-' }}</div>
<div>交货日期{{ plan.deliveryDateStr ?? '-' }}</div>
<div>开始{{ plan.planStartTimeStr || '-' }}</div>
<div>结束{{ plan.planEndTimeStr || '-' }}</div>
<div>最晚开工{{ plan.latestStartTimeStr || '-' }}</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button type="primary" :loading="previewSaveLoading" @click="handlePreviewSave"></el-button>
<el-button @click="previewVisible = false">关闭</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { PlanApi } from '@/api/mes/plan'
import dayjs from 'dayjs'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
defineOptions({ name: 'TaskSchedulePreviewDialog' })
const props = defineProps<{
modelValue: boolean
scheduleList: any[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'saved'): void
}>()
const message = useMessage()
const previewSaveLoading = ref(false)
const ganttContainerRef = ref<HTMLDivElement>()
const activePreviewDevice = ref<any>()
const ganttEventIds = ref<string[]>([])
const previewVisible = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value)
})
const previewScheduleList = computed(() => (Array.isArray(props.scheduleList) ? props.scheduleList : []))
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[] = []
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 lastPlan = validPlans[validPlans.length - 1]
const parentDuration =
firstPlan && lastPlan ? Math.max(lastPlan._end.endOf('day').diff(firstPlan._start.startOf('day'), 'day') + 1, 1) : 1
tasks.push({
id: deviceId,
text: `${device.deviceName ?? '-'}`,
start_date: formatGanttDate(firstPlan?._start),
end_date: formatGanttDate(lastPlan?._end),
duration: parentDuration,
parent: 0,
progress: 0,
open: true,
deviceName: device.deviceName ?? '-',
_deviceData: device
})
let previousPlanTaskId: string | null = null
validPlans.forEach((plan: any, index: number) => {
const startDate = formatGanttDate(plan.planStartTimeStr)
const endDate = formatGanttDate(plan.planEndTimeStr)
const duration = Number(plan.scheduleDays) > 0 ? Number(plan.scheduleDays) : Math.max(plan._end.diff(plan._start, 'day') + 1, 1)
if (!startDate || !endDate) return
const isHistory = String(plan.sourceType ?? '').toUpperCase() === 'HISTORY'
const planTaskId = `plan-${device.deviceId}-${plan.taskDetailId ?? index}-${index}`
tasks.push({
id: planTaskId,
text: `${plan.productCode ?? '-'} / ${plan.productName ?? '-'} / ${plan.taskCode ?? '-'}`,
start_date: startDate,
end_date: endDate,
duration,
parent: deviceId,
progress: 0,
readonly: isHistory,
_planData: plan,
_deviceData: device
})
if (previousPlanTaskId) {
links.push({
id: `link-${linkIndex++}`,
source: previousPlanTaskId,
target: planTaskId,
type: '0'
})
}
previousPlanTaskId = planTaskId
})
})
return { data: tasks, links }
}
const destroyGantt = () => {
ganttEventIds.value.forEach((eventId) => gantt.detachEvent(eventId))
ganttEventIds.value = []
gantt.clearAll()
}
const formatGridDateText = (value: unknown) => {
const date = dayjs(value)
if (!date.isValid()) return '-'
return date.format('YYYY-MM-DD HH:mm:ss')
}
const 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
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 = Math.max(end.endOf('day').diff(start.startOf('day'), 'day') + 1, 1)
activePreviewDevice.value = task._deviceData
}
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').subtract(1, 'day').toDate()
const nextEndDate = dayjs(maxEnd).endOf('day').add(1, 'day').toDate()
const currentStart = dayjs(gantt.config.start_date).valueOf()
const currentEnd = dayjs(gantt.config.end_date).valueOf()
const nextStart = dayjs(nextStartDate).valueOf()
const nextEnd = dayjs(nextEndDate).valueOf()
if (currentStart === nextStart && currentEnd === nextEnd) return
gantt.config.start_date = nextStartDate
gantt.config.end_date = nextEndDate
gantt.render()
}
const initGanttPreview = () => {
if (!ganttContainerRef.value || !previewScheduleList.value.length) return
destroyGantt()
gantt.plugins({ tooltip: true })
gantt.config.readonly = false
gantt.config.drag_move = true
gantt.config.drag_links = false
gantt.config.drag_progress = false
gantt.config.drag_resize = true
gantt.config.details_on_dblclick = false
gantt.config.show_progress = false
gantt.config.row_height = 40
gantt.config.scale_height = 44
gantt.config.xml_date = '%Y-%m-%d %H:%i'
gantt.config.task_height = 24
gantt.config.columns = [
{
name: 'text',
label: '任务名称',
tree: true,
width: '*',
min_width: 200
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 210,
template: (task: any) =>
task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
? `<span class="gantt-inline-editor-trigger" data-field="start_date">${formatGridDateText(task.start_date)}</span>`
: formatGridDateText(task.start_date),
editor: {
type: 'date',
map_to: 'start_date'
}
},
{
name: 'duration',
label: '天数',
align: 'center',
width: 60,
template: (task: any) =>
task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT'
? `<span class="gantt-inline-editor-trigger" data-field="duration">${task.duration ?? 0}</span>`
: String(task.duration ?? 0),
editor: {
type: 'number',
map_to: 'duration',
min: 1
}
}
]
gantt.config.scales = [
{ unit: 'month', step: 1, format: (date) => dayjs(date).format('YYYY年M月') },
{ unit: 'day', step: 1, format: (date) => dayjs(date).format('MM-DD') }
]
gantt.templates.tooltip_text = (start, end, task: any) => {
const plan = task._planData
if (plan) {
return `
<div>产品名称${plan.productName ?? '-'}</div>
<div>计划数量${plan.planNumber ?? '-'}</div>
<div>开始${dayjs(start).format('YYYY-MM-DD HH:mm:ss')}</div>
<div>结束${dayjs(end).format('YYYY-MM-DD HH:mm:ss')}</div>
`
}
const device = task._deviceData
const plans = device?.plans ?? []
return `
<div>设备${device?.deviceName ?? '-'}</div>
<div>计划条数${plans.length}</div>
`
}
gantt.templates.task_class = (_start, _end, task: any) => {
if (!task?._planData) return ''
return String(task?._planData?.sourceType ?? '').toUpperCase() === 'HISTORY' ? 'schedule-plan-task-history' : 'schedule-plan-task'
}
const globalRange = getGlobalDateRange(previewScheduleList.value)
gantt.config.start_date = dayjs(globalRange.start).startOf('day').toDate()
gantt.config.end_date = dayjs(globalRange.end).endOf('day').toDate()
gantt.init(ganttContainerRef.value)
const ganttData = buildPreviewGanttData(previewScheduleList.value)
gantt.parse(ganttData)
if (ganttData.data.length) {
activePreviewDevice.value = ganttData.data[0]._deviceData
gantt.showDate(gantt.config.start_date)
}
const clickEventId = gantt.attachEvent('onTaskClick', (id, event: MouseEvent) => {
const task = gantt.getTask(id)
activePreviewDevice.value = task?._deviceData
const target = event?.target as HTMLElement | null
const editableNode = target?.closest('.gantt-inline-editor-trigger') as HTMLElement | null
const field = editableNode?.dataset?.field
const inlineEditors = (gantt.ext as any)?.inlineEditors
if (field && task?._planData && String(task?._planData?.sourceType ?? '').toUpperCase() === 'CURRENT' && inlineEditors?.startEdit) {
inlineEditors.startEdit(id, field)
return false
}
return true
})
const dragEventId = gantt.attachEvent('onAfterTaskDrag', (id) => {
syncPlanTimeFromTask(gantt.getTask(id))
refreshTimelineRangeByTasks()
})
const updateEventId = gantt.attachEvent('onAfterTaskUpdate', (id) => {
syncPlanTimeFromTask(gantt.getTask(id))
refreshTimelineRangeByTasks()
gantt.refreshTask(id)
})
ganttEventIds.value.push(clickEventId, dragEventId, updateEventId)
}
const handlePreviewSave = async () => {
const createReqVOList = previewScheduleList.value.flatMap((device: any) => {
const plans = Array.isArray(device?.plans) ? device.plans : []
return plans
.filter((plan: any) => String(plan?.sourceType ?? '').toUpperCase() === 'CURRENT')
.map((plan: any) => {
const startValue = dayjs(plan?.planStartTimeStr)
const endValue = dayjs(plan?.planEndTimeStr)
const deliveryDateValue = dayjs(plan?.deliveryDateStr)
const latestStartValue = dayjs(plan?.latestStartTimeStr)
return {
productId: plan?.productId,
taskId: plan?.taskId,
taskDetailId: plan?.taskDetailId,
planNumber: Number(plan?.planNumber ?? 0),
finishNumber: Number(plan?.finishNumber ?? 0),
isCode: typeof plan?.isCode === 'boolean' ? plan.isCode : true,
isPreProduction: Number(plan?.isPreProduction ?? 0),
planStartTime: startValue.isValid() ? startValue.valueOf() : undefined,
planEndTime: endValue.isValid() ? endValue.valueOf() : undefined,
reyaNumber: Number(plan?.reyaNumber ?? plan?.planNumber ?? 0),
workerId: plan?.workerId,
feedingPipeline: plan?.feedingPipeline ?? device?.deviceId,
deviceId: device?.deviceId ?? plan?.deviceId,
deliveryDate: deliveryDateValue.isValid() ? deliveryDateValue.valueOf() : undefined,
latestStartTime: latestStartValue.isValid() ? latestStartValue.valueOf() : undefined
}
})
})
if (!createReqVOList.length) {
message.warning('暂无可保存的计划数据')
return
}
previewSaveLoading.value = true
try {
await PlanApi.createBatch({ createReqVOList })
message.success('排产计划保存成功')
previewVisible.value = false
emit('saved')
} finally {
previewSaveLoading.value = false
}
}
watch(
() => props.modelValue,
async (visible) => {
if (visible) {
activePreviewDevice.value = previewScheduleList.value[0]
await nextTick()
initGanttPreview()
return
}
destroyGantt()
}
)
watch(
() => props.scheduleList,
async () => {
if (!props.modelValue) return
activePreviewDevice.value = previewScheduleList.value[0]
await nextTick()
initGanttPreview()
},
{ deep: true }
)
onBeforeUnmount(() => {
destroyGantt()
})
</script>
<style scoped>
.schedule-preview-wrap {
display: flex;
width: 100%;
gap: 12px;
}
.schedule-gantt-container {
flex: 1;
min-width: 0;
height: 800px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
}
.schedule-detail-panel {
width: 280px;
flex: 0 0 280px;
}
.schedule-detail-title {
margin-bottom: 10px;
font-weight: 600;
}
.schedule-plan-list-title {
margin-top: 14px;
margin-bottom: 10px;
font-weight: 600;
}
.schedule-plan-list {
overflow: auto;
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 + .schedule-plan-item {
margin-top: 8px;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_row) {
font-weight: 600;
}
.schedule-gantt-container :deep(.gantt_grid_data .gantt_cell) {
text-overflow: clip;
}
.schedule-gantt-container :deep(.gantt-inline-editor-trigger) {
display: inline-block;
width: 100%;
overflow: visible;
cursor: pointer;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task) {
background: #67c23a;
border-color: #67c23a;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task .gantt_task_content) {
color: #ffffff;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history) {
background: #909399;
border-color: #909399;
}
.schedule-gantt-container :deep(.gantt_task_line.schedule-plan-task-history .gantt_task_content) {
color: #ffffff;
}
</style>
Loading…
Cancel
Save