使用vue3 + ts +dhtmlx 实现项目任务甘特图展示
支持拖拽,选择人员,优先级,开发状态,进度
效果图
完整代码
安装命令:npm i dhtmlx-gantt
javascript
<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 = true
gantt.config.drag_move = true
gantt.config.drag_resize = true
gantt.config.drag_links = true
// 允许任务排序
gantt.config.order_branch = true
gantt.config.order_branch_free = true
// 显示任务树
gantt.config.show_grid = true
gantt.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 = 40
gantt.i18n.setLocale('cn')
// gantt.config.autosize = true
// gantt.config.readonly = true
gantt.config.show_grid = true
gantt.config.show_task_tooltips = true
gantt.config.show_progress = true
gantt.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>