官方文档:如何从 dhtmlx甘特甘特图文档开始
这个组件比较古老,对vue的兼容性很不友好。以下是一些开发心得:
1.安装dhtmlx 甘特图
npm install @dhtmlx/trial-vue-gantt
2.创建Gantt.vue 组件
<template>
<div class="xh-radio-switch">
<el-select v-model="test" @change="radioSwitchChange">
<el-option v-for="item in options" :key="item.val" :label="item.name" :value="item.val"></el-option>
</el-select>
</div>
<div style="width: 100%;" ref="ganttRef"></div>
</template>
<script setup lang="ts">
import { onMounted, ref, nextTick } from 'vue'
import { gantt } from 'dhtmlx-gantt'
import { ElMessage, ElMessageBox, dayjs } from 'element-plus';
const emits = defineEmits<{
(e: 'openDetail', id: number): void
(e: 'openShow', id: number): void
(e: 'handle', success: true): void
}>()
const test = ref('')
const options = ref(
[
{
name: '日',
val: 'day'
},
{
name: '周',
val: 'week'
},
{
name: '月',
val: 'month'
},
{
name: '季度',
val: 'quarter'
},
]
)
const props = defineProps<{
// 任务对象
tasks?: any
// 显示列设置
columns?: Array<any>
// 显示单位
scaleUnit?: {
type: String,
default: 'day' // “minute”, “hour”, “day”, “week”, “quarter”, “month”, “year”
},
// 时间显示格式
dateScale?: {
type: String,
default: '%Y-%m-%d'
}
}>()
// 挂载ref
const ganttRef = ref()
onMounted(() => {
// 清空之前的配置
gantt.clearAll()
// 默认配置
gantt.config.xml_date = '%Y-%m-%d'
gantt.i18n.setLocale('cn') // 设置中文
gantt.config.readonly = false // 设置为可编辑
gantt.config.autosize = true//自适应尺寸
gantt.config.autofit = true// 表格列宽自适应
gantt.config.autoscroll = true// 把任务或者连线拖拽到浏览器屏幕外时,自动触发滚动效果
gantt.config.drag_progress = false//取消任务进度条进度拖动
//时间栏配置
gantt.config.scales = [
{ unit: 'month', step: 1, format: '%Y年%m月' },
{ unit: 'day', step: 1, format: '%m/%d' },
]
gantt.config.scale_height = 60
//更改树状的图标
gantt.templates.grid_open = (item: any) => {
props.tasks.data.forEach((item: any) => {
changeItemStyle(item)
})
return "<div data-icon='" + (item.$open ? "close" : "open") + "' class='gantt_tree_icon gantt_" +
(item.$open ? "close" : "open") + "'></div>"
}
//更改父项图标
gantt.templates.grid_folder = (item: any) => {
return ""
}
//更改子项图标
gantt.templates.grid_file = (item: any) => {
return ""
}
//自定义配置左侧表格列
gantt.config.columns = [
{ name: "text", label: "事务名称", align: "left", tree: true, width: 260 },
{
name: "status", label: " ", width: 80, align: "center", template: function (task: any) {
switch (task.status) {
case 1:
return (
`<div data-show class="progress progress-success">已完成</div>
<div data-show class="avatar"><img src="${task.avatar}" /></div>`
);
break;
case 2:
return (
`<div data-show class="progress progress-todo">待 办</div>
<div data-show class="avatar"><img src="${task.avatar}" /></div>`
);
break;
case 3:
return (
`<div data-show class="progress progress-doing">进行中</div>
<div data-show class="avatar"><img src="${task.avatar}" /></div>`
);
break;
break;
default:
return (
`<div data-show class="progress progress-todo">待办</div>
<div data-show class="avatar"><img src="${task.avatar}" /></div>`
);
break;
}
}
},
{
name: "add_item", label: "", width: 44, template: () => {
return `<div ref="addRef" id="button" class="add-item" data-action="add"></div>`
}
}
]
//自定义右侧任务栏颜色
gantt.templates.task_class = (start: string, end: string, task: any) => {
if (task.parent) return 'task-child'
return 'task-' + task.id
}
//获取任务时
gantt.attachEvent("onTaskLoading", (task: any) => {
changeItemStyle(task)
return true
})
//打开分支时候
gantt.attachEvent("onTaskOpened", function () {
props.tasks.data.forEach((item: any) => {
changeItemStyle(item)
})
});
//关闭分支时
gantt.attachEvent("onTaskClosed", function () {
props.tasks.data.forEach((item: any) => {
changeItemStyle(item)
})
});
//拖动任务时
gantt.attachEvent("onTaskDrag", function (id: number, mode: any, task: any, original: any) {
changeItemStyle(task)
});
//甘特图 任务栏更新后 可获取当前任务对象
gantt.attachEvent("onAfterTaskUpdate", async function (id: number, item: any) {
changeItemStyle(item)
const params_start = {
issues_id: id,
name: 'start_date',
values: dayjs(item.start_date).format('YYYY-MM-DD')
}
const params_end = {
issues_id: id,
name: 'end_date',
values: dayjs(item.end_date).format('YYYY-MM-DD')
}
const res1 = await xxxxx(params_start)
if (res1.code !== 0) return ElMessage.error(res1.message)
const res2 = await xxxxx(params_end)
if (res2.code !== 0) return ElMessage.error(res2.message)
});
//甘特图 点击事件
gantt.attachEvent("onTaskClick", function (id: number, e: any) {
//点击显示更多按钮
const buttonAdd = e.target.closest("[data-action]")
if (buttonAdd) {
emits('openDetail', id)
}
//点击状态按钮
const buttonShow = e.target.closest("[data-show]")
if (buttonShow) {
emits('openShow', id)
}
//点击展开/收起按钮
const buttonIcon = e.target.closest("[data-icon]")
if (buttonIcon) {
const active = buttonIcon.getAttribute("data-icon")
switch (active) {
case 'open':
gantt.open(id)
break;
case 'close':
gantt.close(id)
break;
default:
break;
}
}
})
//取消默认双击事件
gantt.attachEvent("onTaskDblClick", function (id: number, e: any) {
return false
});
// 处理连线完成事件
let linkAddHandler = null
linkAddHandler = async function (id: number, item: any) {
props.tasks.data.forEach((item: any) => {
changeItemStyle(item)
})
const params = {
issues_id: Number(item.source),
relate_type: 1,
relate_issues_id: Number(item.target)
}
const res = await xxxxxxxx(params)
if (res.code !== 0) return ElMessage.error(res.message)
ElMessage.success(res.data)
emits('handle', true)
}
gantt.attachEvent("onAfterLinkAdd", linkAddHandler)
// 在需要取消监听时执行
gantt.detachEvent("onAfterLinkAdd", linkAddHandler)
// 处理连接线双击事件
let linkDblClickHandler = null
linkDblClickHandler = function (id: number, e: any) {
ElMessageBox.confirm(
'此操作不可恢复',
'删除此关联事务',
{
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
const res = await xxxxxx(id)
if (res.code !== 0) return ElMessage.error(res.message)
ElMessage.success(res.data)
gantt.deleteLink(id)
emits('handle', true)
})
}
gantt.attachEvent('onLinkDblClick', linkDblClickHandler);
// 初始化甘特图
gantt.init(ganttRef.value)
// 渲染数据
gantt.parse(props.tasks)
})
//----------------自定义函数
const backgroundColorFun = (number1: number, number2: number) => {
return `linear-gradient(to right, #68d390 ${0}%, #68d390 ${number1}%,#0085ff ${number1}%,#0085ff ${number2}%, #c4c4c4 ${number2}%, #c4c4c4 ${100}%)`
}
const changeItemStyle = (task: any) => {
if (!task.parent) {
const total = task.done + task.backlog + task.under_way + task.lay_aside
if (total) {
const percentDone = (task.done / total) * 100
const percentUnderWay = (task.under_way / total) * 100
nextTick(() => {
const taskClass = document.querySelector(`.task-${task.id}`) as HTMLElement
if (taskClass) {
taskClass.style.background = `${backgroundColorFun(percentDone, percentDone + percentUnderWay)}`
taskClass.style.borderRadius = '50px'
}
})
} else {
nextTick(() => {
const taskClass = document.querySelector(`.task-${task.id}`) as HTMLElement
if (taskClass) {
taskClass.style.background = `${backgroundColorFun(0, 0)}`
taskClass.style.borderRadius = '50px'
}
})
}
}
}
//日 周 月 季度 切换
const switchToDay = () => {
gantt.config.scale_unit = "day"
gantt.config.step = 1
gantt.render()
}
const switchToWeek = () => {
gantt.config.scale_unit = "week"
gantt.config.step = 1
gantt.render()
}
const switchToMonth = () => {
gantt.config.scale_unit = "month"
gantt.config.step = 1
gantt.render()
}
const switchToQuarter = () => {
gantt.config.scale_unit = "month"
gantt.config.step = 3
gantt.render()
}
const radioSwitchChange = (e: string) => {
switch (e) {
case 'day':
switchToDay()
break;
case 'week':
switchToWeek()
break;
case 'month':
switchToMonth()
break;
case 'quarter':
switchToQuarter()
break;
default:
break;
}
}
</script>
<style lang="less">
.xh-radio-switch {
display: flex;
justify-content: flex-end;
}
.add-item {
cursor: pointer;
width: 20px;
height: 20px;
background-size: cover;
background-image: url(xxxxxx)
}
/* 进度盒 */
.progress {
display: flex;
justify-content: center;
align-items: center;
width: 43px;
height: 20px;
border-radius: 5px;
font-size: 12px;
padding: 3px;
&-success {
background: rgba(63, 200, 114, 0.15);
color: #67d390;
}
&-todo {
background: #dfe1e6;
color: #84526e;
}
&-doing {
background: rgba(0, 133, 255, 0.15);
color: #0085FF;
}
}
/**用户头像 */
.avatar {
display: flex;
img {
width: 24px;
height: 24px;
border-radius: 50%;
margin-left: 10px;
}
}
/** 左侧树状 新增按钮 */
.gantt_add,
.gantt_grid_head_add {
background-image: url(xxxxxxxx) !important;
opacity: .9 !important;
}
.gantt_tree_content {
display: flex;
justify-content: center;
align-items: center;
}
/* 定义任务栏颜色 */
.gantt_task_line {
border-color: rgba(0, 0, 0, 0.25);
}
.gantt_task_line .gantt_task_progress {
background-color: rgba(0, 0, 0, 0.25);
}
/* task */
.gantt_task_line {
.task-5 {
border-radius: 50px;
// background: v-bind(backgroundColor);
}
}
.gantt_task_line.task {
border-radius: 50px;
// background: v-bind(backgroundColor);
}
.gantt_task_line.task-child {
background-color: #ffe176;
border-radius: 50px;
}
.gantt_task_line .gantt_task_progress {
background-color: #68d390;
}
.gantt_task_line.task .gantt_task_content {
color: #fff;
}
</style>
<style>
@import "/node_modules/dhtmlx-gantt/codebase/dhtmlxgantt.css"
</style>
3.在vue中使用
<template>
<Gantt v-if="ganttTasks.data.length" :tasks="ganttTasks" :columns="ganttColumns" style="height: 500px;" @handle="getGanttList()"></Gantt>
</template>
<script setup lang="ts">
import Gantt from './components/Gantt.vue'
//-----------------------------------生命周期
onMounted(() => {
Promise.all([getGanttList()])
})
//-----------------------------------甘特图
const ganttData = ref<xxxx[]>([]) //后端数据
const linkData = ref<any[]>([])//相关事务数据
const ganttKeywords = ref<xxxx>({
synergy_type_id: 1 //事务类型
})
const ganttTasks = ref<any>({
//数据
data: [],
//链接
links: []
})
const ganttColumns = ref([
{ align: 'left', name: 'text', label: ' ', tree: true },
])
const getGanttList = async () => {
const res = await xxxxxxxxxx(ganttKeywords.value)
if (res.code !== 0) return ElMessage.error(res.message)
ganttData.value = res.data
ganttTasks.value.data = ganttData.value.map((item: xxxx) => {
//这里的返回值 就是gantt回调函数中的task形参
return {
id: item.id, parent: item.parent_id, text: 'XH-' + item.id + ' ' + item.name, start_date: item.start_date, end_date: item.end_date, progress: item.completion_progress, status: item.status, avatar: item.principal.avatar || defaultImage, level: item.level, done: item.done, backlog: item.backlog, under_way: item.under_way, lay_aside: item.lay_aside
}
})
linkData.value = ganttData.value.filter(item => item.issues_relation && item.issues_relation.length)
if (linkData.value.length) {
ganttTasks.value.links = linkData.value.map(item => item.issues_relation).flat(Infinity).map((item: xxxx) => {
return {
id: item.id, source: item.synergy_issues_id, target: item.relate_issues_id, type: '0'
}
})
}
}
</script>