|
|
|
|
@ -0,0 +1,407 @@
|
|
|
|
|
<template>
|
|
|
|
|
<ContentWrap>
|
|
|
|
|
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
|
|
|
|
|
<el-form-item label="查询时间" prop="range">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="queryRange"
|
|
|
|
|
type="datetimerange"
|
|
|
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
range-separator="-"
|
|
|
|
|
start-placeholder="开始时间"
|
|
|
|
|
end-placeholder="结束时间"
|
|
|
|
|
class="!w-360px"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />查询</el-button>
|
|
|
|
|
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</ContentWrap>
|
|
|
|
|
<ContentWrap>
|
|
|
|
|
<div v-loading="loading" class="gantt-page-wrap">
|
|
|
|
|
<div ref="ganttContainerRef" class="gantt-chart-container"></div>
|
|
|
|
|
<div class="gantt-detail-panel">
|
|
|
|
|
<div class="gantt-detail-title">设备信息</div>
|
|
|
|
|
<el-descriptions v-if="activeDevice" :column="1" border size="small">
|
|
|
|
|
<el-descriptions-item label="设备名称">{{ activeDevice.deviceName || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="设备编码">{{ activeDevice.deviceCode || '-' }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="计划条数">{{ activeDevice.plans?.length ?? 0 }}</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
<el-empty v-else description="暂无设备计划" :image-size="80" />
|
|
|
|
|
<div class="gantt-plan-list-title">计划明细</div>
|
|
|
|
|
<div class="gantt-plan-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(plan, index) in activeDevice?.plans ?? []"
|
|
|
|
|
:key="`${activeDevice?.deviceId}-${plan.planId}-${index}`"
|
|
|
|
|
class="gantt-plan-item"
|
|
|
|
|
>
|
|
|
|
|
<div>计划ID:{{ plan.planId ?? '-' }}</div>
|
|
|
|
|
<div>产品:{{ plan.productName ?? '-' }}</div>
|
|
|
|
|
<div>计划数量:{{ plan.planNumber ?? '-' }}</div>
|
|
|
|
|
<div>开始:{{ plan.planStartTimeStr || '-' }}</div>
|
|
|
|
|
<div>结束:{{ plan.planEndTimeStr || '-' }}</div>
|
|
|
|
|
<div>最晚开工:{{ plan.latestStartTimeStr || '-' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</ContentWrap>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { DevicePlanGanttRespVO, PlanApi } from '@/api/mes/plan'
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
import { gantt } from 'dhtmlx-gantt'
|
|
|
|
|
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'MesGanttChart' })
|
|
|
|
|
|
|
|
|
|
type DevicePlanView = DevicePlanGanttRespVO & {
|
|
|
|
|
plans: (DevicePlanGanttRespVO['plans'][number] & {
|
|
|
|
|
planStartTimeStr: string
|
|
|
|
|
planEndTimeStr: string
|
|
|
|
|
latestStartTimeStr: string
|
|
|
|
|
})[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const queryFormRef = ref()
|
|
|
|
|
const ganttContainerRef = ref<HTMLDivElement>()
|
|
|
|
|
const ganttEventIds = ref<string[]>([])
|
|
|
|
|
const scheduleList = ref<DevicePlanView[]>([])
|
|
|
|
|
const activeDevice = ref<DevicePlanView>()
|
|
|
|
|
|
|
|
|
|
const buildDefaultRange = () => {
|
|
|
|
|
const start = dayjs().startOf('day')
|
|
|
|
|
const end = start.add(7, 'day').endOf('day')
|
|
|
|
|
return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const queryRange = ref<string[]>(buildDefaultRange())
|
|
|
|
|
const queryParams = reactive({
|
|
|
|
|
startTime: queryRange.value[0],
|
|
|
|
|
endTime: queryRange.value[1]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const getGlobalDateRange = (devices: DevicePlanView[]) => {
|
|
|
|
|
const allPlans = devices.flatMap((item) => item.plans ?? [])
|
|
|
|
|
const starts = allPlans.map((item) => dayjs(item.planStartTime).valueOf()).filter((item) => Number.isFinite(item))
|
|
|
|
|
const ends = allPlans.map((item) => dayjs(item.planEndTime).valueOf()).filter((item) => 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: string) => {
|
|
|
|
|
const date = dayjs(value)
|
|
|
|
|
if (!date.isValid()) return undefined
|
|
|
|
|
return date.format('YYYY-MM-DD HH:mm')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatDetailDate = (value: string) => {
|
|
|
|
|
const date = dayjs(value)
|
|
|
|
|
if (!date.isValid()) return '-'
|
|
|
|
|
return date.format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getDeviceTaskRangeByChildren = (planTasks: { start_date: string; end_date: string }[]) => {
|
|
|
|
|
const starts = planTasks.map((item) => dayjs(item.start_date)).filter((item) => item.isValid())
|
|
|
|
|
const ends = planTasks.map((item) => dayjs(item.end_date)).filter((item) => item.isValid())
|
|
|
|
|
if (!starts.length || !ends.length) return undefined
|
|
|
|
|
const earliestStart = starts.reduce((min, current) => (current.valueOf() < min.valueOf() ? current : min))
|
|
|
|
|
const latestEnd = ends.reduce((max, current) => (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 buildGanttData = (devices: DevicePlanView[]) => {
|
|
|
|
|
const tasks: any[] = []
|
|
|
|
|
const links: any[] = []
|
|
|
|
|
let linkIndex = 1
|
|
|
|
|
|
|
|
|
|
devices.forEach((device) => {
|
|
|
|
|
const deviceId = `device-${device.deviceId}`
|
|
|
|
|
const plans = (device.plans ?? []).map((plan) => ({
|
|
|
|
|
...plan,
|
|
|
|
|
_start: dayjs(plan.planStartTime),
|
|
|
|
|
_end: dayjs(plan.planEndTime)
|
|
|
|
|
}))
|
|
|
|
|
const validPlans = plans
|
|
|
|
|
.filter((plan) => plan._start.isValid() && plan._end.isValid())
|
|
|
|
|
.sort((a, b) => a._start.valueOf() - b._start.valueOf())
|
|
|
|
|
const firstPlan = validPlans[0]
|
|
|
|
|
const deviceRange = getDeviceTaskRangeByChildren(
|
|
|
|
|
validPlans.map((item) => ({
|
|
|
|
|
start_date: item.planStartTime,
|
|
|
|
|
end_date: item.planEndTime
|
|
|
|
|
}))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
tasks.push({
|
|
|
|
|
id: deviceId,
|
|
|
|
|
text: `${device.deviceName ?? '-'}`,
|
|
|
|
|
start_date: formatGanttDate(deviceRange?.start_date ? dayjs(deviceRange.start_date).toISOString() : firstPlan?.planStartTime),
|
|
|
|
|
end_date: formatGanttDate(deviceRange?.end_date ? dayjs(deviceRange.end_date).toISOString() : firstPlan?.planEndTime),
|
|
|
|
|
duration: deviceRange?.duration ?? 1,
|
|
|
|
|
parent: 0,
|
|
|
|
|
progress: 0,
|
|
|
|
|
open: true,
|
|
|
|
|
readonly: true,
|
|
|
|
|
_deviceData: device
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let previousPlanTaskId: string | null = null
|
|
|
|
|
validPlans.forEach((plan, index) => {
|
|
|
|
|
const startDate = formatGanttDate(plan.planStartTime)
|
|
|
|
|
const endDate = formatGanttDate(plan.planEndTime)
|
|
|
|
|
const duration = Math.max(plan._end.endOf('day').diff(plan._start.startOf('day'), 'day') + 1, 1)
|
|
|
|
|
if (!startDate || !endDate) return
|
|
|
|
|
const planTaskId = `plan-${device.deviceId}-${plan.planId ?? index}-${index}`
|
|
|
|
|
tasks.push({
|
|
|
|
|
id: planTaskId,
|
|
|
|
|
text: `${plan.productName ?? '-'} / 数量: ${plan.planNumber ?? '-'}`,
|
|
|
|
|
start_date: startDate,
|
|
|
|
|
end_date: endDate,
|
|
|
|
|
duration,
|
|
|
|
|
parent: deviceId,
|
|
|
|
|
progress: 0,
|
|
|
|
|
readonly: true,
|
|
|
|
|
_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 initGantt = () => {
|
|
|
|
|
if (!ganttContainerRef.value) return
|
|
|
|
|
destroyGantt()
|
|
|
|
|
|
|
|
|
|
gantt.plugins({ tooltip: true })
|
|
|
|
|
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 = 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: 240
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'start_date',
|
|
|
|
|
label: '开始时间',
|
|
|
|
|
align: 'center',
|
|
|
|
|
width: 180,
|
|
|
|
|
template: (task: any) => formatDetailDate(task.start_date)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'duration',
|
|
|
|
|
label: '天数',
|
|
|
|
|
align: 'center',
|
|
|
|
|
width: 70,
|
|
|
|
|
template: (task: any) => String(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) => (task?._planData ? 'gantt-plan-task' : '')
|
|
|
|
|
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
|
|
|
|
|
return `
|
|
|
|
|
<div>设备:${device?.deviceName ?? '-'}</div>
|
|
|
|
|
<div>计划条数:${device?.plans?.length ?? 0}</div>
|
|
|
|
|
`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const globalRange = getGlobalDateRange(scheduleList.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 = buildGanttData(scheduleList.value)
|
|
|
|
|
gantt.parse(ganttData)
|
|
|
|
|
if (ganttData.data.length) {
|
|
|
|
|
activeDevice.value = ganttData.data[0]._deviceData
|
|
|
|
|
gantt.showDate(gantt.config.start_date)
|
|
|
|
|
} else {
|
|
|
|
|
activeDevice.value = undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clickEventId = gantt.attachEvent('onTaskClick', (id) => {
|
|
|
|
|
const task = gantt.getTask(id)
|
|
|
|
|
activeDevice.value = task?._deviceData
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
ganttEventIds.value.push(clickEventId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mapScheduleList = (list: DevicePlanGanttRespVO[]) =>
|
|
|
|
|
(Array.isArray(list) ? list : []).map((device) => ({
|
|
|
|
|
...device,
|
|
|
|
|
plans: (device.plans ?? []).map((plan) => ({
|
|
|
|
|
...plan,
|
|
|
|
|
planStartTimeStr: formatDetailDate(plan.planStartTime),
|
|
|
|
|
planEndTimeStr: formatDetailDate(plan.planEndTime),
|
|
|
|
|
latestStartTimeStr: formatDetailDate(plan.latestStartTime)
|
|
|
|
|
}))
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const getList = async () => {
|
|
|
|
|
loading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const data = await PlanApi.getGanttByDevice({
|
|
|
|
|
startTime: queryParams.startTime,
|
|
|
|
|
endTime: queryParams.endTime
|
|
|
|
|
})
|
|
|
|
|
scheduleList.value = mapScheduleList(data)
|
|
|
|
|
await nextTick()
|
|
|
|
|
initGantt()
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleQuery = () => {
|
|
|
|
|
const [startTime, endTime] = Array.isArray(queryRange.value) ? queryRange.value : []
|
|
|
|
|
queryParams.startTime = startTime || ''
|
|
|
|
|
queryParams.endTime = endTime || ''
|
|
|
|
|
getList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resetQuery = () => {
|
|
|
|
|
queryFormRef.value?.resetFields()
|
|
|
|
|
queryRange.value = buildDefaultRange()
|
|
|
|
|
queryParams.startTime = queryRange.value[0]
|
|
|
|
|
queryParams.endTime = queryRange.value[1]
|
|
|
|
|
getList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
handleQuery()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
destroyGantt()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.gantt-page-wrap {
|
|
|
|
|
display: flex;
|
|
|
|
|
width: 100%;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-chart-container {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
height: calc(100vh - 320px);
|
|
|
|
|
min-height: 640px;
|
|
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-detail-panel {
|
|
|
|
|
width: 320px;
|
|
|
|
|
flex: 0 0 320px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-detail-title {
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-plan-list-title {
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-plan-list {
|
|
|
|
|
max-height: calc(100vh - 420px);
|
|
|
|
|
min-height: 240px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
border: 1px solid var(--el-border-color);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-plan-item {
|
|
|
|
|
padding: 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: var(--el-fill-color-light);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-plan-item + .gantt-plan-item {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-chart-container :deep(.gantt_grid_data .gantt_row) {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-chart-container :deep(.gantt_task_line.gantt-plan-task) {
|
|
|
|
|
background: #409eff;
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gantt-chart-container :deep(.gantt_task_line.gantt-plan-task .gantt_task_content) {
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
}
|
|
|
|
|
</style>
|