复制代码
{
"data": [
{
"actual_end_date": "2025-04-23",
"actual_start_date": "2025-04-15",
"duration": 10,
"end_date": "2025-05-01",
"id": "2|jvUiek",
"parent": "0",
"progress": 0,
"start_date": "2025-04-21",
"text": "2",
"time": -8,
"responsible_person": "1|管理员",
"milestone_node": null
},
{
"actual_end_date": "2025-04-24",
"actual_start_date": "2025-04-21",
"duration": 1,
"end_date": "2025-04-22",
"id": "1|rMyrYE",
"parent": "0",
"progress": 0,
"start_date": "2025-04-21",
"text": "1",
"time": 2,
"responsible_person": "1|管理员",
"milestone_node": null
},
{
"actual_end_date": null,
"actual_start_date": "2025-05-06",
"duration": 7,
"end_date": "2025-05-02",
"id": "1",
"parent": "1|rMyrYE",
"progress": 0.11,
"start_date": "2025-04-25",
"text": "节点2",
"time": 6,
"responsible_person": "1|管理员",
"milestone_node": "0"
},
{
"actual_end_date": null,
"actual_start_date": null,
"duration": 2,
"end_date": "2025-04-29",
"id": "3",
"parent": "1",
"progress": 0,
"start_date": "2025-04-27",
"text": "节点1",
"time": 9,
"responsible_person": "1|管理员",
"milestone_node": "1"
},
{
"actual_end_date": null,
"actual_start_date": null,
"duration": 33,
"end_date": "2025-05-30",
"id": "4",
"parent": "1",
"progress": 0,
"start_date": "2025-04-27",
"text": "节点2",
"time": -22,
"responsible_person": "1|管理员",
"milestone_node": "0"
},
{
"actual_end_date": null,
"actual_start_date": null,
"duration": 1,
"end_date": "2025-05-02",
"id": "2",
"parent": "1|rMyrYE",
"progress": 0,
"start_date": "2025-05-01",
"text": "节点1",
"time": 6,
"responsible_person": "1|管理员",
"milestone_node": "1"
}
]
}
{
"data": [
{
"actual_end_date": "2025-04-23",
"actual_start_date": "2025-04-15",
"duration": 10,
"end_date": "2025-05-01",
"id": "2|jvUiek",
"parent": "0",
"progress": 0,
"start_date": "2025-04-21",
"text": "2",
"time": -8,
"responsible_person": "1|管理员",
"milestone_node": null
},
{
"actual_end_date": "2025-04-24",
"actual_start_date": "2025-04-21",
"duration": 1,
"end_date": "2025-04-22",
"id": "1|rMyrYE",
"parent": "0",
"progress": 0,
"start_date": "2025-04-21",
"text": "1",
"time": 2,
"responsible_person": "1|管理员",
"milestone_node": null
},
{
"actual_end_date": null,
"actual_start_date": "2025-05-06",
"duration": 7,
"end_date": "2025-05-02",
"id": "1",
"parent": "1|rMyrYE",
"progress": 0.11,
"start_date": "2025-04-25",
"text": "节点2",
"time": 6,
"responsible_person": "1|管理员",
"milestone_node": "0"
},
{
"actual_end_date": null,
"actual_start_date": null,
"duration": 2,
"end_date": "2025-04-29",
"id": "3",
"parent": "1",
"progress": 0,
"start_date": "2025-04-27",
"text": "节点1",
"time": 9,
"responsible_person": "1|管理员",
"milestone_node": "1"
},
{
"actual_end_date": null,
"actual_start_date": null,
"duration": 33,
"end_date": "2025-05-30",
"id": "4",
"parent": "1",
"progress": 0,
"start_date": "2025-04-27",
"text": "节点2",
"time": -22,
"responsible_person": "1|管理员",
"milestone_node": "0"
},
{
"actual_end_date": null,
"actual_start_date": null,
"duration": 1,
"end_date": "2025-05-02",
"id": "2",
"parent": "1|rMyrYE",
"progress": 0,
"start_date": "2025-05-01",
"text": "节点1",
"time": 6,
"responsible_person": "1|管理员",
"milestone_node": "1"
}
]
}
vue
复制代码
<template>
<div>
<div v-loading="loading" ref="ganttRef"></div>
</div>
</template>
<script setup lang='ts'>
import {gantt} from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import {onBeforeUnmount} from 'vue';
import demoData from './ganttData.json'
import {ElLoading} from "element-plus";
const props = defineProps({
taskData: {
type: Array,
default: () => []
}
});
const loading = ref(false);
const ganttRef = ref(null);
// 通过taskData计算links数组
const generateLinks = (data: any) => {
const links: any = [];
data.forEach((task: any) => {
if (task.parent) {
const linkId = `${task.parent}-${task.id}`;
links.push({
id: linkId,
source: task.parent,
target: task.id,
type: '0'
});
}
});
return links;
};
const initGantt = (task: any) => {
let loading = ElLoading.service({
target: '.dialogLoading',
lock: true,
background: 'rgba(0, 0, 0, 0.3)',
text: '甘特图渲染中...',
});
gantt.clearAll();
gantt.i18n.setLocale('cn');
gantt.config.columns = [
{
name: 'text',
label: '项目名称',
resize: true,
tree: true,
width: '150',
// 自定义左侧的数据,比如有的任务是里程碑节点,在这里加个小旗子的图标
template: (item) => {
const milestoneIcon = item.milestone_node == '0' ?
`<svg xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
style="vertical-align: middle; margin-right: 6px;">
<path fill="#ff4d4f" d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/>
</svg>`
: '';
return `
<div class="text_cell" style="display: flex; align-items: center;">
${milestoneIcon}
<span class="text-cell-text">${item.text}</span>
</div>
`;
},
},
{
name: 'start_date',
label: '计划开始时间',
align: 'center',
resize: true,
tree: false,
width: 110,
},
{
name: 'end_date',
label: '计划结束时间',
width: 110,
align: 'center',
resize: true,
},
{
name: 'actual_start_date',
label: '实际开始时间',
align: 'center',
resize: true,
tree: false,
width: 110,
},
{
name: 'actual_end_date',
label: '实际结束时间',
width: 110,
align: 'center',
resize: true,
},
{
name: 'time',
label: '时间差值',
width: 60,
align: 'center',
resize: true,
template: (item) => {
const time = Number(item.time);
const statusClass = time >= 0 ? 'negative' : 'positive';
return `<div class="status-cell"><span class="status-text ${statusClass}">${time}</span></div>`;
},
},
{
name: 'progress',
label: '项目进度',
align: 'center',
tree: false,
resize: true,
width: '100',
template: (item) => {
return `<div class="status-cell"><span class="status-text">${(item.progress * 100).toFixed(2)}%</span></div>`;
},
},
];
gantt.plugins({tooltip: true});
gantt.config.smart_rendering = true;
gantt.config.auto_scheduling = true;
gantt.config.auto_scheduling_strict = true;
gantt.config.show_grid = true;
gantt.config.xml_date = '%Y-%m-%d';
gantt.config.autosize = true;
gantt.config.show_links = true;
gantt.config.readonly = true;
gantt.config.sort = true;
gantt.config.drag_project = true;
gantt.config.scale_height = 50;
gantt.config.open_split_tasks = true;
gantt.config.show_tasks_outside_timescale = true;
// 鼠标悬浮提示框
gantt.templates.tooltip_text = function (start, end, task) {
// console.log('start----------->', start)
// console.log('end----------->', end)
// console.log('task----------->', task)
return (
'<b>标题:</b> ' +
task.text +
'<br/><span>开始:</span> ' +
gantt.templates.tooltip_date_format(start) +
'<br/><span>结束:</span> ' +
gantt.templates.tooltip_date_format(end) +
'<br/><span>进度:</span> ' +
Math.round(task.progress * 100) +
'%'
);
};
gantt.config.tooltip_offset_x = 10;
gantt.config.tooltip_offset_y = 30;
gantt.init(ganttRef.value);
gantt.parse(task);
loading.close();
// 设置鼠标悬浮与之关联的线产生高亮效果
let lastLinkIds: string[] = [];
gantt.event(gantt.$task_data, 'mousemove', (e: MouseEvent) => {
const taskId = gantt.locate(e);
if (taskId && gantt.isTaskExists(taskId)) {
// 找到所有与当前任务关联的链接
const related = gantt.getLinks().filter(l => l.source==taskId || l.target==taskId);
const newLinkIds = related.map(l => l.id);
// 1) 移除上次高亮中,本次不再关联的
lastLinkIds
.filter(id => !newLinkIds.includes(id))
.forEach(id => {
const el = gantt.getLinkNode(id);
el && el.style.removeProperty('--dhx-gantt-link-background');
});
// 2) 给本次所有关联加高亮
newLinkIds
.filter(id => !lastLinkIds.includes(id))
.forEach(id => {
const el = gantt.getLinkNode(id);
el && el.style.setProperty('--dhx-gantt-link-background', '#ffb84d');
});
// 更新记录
lastLinkIds = newLinkIds;
return;
}
// 鼠标不在任务上时,清除所有高亮
lastLinkIds.forEach(id => {
const el = gantt.getLinkNode(id);
el && el.style.removeProperty('--dhx-gantt-link-background');
});
lastLinkIds = [];
});
// 在配置项中添加任务颜色模板(在 initGantt 函数中添加)
gantt.templates.task_class = function (start, end, task) {
// return task.time < 0 ? "positive-task" : "negative-task";
if (task.actual_end_date) {
if (task.time <= 0) {
return "tqTask"; // 提前
} else if (task.time > 0) {
return "cqTask"; // 超期
} else {
return "style0";
}
} else { // 如果没有实际结束时间,则根据当前时间判断任务状态
const currentTime = new Date();
if (currentTime > end) {
return "cqTask"; // 超期
} else {
return "style0";
}
}
};
// 设置任务条上展示的内容,参数task会返回当前任务的数据
gantt.templates.task_text = function (start, end, task) {
// console.log('task--111--------->', task)
return task.text + "-" + (task.responsible_person ? task.responsible_person.split('|')[1] : '');
};
// 遍历所有任务并展开父任务
gantt.eachTask(function (task) {
if (gantt.hasChild(task.id)) {
gantt.open(task.id); // 展开任务
}
});
gantt.attachEvent('onBeforeTaskChanged', (id, mode, task) => {
console.log('Before task changed:', task);
return true;
});
gantt.attachEvent('onAfterTaskDrag', (id, mode, e) => {
const task = gantt.getTask(id);
gantt.resetLayout();
});
gantt.attachEvent('onAfterTaskUpdate', (id, item) => {
console.log('After task update:', item)
let links = gantt.getLinks();
let end_date = item.end_date.getTime();
const endDateCache = {};
const updatedTasks = [];
links.forEach(link => {
const sourceId = link.source + '';
if (!endDateCache[sourceId]) {
endDateCache[sourceId] = gantt.getTask(sourceId).end_date.getTime();
}
});
let findlinks = links.filter((link) => link.source + '' === id + '');
findlinks.forEach((link) => {
let linktask = gantt.getTask(link.target);
let sourceList = links.filter((linkInfo) => linkInfo.target + '' === linktask.id + '');
let linkDate = end_date;
sourceList.forEach((s) => {
let sourceTask = gantt.getTask(s.source);
let time = sourceTask.end_date.getTime();
if (linkDate < time) {
linkDate = time;
}
});
let taskEndDate = new Date().getTime();
let days = linktask.duration * 3600000 * 24;
taskEndDate = linkDate + days;
let taskInfo = {
duration: linktask.duration,
id: linktask.id,
open: linktask.open,
parent: linktask.parent,
progress: linktask.progress,
text: linktask.text,
type: linktask.type,
color: linktask.color,
start_date: new Date(linkDate),
end_date: new Date(taskEndDate),
};
updatedTasks.push(taskInfo);
});
updatedTasks.forEach(taskInfo => {
gantt.updateTask(taskInfo.id, taskInfo);
});
return true;
});
};
// 监听数据的变化,可以在这里将demoData换成真实的数据,但是格式得和demoData一样
watch(
() => props.taskData,
(newValue) => {
if (newValue) {
const taskLinks = generateLinks(demoData.data);
const task = {
data: newValue,
demoData.data, // 测试
links: taskLinks
};
nextTick(() => {
loading.value = true
initGantt(task);
loading.value = false
});
}
}
);
// 销毁甘特图实例
onBeforeUnmount(() => {
gantt.clearAll()
})
</script>
<style scoped lang="scss">
.gantt-container {
width: 100%;
height: 100%;
}
.status-text {
font-weight: bold;
}
::v-deep(.positive) {
color: #67C23A !important;
}
::v-deep(.negative) {
color: #F56C6C !important;
}
:deep(.gantt_task_line) {
&.style0 {
border: 1px solid #409EFF;
background: #409EFF;
}
&.tqTask {
border: 1px solid #67C23A;
background: #67C23A;
}
&.cqTask {
border: 1px solid #F56C6C;
background: #F56C6C;
}
}
::v-deep .text_cell {
display: flex;
align-items: center;
}
::v-deep .text-cell-text {
overflow: hidden;
text-overflow: ellipsis;
}
/* 自定义图标颜色 */
::v-deep .text_cell img {
filter: hue-rotate(120deg); /* 调整颜色 */
}
.gantt_task_line {
&.highlighted {
border: 2px solid #ff4d4f;
background-color: rgba(255, 77, 79, 0.2);
}
}
.gantt_task_link.highlighted-link .gantt_line_wrapper div {
background-color: #ff4d4f;
box-shadow: 0 0 5px 0 #ff4d4f;
}
.gantt_task_link.highlighted-link .gantt_link_arrow_left,
.gantt_task_link.highlighted-link .gantt_link_arrow_right {
border-left-color: #ff4d4f !important;
border-right-color: #ff4d4f !important;
}
::v-deep(.gantt_task_link.highlight-link) {
--dhx-gantt-link-background: #ff4d4f !important;
}
</style>