vue3+dhtmlx 甘特图真是案例
使用vue3 + ts +dhtmlx 实现项目任务甘特图展示
支持拖拽,选择人员,优先级,开发状态,进度
效果图
完整代码
安装命令:npm i dhtmlx-gantt
<template><div style="height: 100%; background-color: white"><div class="gantt-header"><el-button type="primary" @click="addNewTask"><el-icon><Plus /></el-icon>新建任务</el-button><el-button type="primary" @click="handVal">获取数据</el-button><!-- <el-button type="success" @click="addSubTaskToSelected"><el-icon><Plus /></el-icon>添加子任务</el-button> --></div><div ref="ganttRef" style="width: 100%; height: 600px"></div></div>
</template><script setup name="gantt-widget">
import { ref, reactive, onMounted, defineEmits } from 'vue'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'import { defineProps } from 'vue'
const props = defineProps({widgetObj: {type: Object,required: true,},
})const ganttRef = ref()
const tasks = ref({})//动态加载数据
const fetchData = () => {tasks.value = {data: [{id: 10,text: 'RFQ&项目启动',type: 'project',progress: 0.1,open: true,person: '张三',priority: 1,},{id: 12,text: '产品需求 #1.0.1',start_date: '02-01-2025',duration: 3,parent: 10,progress: 1,person: '张三',priority: 2,},{id: 13,text: '产品数据 #1.0.2',start_date: '05-01-2025',duration: 3,parent: 10,progress: 0.8,person: '张三',},{id: 14,text: '环境数据 #1.0.3',start_date: '08-01-2025',duration: 3,parent: 10,progress: 0,person: '张三',},{id: 15,text: '项目评估指令 #1.1',start_date: '11-01-2025',duration: 3,parent: 10,progress: 0,person: '张三',},{id: 16,text: '成立项目小组 #1.2.1',start_date: '12-01-2025',duration: 2,parent: 10,progress: 0,person: '张三',},{id: 17,text: '可行性评估 #1.2.2',start_date: '13-01-2025',duration: 3,parent: 10,progress: 0,person: '张三',},{id: 18,text: '输入评审 #1.2.3',start_date: '14-01-2025',duration: 2,parent: 10,progress: 0,person: '张三',},{id: 19,text: '初始产品方案 #1.2.4',start_date: '16-01-2025',duration: 2,parent: 10,progress: 0,person: '张三',},{id: 20,text: '产品设计&开发',type: 'project',progress: 0,person: '张三',open: false,},{id: 21,text: '设计输入信息管理#3.0.1',start_date: '18-01-2025',duration: 2,parent: 20,progress: 0,person: '张三',},{id: 22,text: '产品设计过往教训展开 #3.0.2',start_date: '20-01-2025',duration: 2,parent: 20,progress: 0,person: '张三',},{id: 23,text: '产品设计进度管理 #3.0.3',start_date: '22-01-2025',duration: 2,parent: 20,progress: 0,person: '张三',},{id: 24,text: '产品设计方案 #3.0.4',start_date: '24-01-2025',duration: 2,parent: 20,progress: 0,person: '张三',},{id: 25,text: '产品特殊特性识别 #3.0.5',start_date: '26-01-2025',duration: 2,parent: 20,progress: 0,person: '张三',},{id: 26,text: '产品设计方案评审 #3.0.6',start_date: '28-01-2025',duration: 2,parent: 20,progress: 0,person: '张三',},{id: 30,text: '过程设计&开发',type: 'project',progress: 0,person: '张三',open: false,},{id: 31,text: '场地规划 #5.0.1',start_date: '28-02-2025',duration: 3,parent: 30,progress: 0,person: '张三',},{id: 32,text: '场地评审 #5.0.2',start_date: '28-02-2025',duration: 3,parent: 30,progress: 0,person: '张三',},{id: 33,text: '过程检验标准 #5.0.3',start_date: '29-02-2025',duration: 3,parent: 30,progress: 0,person: '张三',},{id: 40,text: '产品&过程验证',type: 'project',open: false,progress: 0,person: '张三',},{id: 41,text: '量具重复再现性分析 #6.0.1',start_date: '29-02-2025',duration: 3,parent: 40,progress: 0,person: '张三',},{id: 42,text: '检具使用验收 #6.0.2',start_date: '01-03-2025',duration: 3,parent: 40,progress: 0,person: '张三',},{id: 43,text: '工装-厂外预验收 #6.1.1',start_date: '02-03-2025',duration: 3,parent: 40,progress: 0,person: '张三',},{id: 44,text: '设备-厂外预验收 #6.2.1',start_date: '03-03-2025',duration: 3,parent: 40,progress: 0,person: '张三',},{id: 45,text: '模具-厂外预验收 #6.3.1',start_date: '04-03-2025',duration: 3,parent: 40,progress: 0,person: '张三',},{id: 50,text: '过程验证',type: 'project',open: false,progress: 0,person: '张三',},{id: 51,text: '小批量试生产总结 #7.0.1',start_date: '28-04-2025',duration: 3,parent: 50,progress: 0,person: '张三',},{id: 52,text: '产品尺寸记录 #7.0.2',start_date: '29-04-2025',duration: 3,parent: 50,progress: 0,person: '张三',},{id: 52,text: '过程能力研究 #7.0.3',start_date: '30-04-2025',duration: 3,parent: 50,progress: 0,person: '张三',},{id: 60,text: '反馈,纠正和改进',type: 'project',open: false,progress: 0,person: '张三',},{id: 61,text: '模工检移交 #8.0.1',start_date: '28-05-2025',duration: 3,parent: 60,progress: 0,person: '张三',},{id: 62,text: '项目移交会议 #8.0.2',start_date: '29-05-2025',duration: 3,parent: 60,progress: 0,person: '张三',},{id: 63,text: '取得量产交付计划 #8.1.1',start_date: '30-05-2025',duration: 3,parent: 60,progress: 0,person: '张三',},],links: [{ id: 10, source: 12, target: 13, type: 1 },{ id: 11, source: 13, target: 14, type: 1 },{ id: 12, source: 14, target: 15, type: 1 },]}
}// 添加子任务到选中的任务
const addSubTaskToSelected = () => {const selectedTask = gantt.getSelectedId()if (selectedTask) {addSubTask(selectedTask)} else {ElMessage.warning('请先选择一个任务')}
}// 添加新任务
const addNewTask = (parentId = null) => {const newTask = {id: gantt.uid(),text: '新任务',start_date: new Date(),duration: 1,progress: 0, // 默认状态为未开始person: '张三', // 默认负责人parent: parentId,priority: 2 // 默认优先级为中}gantt.addTask(newTask)
}// 添加子任务
const addSubTask = (parentId) => {addNewTask(parentId)
}
const handVal = () => {// 获取甘特图所有任务数据const data = gantt.serialize()console.log('甘特图数据:', data)// 获取选中的任务const selectedTaskId = gantt.getSelectedId()if (selectedTaskId) {const selectedTask = gantt.getTask(selectedTaskId)console.log('选中的任务:', selectedTask)}
}//初始化配置
const initGantt = () => {gantt.plugins({ tooltip: true, // 鼠标悬停显示任务信息quick_info: true // 快速信息})// 隐藏右侧日期列表// gantt.config.show_chart = false// 添加右键菜单gantt.config.context_menu = {items: {add: {text: "添加任务",icon: "gantt_add",onclick: function (id) {addNewTask()}},add_sub: {text: "添加子任务",icon: "gantt_add",onclick: function (id) {addSubTask(id)}},delete: {text: "删除",icon: "gantt_delete",onclick: function (id) {gantt.deleteTask(id)}}}}// 允许拖放gantt.config.drag_project = truegantt.config.drag_move = truegantt.config.drag_resize = truegantt.config.drag_links = true// 允许任务排序gantt.config.order_branch = truegantt.config.order_branch_free = true// 显示任务树gantt.config.show_grid = truegantt.config.show_task_cells = true// 隐藏任务之间的连线gantt.config.show_links = false// 汉化窗口gantt.locale.labels = {dhx_cal_today_button: '今天',day_tab: '日',week_tab: '周',month_tab: '月',new_event: '新建日程',icon_save: '保存',icon_cancel: '关闭',icon_details: '详细',icon_edit: '编辑',icon_delete: '删除',confirm_closing: '请确认是否撤销修改!', //Your changes will be lost, are your sure?confirm_deleting: '是否删除计划?',section_description: '描述:',section_time: '时间范围:',section_type: '类型:',section_text: '计划名称:',section_test: '测试:',section_projectClass: '项目类型:',taskProjectType_0: '项目任务',taskProjectType_1: '普通任务',section_head: '负责人:',section_priority: '优先级:',taskProgress: '任务状态',taskProgress_0: '未开始',taskProgress_1: '进行中',taskProgress_2: '已完成',taskProgress_3: '已延期',taskProgress_4: '搁置中',section_template: 'Details',/* grid columns */column_text: '计划名称',column_start_date: '开始时间',column_duration: '持续时间',column_add: '',column_priority: '难度',/* link confirmation */link: '关联',confirm_link_deleting: '将被删除',message_ok: '确定',message_cancel: '取消',link_start: ' (开始)',link_end: ' (结束)',type_task: '任务',type_project: '项目',type_milestone: '里程碑',minutes: '分钟',hours: '小时',days: '天',weeks: '周',months: '月',years: '年',}// 格式化日期gantt.locale.date = {month_full: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月',],month_short: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月',],day_full: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六',],day_short: ['星期日','星期一','星期二','星期三','星期四','星期五','星期六',],}// 当task的长度改变时,自动调整图表坐标轴区间用于适配task的长度gantt.config.fit_tasks = true// 定义时间格式gantt.config.scales = [{ unit: 'month', step: 1, date: '%F, %Y' },{ unit: 'day', step: 1, date: '%j, %D' },]// gantt.config.scale_height = 80// gantt.config.row_height = 60// gantt.config.bar_height = 40gantt.i18n.setLocale('cn')// gantt.config.autosize = true// gantt.config.readonly = truegantt.config.show_grid = truegantt.config.show_task_tooltips = truegantt.config.show_progress = truegantt.config.branches = {open: 'open',closed: 'closed',}gantt.templates.tooltip_text = (start, end, task) => `<div><div>任务:${task.text}</div><div>开始时间:${formatDate(task.start_date, '{y}-{m}-{d}')}</div><div>结束时间:${formatDate(task.end_date, '{y}-{m}-{d}')}</div><div>进度:${task.progress * 100}%</div></div>`gantt.config.columns = [{name: 'text',label: '任务名称',width: '250',tree: true,align: 'left',},{ name: 'start_date', label: '起始时间', width: '100', align: 'center' },{ name: 'duration', label: '持续时间', width: '80', align: 'center' },{name: 'progress',label: '进度',width: '100',align: 'center',template: function (obj) {return obj.progress * 100 + '%'},},{ name: 'person', label: '负责人', width: '80', align: 'center' },{name: 'status',label: '状态',width: '100',align: 'center',template: function (obj) {const status = getTaskStatus(obj)return `<span class="task-status ${status.class}">${status.text}</span>`}},{name: 'priority',label: '优先级',width: '80',align: 'center',template: function (obj) {const priority = obj.priority return `<span class="task-priority priority-${priority}">${getPriorityText(priority)}</span>` // return priority}},{name: 'add',label: '操作',width: '100',align: 'center',template: function (obj) {return `<div class="task-actions"><el-button type="primary" size="small" onclick="gantt.$vue.addSubTask(${obj.id})"><el-icon><Plus /></el-icon>添加子任务</el-button></div>`}}]gantt.config.lightbox_zindex = 10000// 定义可选的人员列表gantt.serverList("person", [{ key: "张三", label: "张三" },{ key: "李四", label: "李四" },{ key: "王五", label: "王五" },{ key: "赵六", label: "赵六" }]);// 定义任务状态列表gantt.serverList("status", [{ key: 0, label: "未开始" },{ key: 0.5, label: "进行中" },{ key: 1, label: "已完成" }]);// 定义优先级列表gantt.serverList("priority", [{ key: 1, label: "高" },{ key: 2, label: "中" },{ key: 3, label: "低" }]);// 添加弹窗属性gantt.config.lightbox.sections = [{name: 'description',height: 70,map_to: 'text',type: 'textarea',focus: true,},{ name: 'type', type: 'typeselect', map_to: 'type' },{ name: 'time', type: 'duration', map_to: 'auto' },{name: 'priority',height: 30,map_to: 'priority',type: 'select',label: '优先级',options: gantt.serverList("priority")},{name: 'person',height: 30,map_to: 'person',type: 'select',label: '负责人',options: gantt.serverList("person")},{name: 'progress',height: 30,map_to: 'progress',type: 'select',label: '状态',options: gantt.serverList("status")}]// 获取优先级文本const getPriorityText = (priority) => {const priorityList = gantt.serverList("priority")const found = priorityList.find(item => item.key == priority)return found ? found.label : ''}// 获取任务状态const getTaskStatus = (task) => {if (task.progress === 1) {return { text: '已完成', class: 'status-completed' }} else if (task.progress > 0) {return { text: '进行中', class: 'status-in-progress' }} else {return { text: '未开始', class: 'status-not-started' }}}// 将 Vue 实例挂载到 gantt 对象上,以便在模板中访问gantt.$vue = {addSubTask: addSubTask,getTaskStatus: getTaskStatus,getPriorityText: getPriorityText}// 初始化gantt.init(ganttRef.value)// 清空旧数据gantt.clearAll()// 数据解析gantt.parse(tasks.value)
}const formatDate = (date, format) => {if (!date) return ''const d = new Date(date)const year = d.getFullYear()const month = String(d.getMonth() + 1).padStart(2, '0')const day = String(d.getDate()).padStart(2, '0')return format.replace('{y}', year).replace('{m}', month).replace('{d}', day)
}onMounted(() => {fetchData()initGantt()
})
</script><style lang="scss" scoped>
.gantt_cal_light {z-index: 9999 !important;
}.gantt_cal_cover {z-index: 10000 !important;
}.gantt-header {padding: 10px;border-bottom: 1px solid #ebeef5;margin-bottom: 10px;
}:deep(.task-actions) {.el-button {padding: 4px 8px;font-size: 12px;}
}:deep(.task-status) {padding: 2px 8px;border-radius: 4px;font-size: 12px;&.status-completed {background-color: #f0f9eb;color: #67c23a;}&.status-in-progress {background-color: #fdf6ec;color: #e6a23c;}&.status-not-started {background-color: #f4f4f5;color: #909399;}
}:deep(.task-priority) {padding: 2px 8px;border-radius: 4px;font-size: 12px;&.priority-1 {background-color: #fef0f0;color: #f56c6c;}&.priority-2 {background-color: #fdf6ec;color: #e6a23c;}&.priority-3 {background-color: #f0f9eb;color: #67c23a;}
}
</style>