admin-vben/apps/web-antd/src/views/mes/pro/task/components/gantt-chart.vue

240 lines
7.0 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.

<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { gantt } from 'dhtmlx-gantt';
import { BarcodeBizTypeEnum } from '#/views/mes/utils/constants';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
/**
* 甘特图组件(基于 dhtmlx-gantt
*
* 1. 按工单分组展示生产任务,工单为 project 行,任务为子行
* 2. 支持只读预览和拖拽编辑两种模式
* 3. 拖拽后触发 taskUpdate 事件,通知父组件批量保存
* 4. 时间刻度:周 → 日 → 8 小时1 工作日 = 8 小时)
*/
defineOptions({ name: 'GanttChart', inheritAttrs: false });
const props = withDefaults(
defineProps<{
height?: number; // 甘特图高度
readonly?: boolean; // 是否只读
tasks?: any[]; // 甘特图任务数据
}>(),
{
height: 350,
readonly: false,
tasks: () => [],
},
);
const emit = defineEmits<{
taskClick: [id: number | string];
taskUpdate: [task: any];
}>();
const ganttContainer = ref<HTMLElement>(); // 甘特图挂载容器
const ganttInited = ref(false); // 甘特图是否已初始化
/** 初始化甘特图配置 */
function 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;
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) => gantt.date.date_to_str('%M %d')(date);
const daysStyle = (date: Date) =>
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 },
];
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) =>
task.type === gantt.config.types.project ? 'gantt-project-bar' : '';
gantt.templates.timeline_cell_class = () => '';
gantt.templates.task_row_class = () => '';
// 编辑事件监听(通过 lightbox 弹窗编辑后触发)
if (!props.readonly) {
gantt.attachEvent('onAfterTaskUpdate', (id: number | string) => {
const task = gantt.getTask(id);
// 只回写 task 节点,避免把工单(project)节点当成任务保存
if (task.type !== gantt.config.types.task || !task.originalId) {
return;
}
emit('taskUpdate', {
duration: task.duration,
endTime: task.end_date,
id: task.originalId,
startTime: task.start_date,
});
});
}
gantt.attachEvent('onTaskClick', (id: number | string) => {
emit('taskClick', id);
return true;
});
gantt.init(ganttContainer.value);
ganttInited.value = true;
}
/** 加载数据到甘特图 */
function loadData(tasks: any[]) {
if (!ganttInited.value) {
return;
}
gantt.clearAll();
// 后端 type 使用整数业务类型,需映射为 gantt 类型字符串
const typeMap: Record<number, string> = {
[BarcodeBizTypeEnum.WORKORDER]: 'project',
[BarcodeBizTypeEnum.TASK]: 'task',
};
gantt.parse({
data: tasks.map((item: any) => ({
...item,
type: typeMap[item.type] || item.type,
start_date: item.startDate ? new Date(item.startDate) : undefined,
end_date: item.endDate ? new Date(item.endDate) : undefined,
})),
links: [],
});
}
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>
<template>
<div ref="ganttContainer" :style="{ width: '100%', height: `${height}px` }"></div>
</template>
<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>