admin-vue3/src/views/mes/pro/task/components/GanttChart.vue

263 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!-- dhtmlx-gantt Vue 3 封装组件基于 dhtmlx-gantt 实现甘特图展示和拖拽编辑 -->
<template>
<div ref="ganttContainer" :style="{ width: '100%', height: height + 'px' }"></div>
</template>
<script setup lang="ts">
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import { BarcodeBizTypeEnum } from '@/views/mes/utils/constants'
/**
* GanttChart - 甘特图组件
*
* 功能:
* 1. 按工单分组展示生产任务,工单为 project 行,任务为子行
* 2. 支持只读预览和拖拽编辑两种模式
* 3. 拖拽后触发 task-update 事件,通知父组件批量保存
* 4. 时间刻度:周 → 日工作日单位1 工作日 = 8 小时)
*/
defineOptions({ name: 'GanttChart' })
const props = withDefaults(
defineProps<{
tasks?: any[] // 甘特图任务数据
readonly?: boolean // 是否只读
height?: number // 甘特图高度
}>(),
{
tasks: () => [],
readonly: false,
height: 350
}
)
const emit = defineEmits<{
'task-update': [task: any]
'task-click': [id: string | number]
}>()
const ganttContainer = ref<HTMLElement>()
const ganttInited = ref(false)
/** 初始化甘特图配置 */
const initGantt = () => {
if (!ganttContainer.value) {
return
}
// 中文国际化
gantt.i18n.setLocale('cn')
// 基础配置
gantt.config.readonly = props.readonly
gantt.config.date_format = '%Y-%m-%d %H:%i:%s'
gantt.config.duration_unit = 'hour' // 使用小时作为持续时间单位,配合 duration_step 实现工作日单位
gantt.config.duration_step = 8 // 1 工作日 = 8 小时
gantt.config.row_height = 36 // 显式声明更清晰
gantt.config.bar_height = 24 // 让条更紧凑美观
gantt.config.fit_tasks = true // 时间范围自动适应
gantt.config.auto_scheduling = false // 显式关闭防意外
gantt.config.drag_links = false // 禁止拖动任务关系
gantt.config.details_on_create = true // 单击显示添加详情
gantt.config.details_on_dblclick = true // 双击显示明细
gantt.config.show_progress = true // 确保进度条显示
gantt.config.open_tree_initially = true // 初始展开树结构
gantt.config.auto_types = false // 禁止自动升级为 project
gantt.config.drag_move = !props.readonly // 编辑态允许直接拖动任务
gantt.config.drag_resize = !props.readonly // 编辑态允许直接调整任务持续时间
gantt.config.drag_progress = false // 禁止拖动进度条
// lightbox 弹窗配置:只保留时间编辑,去掉描述编辑和删除按钮
gantt.config.lightbox.sections = [{ name: 'time', type: 'duration', map_to: 'auto' }]
gantt.config.buttons_left = ['gantt_save_btn']
gantt.config.buttons_right = ['gantt_cancel_btn']
// 时间刻度:周 > 日 > 8小时
const weekScaleTemplate = (date: Date) => {
const dateToStr = gantt.date.date_to_str('%M %d')
const endDate = gantt.date.add(gantt.date.add(date, 1, 'week'), -1, 'day')
return dateToStr(date) + ' - ' + dateToStr(endDate)
}
const dayTemplate = (date: Date) => {
return gantt.date.date_to_str('%M %d')(date)
}
const daysStyle = (date: Date) => {
return date.getDay() === 0 || date.getDay() === 6 ? 'weekend' : ''
}
gantt.config.scales = [
{ unit: 'week', step: 1, format: weekScaleTemplate },
{ unit: 'day', step: 1, format: dayTemplate, css: daysStyle },
{ unit: 'hour', step: 8, format: '%H:%i' }
]
gantt.config.scale_height = 50
gantt.config.show_task_cells = true // 显示任务单元格边框
// 左侧列配置
gantt.config.columns = [
{ name: 'text', label: '任务名称', tree: true, width: 180, resize: true },
{ name: 'workstation', label: '工作站', align: 'center', width: 100, resize: true },
{ name: 'process', label: '工序', align: 'center', width: 100, resize: true },
{ name: 'start_date', label: '开始时间', align: 'center', width: 130 },
{ name: 'end_date', label: '结束时间', align: 'center', width: 130 }
]
// 今天标记线 + tooltip 插件
gantt.plugins({ marker: true, tooltip: true })
gantt.addMarker({
start_date: new Date(),
css: 'today',
text: '今天'
})
// 甘特条上的文本
gantt.templates.task_text = (_start: any, _end: any, task: any) => {
const percent = Math.round((task.progress || 0) * 100)
if (task.type === 'project') {
return `<b>生产工单:</b> ${task.text} <span>完成比例:${percent}%</span>`
}
return `<b>生产任务:</b> ${task.process || ''} ${task.text} <span>完成比例:${percent}%</span>`
}
// 鼠标悬浮提示
gantt.templates.tooltip_text = (_start: any, _end: any, task: any) => {
const percent = Math.round((task.progress || 0) * 100)
if (task.type === 'project') {
return `<b>生产工单:</b> ${task.text} <span>完成比例:${percent}%</span>`
}
return `<b>生产任务:</b> ${task.process || ''} ${task.text} <span>完成比例:${percent}%</span>`
}
// 任务颜色模板
gantt.templates.task_class = (_start: any, _end: any, task: any) => {
if (task.type === gantt.config.types.project) {
return 'gantt-project-bar'
}
return ''
}
gantt.templates.timeline_cell_class = () => '' // 防止 gantt 添加默认样式类
gantt.templates.task_row_class = () => '' // 防止 gantt 添加默认样式类
// 编辑事件监听(通过 lightbox 弹窗编辑后触发)
if (!props.readonly) {
gantt.attachEvent('onAfterTaskUpdate', (id: string | number) => {
const task = gantt.getTask(id)
// 生产排产只允许回写 task 节点,避免把工单(project)节点当成任务保存
if (task.type !== gantt.config.types.task || !task.originalId) {
return
}
// 触发 task-update 事件,通知父组件保存修改
emit('task-update', {
id: task.originalId,
startTime: task.start_date,
endTime: task.end_date,
duration: task.duration
})
})
}
// 点击任务事件
gantt.attachEvent('onTaskClick', (id: string | number) => {
emit('task-click', id)
return true
})
// 初始化
gantt.init(ganttContainer.value)
ganttInited.value = true
}
/** 加载数据到甘特图 */
const loadData = (tasks: any[]) => {
if (!ganttInited.value) {
return
}
gantt.clearAll()
// 后端 type 使用 MesBizTypeConstants 整数,需映射为 gantt 类型字符串
const TYPE_MAP: Record<number, string> = {
[BarcodeBizTypeEnum.WORKORDER]: 'project',
[BarcodeBizTypeEnum.TASK]: 'task'
}
const transformed = {
data: tasks.map((item: any) => ({
...item,
type: TYPE_MAP[item.type] || item.type,
start_date: item.startDate ? new Date(item.startDate) : undefined,
end_date: item.endDate ? new Date(item.endDate) : undefined
})),
links: []
}
gantt.parse(transformed)
}
/** 监听 tasks 变化 */
watch(
() => props.tasks,
(val) => {
if (val?.length && ganttInited.value) {
loadData(val)
}
},
{ deep: true }
)
/** 组件挂载后初始化甘特图 */
onMounted(() => {
initGantt()
if (props.tasks?.length) {
loadData(props.tasks)
}
})
/** 组件卸载前清理甘特图 */
onBeforeUnmount(() => {
if (ganttInited.value) {
gantt.clearAll()
}
})
defineExpose({ loadData })
</script>
<style>
/* 今天标记线 */
.gantt_marker.today {
background-color: #f44;
opacity: 0.4;
}
.gantt_marker.today .gantt_marker_content {
font-size: 12px;
color: #f44;
}
/* 工单project行样式 */
.gantt-project-bar .gantt_task_progress {
background: #7b68ee;
}
/* 甘特条圆角 */
.gantt_task_line {
border-radius: 8px;
}
/* 周末背景色 */
.weekend {
background: #f0f0f0 !important;
}
/* 行悬浮高亮 */
.gantt_grid_data .gantt_row:hover,
.gantt_grid_data .gantt_row.odd:hover {
background-color: #f3f1fe !important;
}
/* 选中行高亮 */
.gantt_grid_data .gantt_row.gantt_selected,
.gantt_grid_data .gantt_row.odd.gantt_selected,
.gantt_task_row.gantt_selected {
background-color: #f3f1fe !important;
}
</style>