vue3 甘特图(四):甘特图进度条拆分
目前项目采取有两种拆分进度条的方式。单个进度条展示多维度数据,一个总任务条下有多个子进程展示。
1.1 单个进度条展示多维度数据(不关联时间),如图
此方法是通过设置单个进度条上内容,以一个进度条为容器,修改内容(即task_text内容)展示多维度的数据,比如当前项目需要展示项目消耗的工时/预估工时、进度、告警事件、滞后事件,将其分割成固定的多少段(不关联时间)直接展示数据。
核心逻辑(直接替换进度条内容):
gantt.templates.task_text = function (start, end, task) {
if (task.projectStatus == '暂无任务') {
return `
<div class="project-bar no-data">
<div class="no">暂无任务</div>
</div>
`
} else {
let progressState = ''
let progressPerson = ''
// 超前 滞后 完成 暂无任务 延期
if (task.projectStatus == '滞后' || task.projectStatus == '延期') {
progressState = 'lag'
let warnDiv = '' //告警事件 部门
let errorDic = task.functionName //滞后严重功能
let _index = 1
let warnIcon = ''
let errorIcon = ''
for (const key in task.projectMap) {
if (_index <= 3) {
warnDiv += `${_index}.${key}(${task.projectMap[key]}) `
_index += 1
}
}
if (Object.keys(task.projectMap).length > 0) {
warnIcon = `<div class="icon-box">
<div class="warn-icon"></div>
<div class="icon-line warn"></div>
</div>`
}
if (errorDic) {
errorIcon = `<div class="icon-box">
<div class="error-icon"></div>
<div class="icon-line error"></div>
</div>`
}
// 判断是否因为时间过短 导致图上文字显示不全 采用钓鱼线引出
let inLine = data.topTime == 'year' ? 'no-in-line' : 'in-line'
if (
new Date(task.endTime).getTime() - new Date(task.startTime).getTime() <
3600 * 1000 * 24 * 30
) {
inLine = 'no-in-line'
}
progressPerson = ` <div class="project-warn ${inLine}">
${warnIcon}
<div class="war-des">${warnDiv}</div>
</div>
<div class="project-error ${inLine}">
${errorIcon}
<div class="error-des">${errorDic}</div>
</div>
`
} else {
progressState = 'normal'
}
return `
<div class="project-bar">
<div class="project-all-time">${task.projectUsedTime}/${
task.projectTotalTime
}</div>
<div class="project-progress ${progressState}">${
(task.projectProgress * 100).toFixed(1) + '%'
}</div>
${progressPerson}
</div>
`
}
}
完整代码:
<section class="my-gantt">
<div class="time-box">
<el-radio-group v-model="data.timeState" @change="changeTime">
<el-radio-button
v-for="(time, t_index) in data.timeList"
:key="t_index"
:label="time.code"
size="default"
border
>{{ time.name }}</el-radio-button
>
</el-radio-group>
</div>
<div id="gantt_here" class="gantt-container"></div>
</section>
</template>
<script setup>
import { reactive, toRefs, onBeforeMount, onMounted, watchEffect, defineExpose } from 'vue'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
// 此方法是通过设置单个进度条上内容,以一个进度条为容器,修改内容(即task_text内容)展示多维度的数据,比如当前项目需要展示项目消耗的工时/预估工时、进度、告警事件、滞后事件,将其分割成固定的多少段(不关联时间)直接展示数据
const data = reactive({
timeList: [
// {
// name: "周",
// code: "week",
// },
{
name: '月',
code: 'month'
},
{
name: '季',
code: 'quarter'
},
{
name: '年',
code: 'year'
}
],
timeState: 'month',
demoData: {
data: [
{
id: 520,
projectName: '项目1',
startTime: '2023-09-25',
endTime: '2023-10-31',
showEndTime: '2023-11-01',
projectStatus: '暂无任务',
projectProgress: 0,
projectRatio: '',
projectTotalTime: 0,
projectUsedTime: 0,
functionName: '',
xmdj: '2',
cityName: '成都',
name: '1',
projectMap: {},
parent: 0,
start_date: '2023-09-24 16:00:00.000',
end_date: '2023-10-31 16:00:00.000',
progress: 0.5,
duration: 37
},
{
id: 517,
projectName: '项目2',
startTime: '2023-09-18',
endTime: '2023-10-23',
showEndTime: '2023-10-24',
projectStatus: '暂无任务',
projectProgress: 0,
projectRatio: '',
projectTotalTime: 0,
projectUsedTime: 0,
functionName: '',
xmdj: '0',
cityName: '深圳',
name: '2',
projectMap: {},
parent: 0,
start_date: '2023-09-17 16:00:00.000',
end_date: '2023-10-23 16:00:00.000',
progress: 0.2
},
{
id: 505,
projectName: '项目3',
startTime: '2023-09-04',
endTime: '2023-09-30',
showEndTime: '2023-10-01',
projectStatus: '滞后',
projectProgress: 0.76,
projectRatio: 0.12,
projectTotalTime: 3267.6,
projectUsedTime: 2477.7,
functionName: '现状还原',
xmdj: '3',
cityName: '成都',
name: '3',
projectMap: {},
parent: 0,
start_date: '2023-09-03 16:00:00.000',
end_date: '2023-09-30 16:00:00.000',
progress: 0.1
},
{
id: 508,
projectName: '项目4',
startTime: '2023-09-04',
endTime: '2023-10-20',
showEndTime: '2023-10-21',
projectStatus: '滞后',
projectProgress: 0.57,
projectRatio: 0.04,
projectTotalTime: 3582.5,
projectUsedTime: 2033.2,
functionName: '测试功能',
xmdj: '1',
cityName: '成都',
name: '4',
projectMap: {},
parent: 0,
start_date: '2023-09-03 16:00:00.000',
end_date: '2023-10-20 16:00:00.000',
progress: 0.15
},
{
id: 511,
projectName: '项目5',
startTime: '2023-09-01',
endTime: '2023-10-31',
showEndTime: '2023-11-01',
projectStatus: '滞后',
projectProgress: 0.07,
projectRatio: 0.03,
projectTotalTime: 2150.5,
projectUsedTime: 140,
functionName: '悬浮球',
xmdj: '1',
cityName: '成都',
name: '5',
projectMap: {},
parent: 0,
start_date: '2023-07-31 16:00:00.000',
end_date: '2023-10-31 16:00:00.000',
progress: 0.28
},
{
id: 507,
projectName: '项目6',
startTime: '2023-08-28',
endTime: '2023-10-01',
showEndTime: '2023-10-02',
projectStatus: '滞后',
projectProgress: 0.48,
projectRatio: 0.21,
projectTotalTime: 4957,
projectUsedTime: 2367,
functionName: '产品原型图',
xmdj: '1',
cityName: '三亚',
name: '6',
projectMap: {
部门1: 1
},
parent: 0,
start_date: '2023-07-27 16:00:00.000',
end_date: '2023-10-01 16:00:00.000',
progress: 0.33
},
{
id: 1,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '超前',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
},
{
id: 2,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '滞后',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
},
{
id: 3,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '滞后',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
},
{
id: 4,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '滞后',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
},
{
id: 5,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '滞后',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
},
{
id: 6,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '滞后',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
},
{
id: 7,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '滞后',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
},
{
id: 8,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '滞后',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
},
{
id: 9,
projectName: '项目7',
startTime: '2023-08-28',
endTime: '2023-10-25',
showEndTime: '2023-10-26',
projectStatus: '滞后',
projectProgress: 0.27,
projectRatio: 0.15,
projectTotalTime: 2027.5,
projectUsedTime: 557,
functionName: '测量工具',
xmdj: '1',
cityName: '佛山',
name: '7',
projectMap: {},
parent: 0,
start_date: '2023-06-27 16:00:00.000',
end_date: '2023-10-25 16:00:00.000',
progress: 0.67
}
]
}
})
const zoomConfig = {
levels: [
{
name: 'day',
scale_height: 60,
scales: [{ unit: 'day', step: 1, format: '%d %M' }]
},
{
name: 'week',
scale_height: 60,
scales: [
{
unit: 'week',
step: 1,
format: function (date) {
let dateToStr = gantt.date.date_to_str('%m-%d')
let endDate = gantt.date.add(date, -6, 'day')
let weekNum = gantt.date.date_to_str('%W')(date) //第几周
return dateToStr(endDate) + ' 至 ' + dateToStr(date)
}
},
{
unit: 'day',
step: 1,
format: '%d', // + "周%D"
css: function (date) {
if (date.getDay() == 0 || date.getDay() == 6) {
return 'day-item weekend weekend-border-bottom'
} else {
return 'day-item'
}
}
}
]
},
{
name: 'month',
scale_height: 60,
min_column_width: 18,
scales: [
{ unit: 'month', format: '%Y-%m' },
{
unit: 'day',
step: 1,
format: '%d',
css: function (date) {
if (date.getDay() == 0 || date.getDay() == 6) {
return 'day-item weekend weekend-border-bottom'
} else {
return 'day-item'
}
}
}
]
},
{
name: 'quarter',
height: 60,
min_column_width: 110,
scales: [
{
unit: 'quarter',
step: 1,
format: function (date) {
let yearStr = new Date(date).getFullYear() + '年'
let dateToStr = gantt.date.date_to_str('%M')
let endDate = gantt.date.add(gantt.date.add(date, 3, 'month'), -1, 'day')
return yearStr + dateToStr(date) + ' - ' + dateToStr(endDate)
}
},
{
unit: 'week',
step: 1,
format: function (date) {
let dateToStr = gantt.date.date_to_str('%m-%d')
let endDate = gantt.date.add(date, 6, 'day')
let weekNum = gantt.date.date_to_str('%W')(date)
return dateToStr(date) + ' 至 ' + dateToStr(endDate)
}
}
]
},
{
name: 'year',
scale_height: 50,
min_column_width: 150,
scales: [
{ unit: 'year', step: 1, format: '%Y年' },
{ unit: 'month', format: '%Y-%m' }
]
}
]
}
//初始化甘特图
const initGantt = () => {
let dateToStr = gantt.date.date_to_str('%Y.%m.%d')
gantt.config.grid_width = 350
gantt.config.add_column = false //添加符号
//时间轴图表中,如果不设置,只有行边框,区分上下的任务,设置之后带有列的边框,整个时间轴变成格子状。
gantt.config.autofit = false
gantt.config.row_height = 60
gantt.config.bar_height = 34
// gantt.config.fit_tasks = true //自动延长时间刻度,以适应所有显示的任务
gantt.config.auto_types = true //将包含子任务的任务转换为项目,将没有子任务的项目转换回任务
gantt.config.xml_date = '%Y-%m-%d' //甘特图时间数据格式
gantt.config.readonly = true //是否只读
gantt.templates.task_text = function (start, end, task) {
if (task.projectStatus == '暂无任务') {
return `
<div class="project-bar no-data">
<div class="no">暂无任务</div>
</div>
`
} else {
let progressState = ''
let progressPerson = ''
// 超前 滞后 完成 暂无任务 延期
if (task.projectStatus == '滞后' || task.projectStatus == '延期') {
progressState = 'lag'
let warnDiv = '' //告警事件 部门
let errorDic = task.functionName //滞后严重功能
let _index = 1
let warnIcon = ''
let errorIcon = ''
for (const key in task.projectMap) {
if (_index <= 3) {
warnDiv += `${_index}.${key}(${task.projectMap[key]}) `
_index += 1
}
}
if (Object.keys(task.projectMap).length > 0) {
warnIcon = `<div class="icon-box">
<div class="warn-icon"></div>
<div class="icon-line warn"></div>
</div>`
}
if (errorDic) {
errorIcon = `<div class="icon-box">
<div class="error-icon"></div>
<div class="icon-line error"></div>
</div>`
}
// 判断是否因为时间过短 导致图上文字显示不全 采用钓鱼线引出
let inLine = data.topTime == 'year' ? 'no-in-line' : 'in-line'
if (
new Date(task.endTime).getTime() - new Date(task.startTime).getTime() <
3600 * 1000 * 24 * 30
) {
inLine = 'no-in-line'
}
progressPerson = ` <div class="project-warn ${inLine}">
${warnIcon}
<div class="war-des">${warnDiv}</div>
</div>
<div class="project-error ${inLine}">
${errorIcon}
<div class="error-des">${errorDic}</div>
</div>
`
} else {
progressState = 'normal'
}
return `
<div class="project-bar">
<div class="project-all-time">${task.projectUsedTime}/${
task.projectTotalTime
}</div>
<div class="project-progress ${progressState}">${
(task.projectProgress * 100).toFixed(1) + '%'
}</div>
${progressPerson}
</div>
`
}
}
gantt.config.columns = [
{
name: 'projectName',
label: '项目名称',
tree: true,
width: '*'
},
{
name: '',
label: '时间',
align: 'center',
width: 150,
template: function (item) {
return `<div class="project-time">${
dateToStr(item.start_date) + '-' + item.endTime.replace(/[-]/g, '.')
}</div>`
}
}
]
gantt.i18n.setLocale('cn') //设置语言
gantt.init('gantt_here') //初始化
gantt.parse(data.demoData) //填充数据
scrollInit()
gantt.ext.zoom.init(zoomConfig) //配置初始化扩展
gantt.ext.zoom.setLevel('month') //切换到指定的缩放级别
}
//拖拽滚动
const scrollInit = () => {
const nav = document.querySelectorAll('.gantt_task')[0]
const parNav = document.querySelectorAll('.gantt_hor_scroll')[0]
parNav.scrollLeft = 0
let flag
let downX
let scrollLeft
nav.addEventListener('mousedown', function (event) {
flag = true
downX = event.clientX // 获取到点击的x下标
scrollLeft = this.scrollLeft // 获取当前元素滚动条的偏移量
})
nav.addEventListener('mousemove', function (event) {
if (flag) {
let moveX = event.clientX
let scrollX = moveX - downX
parNav.scrollLeft = scrollLeft - scrollX
}
})
// 鼠标抬起停止拖动
nav.addEventListener('mouseup', function () {
flag = false
})
// 鼠标离开元素停止拖动
nav.addEventListener('mouseleave', function (event) {
flag = false
})
}
const changeTime = () => {
gantt.ext.zoom.setLevel(data.timeState)
}
onBeforeMount(() => {})
onMounted(() => {
initGantt()
})
watchEffect(() => {})
defineExpose({
...toRefs(data)
})
</script>
<style scoped lang="scss">
.my-gantt {
height: 800px;
width: 100vw;
.time-box {
text-align: center;
margin-bottom: 20px;
}
::v-deep .gantt-container {
border-radius: 8px 0px 0px 8px;
overflow: hidden;
height: 100%;
.no-data {
background: #d4d4d4;
.no {
padding: 0 10px;
border-radius: 4px;
box-shadow: 4px 0px 4px 0px rgba(0, 0, 0, 0.25);
}
}
.gantt_grid_head_projectText {
line-height: 2.8;
}
.gantt_task_line {
background: #86afff;
}
.gantt_side_content,
.gantt_task_content,
.gantt_task_progress {
overflow: visible !important;
}
.project-progress-marker {
position: absolute;
z-index: 10;
display: flex;
align-content: center;
align-items: center;
.icon {
width: 20px;
height: 20px;
background-size: 100% 100%;
background-repeat: no-repeat;
margin-right: 2px;
}
}
.marker-lag {
color: #d81e06;
.icon {
background-image: url('./../../assets/img/manage/icon_lag.png');
}
}
.marker-advance {
color: #14cf20;
.icon {
background-image: url('./../../assets/img/manage/icon_advance.png');
}
}
.project-bar {
display: flex;
border-radius: 4px;
color: #333;
font-size: 12px;
font-weight: 400;
text-align: center;
.project-all-time {
width: 20%;
background: #ffd28f;
border-radius: 4px;
box-shadow: 4px 0px 4px 0px rgba(0, 0, 0, 0.25);
position: relative;
z-index: 12;
overflow: hidden;
min-width: min-content;
padding: 0 2px;
}
.project-progress {
width: 20%;
border-radius: 0 4px 4px 0;
box-shadow: 4px 0px 4px 0px rgba(0, 0, 0, 0.25);
transform: translateX(-1px);
position: relative;
z-index: 11;
overflow: hidden;
min-width: min-content;
padding: 0 2px;
}
.lag {
background: #ff8f8f;
}
.normal {
background: #b3ff8f;
}
.project-normal {
width: -webkit-fill-available;
text-align: center;
}
.icon-line {
width: 21px;
height: 10px;
background-image: url('./../../assets/img/gante/icon_line.png');
background-size: 100% 100%;
background-repeat: no-repeat;
position: absolute;
left: 30px;
bottom: -6px;
z-index: 10;
}
.project-warn {
width: 40%;
background: #86afff;
border-radius: 4px;
box-shadow: 4px 0px 4px 0px rgba(0, 0, 0, 0.25);
text-align: left;
display: flex;
// -webkit-box-orient: vertical;
// -webkit-line-clamp: 1;
// overflow: hidden;
position: relative;
z-index: 10;
transform: translateX(-1px);
padding: 0 2px;
.warn-icon {
min-width: 34px;
height: 34px;
background-image: url('./../../assets/img/gante/warn_icon.png');
background-size: 100% 100%;
background-repeat: no-repeat;
}
.war-des {
line-height: 1;
position: absolute;
left: 50px;
bottom: -20px;
padding-top: 2px;
border-top: 1px solid #000;
}
}
.project-error {
width: 20%;
background: #86afff;
border-radius: 4px;
box-shadow: 4px 0px 4px 0px rgba(0, 0, 0, 0.25);
text-align: left;
display: flex;
// -webkit-box-orient: vertical;
// -webkit-line-clamp: 1;
position: relative;
padding: 0 2px;
.error-icon {
min-width: 34px;
height: 34px;
background-image: url('./../../assets/img/gante/error_icon.png');
background-size: 100% 100%;
background-repeat: no-repeat;
}
.error {
bottom: 10px;
left: 28px;
}
.error-des {
line-height: 1;
position: absolute;
left: 48px;
bottom: 10px;
padding-bottom: 2px;
border-bottom: 1px solid #000;
}
}
.in-line {
.icon-box {
.icon-line {
display: none;
}
}
.war-des {
line-height: 34px;
position: absolute;
left: 35px;
bottom: auto;
padding-top: 2px;
border-top: none;
}
.error-des {
line-height: 34px;
position: absolute;
left: 35px;
bottom: auto;
border-bottom: none;
}
}
}
.col-project-name {
width: 100%;
line-height: 1;
text-align: center;
height: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
align-content: center;
&:hover {
.project-name {
color: #2c94ff;
}
}
.project-index {
width: 20%;
height: 100%;
font-size: 14px;
font-weight: 400;
text-align: center;
color: rgba(0, 0, 0, 0.37);
border-right: 1px solid #d9d9d9;
display: inline-flex;
flex-wrap: wrap;
align-content: center;
justify-content: center;
}
.project-name {
width: 75%;
height: 100%;
padding: 2px 0;
font-size: 12px;
font-weight: 400;
text-decoration: underline;
text-align: center;
color: #1e86ff;
cursor: pointer;
display: inline-flex;
flex-wrap: wrap;
align-content: center;
div {
width: 100%;
margin: 2px 0;
}
}
}
.gantt_tree_icon {
display: none;
}
.gantt_cell_tree {
box-shadow: 1px 0px 4px 0px rgba(0, 0, 0, 0.15);
}
.gantt_grid_head_project {
box-shadow: 1px 0px 4px 0px rgba(0, 0, 0, 0.15);
}
.gantt_cell {
padding: 0;
}
}
}
</style>
1.2 任务条下有多个子进程展示(关联时间),如图
此方法是通过父子节点的关联性设置并且与时间相关联,父节点 设置 render: 'split'拆分子任务过程,子任务过程关联上父节点id(用parent)。可以通过设置子进程的开始结束时间或持续时间(duration)来设置子进程的长度。
核心逻辑(数据层次):
{
id: 13, //通过父子节点关系 父节点 设置 render: 'split'拆分子任务过程
text: '任务 #2',
start_date: '2023-03-04 00:00',
type: 'project',
render: 'split',
parent: '11',
progress: 0.5,
open: false,
duration: 11
},
{
//子任务过程关联上父节点id
id: 17,
text: '过程 #1',
start_date: '2023-03-04 00:00',
end_date: '2023-03-15 00:00',
// duration: 10,
parent: '13',
progress: 0,
open: true
},
{
id: 18,
text: '过程 #2',
start_date: '2023-05-04 00:00',
duration: 20,
parent: '13',
progress: 0,
open: true
},
完整代码:
<section class="my-gantt">
<div class="time-box">
<el-radio-group v-model="data.timeState" @change="changeTime">
<el-radio-button
v-for="(time, t_index) in data.timeList"
:key="t_index"
:label="time.code"
size="default"
border
>{{ time.name }}</el-radio-button
>
</el-radio-group>
</div>
<div id="gantt_here" class="gantt-container"></div>
</section>
</template>
<script setup>
import { reactive, toRefs, onBeforeMount, onMounted, watchEffect, defineExpose } from 'vue'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
//此方法是通过父子节点的关联性设置并且与时间相关联,父节点 设置 render: 'split'拆分子任务过程,子任务过程关联上父节点id(用parent)。可以通过设置子进程的开始结束时间或持续时间(duration)来设置子进程的长度。
// 注意id 的唯一性
const data = reactive({
timeList: [
{
name: '月',
code: 'month'
},
{
name: '季',
code: 'quarter'
},
{
name: '年',
code: 'year'
}
],
timeState: 'year',
demoData: {
data: [
{
id: 11,
text: '项目 #1',
type: 'project',
progress: 0,
open: true,
start_date: '2023-02-04 00:00',
duration: 13,
parent: 0
},
{
id: 12,
text: '任务 #1',
start_date: '2023-03-04 00:00',
duration: 5,
parent: '11',
progress: 0,
open: true
},
{
id: 13, //通过父子节点关系 父节点 设置 render: 'split'拆分子任务过程
text: '任务 #2',
start_date: '2023-03-04 00:00',
type: 'project',
render: 'split',
parent: '11',
progress: 0.5,
open: false,
duration: 11
},
{
//子任务过程关联上父节点id
id: 17,
text: '过程 #1',
start_date: '2023-03-04 00:00',
end_date: '2023-03-15 00:00',
// duration: 10,
parent: '13',
progress: 0,
open: true
},
{
id: 18,
text: '过程 #2',
start_date: '2023-05-04 00:00',
duration: 20,
parent: '13',
progress: 0,
open: true
},
{
id: 19,
text: '过程 #3',
start_date: '2023-08-04 00:00',
duration: 10,
parent: '13',
progress: 0,
open: true
},
{
id: 20,
text: '过程 #4',
start_date: '2023-10-04 00:00',
duration: 45,
parent: '13',
progress: 0,
open: true
},
{
id: 14,
text: '任务 #3',
start_date: '2023-02-04 00:00',
duration: 6,
parent: '11',
progress: 0,
open: true
},
{
id: 15,
text: '任务 #4',
type: 'project',
render: 'split',
parent: '11',
progress: 0,
open: true,
start_date: '2023-03-04 00:00',
duration: 11
},
{
id: 21,
text: '过程 #1',
start_date: '2023-03-04 00:00',
duration: 4,
parent: '15',
progress: 0,
open: true
},
{
id: 22,
text: '过程 #2',
start_date: '2023-08-04 00:00',
duration: 3,
parent: '15',
progress: 0,
open: true
}
]
}
})
const zoomConfig = {
levels: [
{
name: 'day',
scale_height: 60,
scales: [{ unit: 'day', step: 1, format: '%d %M' }]
},
{
name: 'week',
scale_height: 60,
scales: [
{
unit: 'week',
step: 1,
format: function (date) {
let dateToStr = gantt.date.date_to_str('%m-%d')
let endDate = gantt.date.add(date, -6, 'day')
let weekNum = gantt.date.date_to_str('%W')(date) //第几周
return dateToStr(endDate) + ' 至 ' + dateToStr(date)
}
},
{
unit: 'day',
step: 1,
format: '%d', // + "周%D"
css: function (date) {
if (date.getDay() == 0 || date.getDay() == 6) {
return 'day-item weekend weekend-border-bottom'
} else {
return 'day-item'
}
}
}
]
},
{
name: 'month',
scale_height: 60,
min_column_width: 18,
scales: [
{ unit: 'month', format: '%Y-%m' },
{
unit: 'day',
step: 1,
format: '%d',
css: function (date) {
if (date.getDay() == 0 || date.getDay() == 6) {
return 'day-item weekend weekend-border-bottom'
} else {
return 'day-item'
}
}
}
]
},
{
name: 'quarter',
height: 60,
min_column_width: 110,
scales: [
{
unit: 'quarter',
step: 1,
format: function (date) {
let yearStr = new Date(date).getFullYear() + '年'
let dateToStr = gantt.date.date_to_str('%M')
let endDate = gantt.date.add(gantt.date.add(date, 3, 'month'), -1, 'day')
return yearStr + dateToStr(date) + ' - ' + dateToStr(endDate)
}
},
{
unit: 'week',
step: 1,
format: function (date) {
let dateToStr = gantt.date.date_to_str('%m-%d')
let endDate = gantt.date.add(date, 6, 'day')
let weekNum = gantt.date.date_to_str('%W')(date)
return dateToStr(date) + ' 至 ' + dateToStr(endDate)
}
}
]
},
{
name: 'year',
scale_height: 50,
min_column_width: 150,
scales: [
{ unit: 'year', step: 1, format: '%Y年' },
{ unit: 'month', format: '%Y-%m' }
]
}
]
}
//初始化甘特图
const initGantt = () => {
let dateToStr = gantt.date.date_to_str('%Y.%m.%d')
gantt.config.grid_width = 350
gantt.config.add_column = false //添加符号
//时间轴图表中,如果不设置,只有行边框,区分上下的任务,设置之后带有列的边框,整个时间轴变成格子状。
gantt.config.autofit = false
gantt.config.row_height = 60
gantt.config.bar_height = 34
// gantt.config.fit_tasks = true //自动延长时间刻度,以适应所有显示的任务
gantt.config.auto_types = true //将包含子任务的任务转换为项目,将没有子任务的项目转换回任务
gantt.config.xml_date = '%Y-%m-%d' //甘特图时间数据格式
gantt.config.readonly = true //是否只读
gantt.config.columns = [
{
name: 'text',
label: '项目名称',
tree: true,
width: '*'
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 150
}
]
gantt.i18n.setLocale('cn') //设置语言
gantt.init('gantt_here') //初始化
gantt.parse(data.demoData) //填充数据
scrollInit()
gantt.ext.zoom.init(zoomConfig) //配置初始化扩展
gantt.ext.zoom.setLevel('year') //切换到指定的缩放级别
}
//拖拽滚动
const scrollInit = () => {
const nav = document.querySelectorAll('.gantt_task')[0]
const parNav = document.querySelectorAll('.gantt_hor_scroll')[0]
parNav.scrollLeft = 0
let flag
let downX
let scrollLeft
nav.addEventListener('mousedown', function (event) {
flag = true
downX = event.clientX // 获取到点击的x下标
scrollLeft = this.scrollLeft // 获取当前元素滚动条的偏移量
})
nav.addEventListener('mousemove', function (event) {
if (flag) {
let moveX = event.clientX
let scrollX = moveX - downX
parNav.scrollLeft = scrollLeft - scrollX
}
})
// 鼠标抬起停止拖动
nav.addEventListener('mouseup', function () {
flag = false
})
// 鼠标离开元素停止拖动
nav.addEventListener('mouseleave', function (event) {
flag = false
})
}
const changeTime = () => {
gantt.ext.zoom.setLevel(data.timeState)
}
onBeforeMount(() => {})
onMounted(() => {
initGantt()
})
watchEffect(() => {})
defineExpose({
...toRefs(data)
})
</script>
<style scoped lang="scss">
.my-gantt {
height: 800px;
width: 100vw;
.time-box {
text-align: center;
margin-bottom: 20px;
}
::v-deep .gantt-container {
border-radius: 8px 0px 0px 8px;
overflow: hidden;
height: 100%;
}
}
</style>
总结:也可以将两者相结合,在例2的子进程里面也开业包含例1的内容。
除此之外还有一些里程版或者重要节点的功能,也可以通过采用例1的方法去实现,比使用原生的自带方法更好。还有任务与任务直接的连接线,直接修改任务条数据等,看后续是否有相关需求。
附件:例子中的甘特图样式或数据。