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

289 lines
7.2 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'
/**
* 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
// 中文 locale
gantt.locale = {
date: {
month_full: [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
],
month_short: [
'1月',
'2月',
'3月',
'4月',
'5月',
'6月',
'7月',
'8月',
'9月',
'10月',
'11月',
'12月'
],
day_full: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
day_short: ['日', '一', '二', '三', '四', '五', '六']
},
labels: {
new_task: '新任务',
icon_save: '保存',
icon_cancel: '取消',
icon_details: '详情',
icon_edit: '编辑',
icon_delete: '删除',
confirm_closing: '',
confirm_deleting: '确认删除?',
section_description: '描述',
section_time: '时间',
section_type: '类型',
column_wbs: 'WBS',
column_text: '任务名称',
column_start_date: '开始时间',
column_duration: '时长',
column_add: '',
link: '关联',
confirm_link_deleting: '将被删除',
link_start: '(开始)',
link_end: '(结束)',
type_task: '任务',
type_project: '项目',
type_milestone: '里程碑',
minutes: '分钟',
hours: '小时',
days: '天',
weeks: '周',
months: '月',
years: '年'
}
}
// 基础配置
gantt.config.date_format = '%Y-%m-%d %H:%i:%s'
gantt.config.duration_unit = 'hour'
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 = false
gantt.config.details_on_dblclick = false
gantt.config.show_progress = true
// 只读模式
if (props.readonly) {
gantt.config.drag_move = false
gantt.config.drag_resize = false
gantt.config.drag_progress = false
} else {
gantt.config.drag_move = true
gantt.config.drag_resize = true
gantt.config.drag_progress = false
}
// 时间刻度:日 > 8小时00:00 / 08:00 / 16:00
gantt.config.scales = [
{ unit: 'day', step: 1, format: '%Y-%m-%d %D' },
{
unit: 'hour',
step: 8,
format: (date: Date) => {
return String(date.getHours()).padStart(2, '0') + ':00'
}
}
]
// 左侧列配置
gantt.config.columns = [
{ name: 'text', label: '任务名称', tree: true, width: 180, resize: true },
{ name: 'workstationName', label: '工作站', align: 'center', width: 100, resize: true },
{ name: 'processName', label: '工序', align: 'center', width: 100, resize: true },
{ name: 'start_date', label: '开始时间', align: 'center', width: 130 },
{ name: 'end_date', label: '结束时间', align: 'center', width: 130 }
]
// 今天标记线
gantt.plugins({ marker: true })
gantt.addMarker({
start_date: new Date(),
css: 'today',
text: '今天'
})
// 任务颜色模板
gantt.templates.task_class = (_start: any, _end: any, task: any) => {
if (task.type === gantt.config.types.project) return 'gantt-project-bar'
return ''
}
gantt.templates.task_cell_class = () => ''
gantt.templates.task_row_class = () => ''
// 事件监听
if (!props.readonly) {
gantt.attachEvent('onAfterTaskDrag', (id: string | number) => {
const task = gantt.getTask(id)
emit('task-update', {
id: task.taskId || id,
startTime: task.start_date,
endTime: task.end_date,
duration: (task.duration as number) / 8 // 转回工作日
})
})
}
gantt.attachEvent('onTaskClick', (id: string | number) => {
emit('task-click', id)
return true
})
gantt.init(ganttContainer.value)
ganttInited.value = true
}
/** 转换数据为甘特图格式 */
const convertToGanttData = (tasks: any[]) => {
const data: any[] = []
// 按工单分组
const workOrderMap = new Map<number, any[]>()
tasks.forEach((task) => {
const woId = task.workOrderId
if (!workOrderMap.has(woId)) {
workOrderMap.set(woId, [])
}
workOrderMap.get(woId)!.push(task)
})
workOrderMap.forEach((woTasks, woId) => {
const firstTask = woTasks[0]
// 工单作为 project 行
data.push({
id: 'wo_' + woId,
text: firstTask.workOrderCode + ' ' + firstTask.workOrderName,
type: gantt.config.types.project,
open: true
})
// 任务行
woTasks.forEach((t) => {
data.push({
id: 'task_' + t.id,
taskId: t.id,
text: t.name,
workstationName: t.workstationName || '',
processName: t.processName || '',
start_date: t.startTime ? new Date(t.startTime) : new Date(),
duration: (t.duration || 1) * 8, // 工作日 -> 小时end_date 由 gantt 自动计算)
parent: 'wo_' + woId,
progress: t.quantity > 0 ? t.producedQuantity / t.quantity : 0,
color: t.colorCode || '#00AEF3'
})
})
})
return { data, links: [] }
}
/** 加载数据到甘特图 */
const loadData = (tasks: any[]) => {
if (!ganttInited.value) return
gantt.clearAll()
const ganttData = convertToGanttData(tasks)
gantt.parse(ganttData)
}
// 监听 tasks 变化
watch(
() => props.tasks,
(val) => {
if (val && 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: #ff4444;
opacity: 0.4;
}
.gantt_marker.today .gantt_marker_content {
color: #ff4444;
font-size: 12px;
}
/* 工单project行样式 */
.gantt-project-bar .gantt_task_progress {
background: #7b68ee;
}
</style>