🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
vue3封装一个基础甘特图
只支持简单展示功能
代码通俗易懂
效果图:
主要计算
计算出整体的日期范围
根据每项开始时间和结束时间计算出每一项所占的长度
// 基础甘特图封装
import { ref } from 'vue'
import dayjs from 'dayjs'
const data = [
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-15', progress: 50 },
{ order: 'AWM69963320-1', start: '2023-01-18', end: '2023-02-15', progress: 30 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-20', progress: 20 },
{ order: 'AWM69963320-1', start: '2023-02-18', end: '2023-02-25', progress: 10 },
]
// 日期数组
const dates = ref<string[]>([])
const itemWidth = 30
const init = () => {
const earliestStart = data.reduce((min, item) => dayjs(item.start).isBefore(dayjs(min)) ? item.start : min, data[0].start)
const latestEnd = data.reduce((max, item) => dayjs(item.end).isAfter(dayjs(max)) ? item.end : max, data[0].end)
let currentDate = dayjs(earliestStart)
let endDate = dayjs(latestEnd).add(1, 'day')
while (currentDate.isBefore(endDate)) {
dates.value.push(currentDate.format('YYYY-MM-DD'));
currentDate = currentDate.add(1, 'day');
}
}
// 计算每个任务的起始位置和宽度
const calculatePositionAndWidth = (item: any) => {
const startIndex = dates.value.findIndex((date) => date === item.start)
const endIndex = dates.value.findIndex((date) => date === item.end)
const position = startIndex * itemWidth
const width = (endIndex - startIndex + 1) * itemWidth
console.log(width, position);
return { position, width }
}
init()
代码还是非常少的,计算出日期存储到数组中用于渲染头部和每项长度的计算。
itemWidth: 头部每个日期的宽度(可以根据实际需求自行调整,我这只需要展示日,所以设定的比较短。)
渲染每条数据时都调用函数 calculatePositionAndWidth 计算出元素的偏移量和宽度。
偏移量:起始日期在数组中的位置 x 每个日期的宽度(itemWidth)
实际宽度:(结束日期在数组中的位置 - 起始日期在数组中的位置 + 1)x 每个日期的宽度(itemWidth),宽度需要加一个单位,这很好理解吧,假设起始日期在0位置,结束日期在1位置,1 - 0 = 1,而实际占了两个位置,所以需要 + 1。
有了偏移量和宽度,剩下的就是布局了,完整代码如下。
完整代码
css中: --labelWidth: 150px;
–labelWidth: 侧边栏的宽度,相当于 5 个日期长度,这个长度布局会用到。
“{ width: ${(dates.length + 5) * itemWidth}px }”,给每一项都赋予宽度,这个 + 5 就是侧边栏的宽度。数据太多太长的,滚动的时候需要固定左侧栏目和顶部日期,利于阅读。如果不指定每一项宽度,可能会导致,底部的border只有一部分。
至此,一个简单的甘特图封装完成。=。
<template>
<div class="ganttChart">
<ul class="itemList scrollBar">
<li class="item header" :style="{ width: `${(dates.length + 5) * itemWidth}px` }">
<div class="label"></div>
<div class="dates">
<div class="date" :style="{ width: `${itemWidth}px` }" v-for="date in dates" :key="date">{{ date.split('-')[2] }}</div>
</div>
</li>
<li class="item itemData" v-for="item in data" :style="{ width: `${(dates.length + 5) * itemWidth}px` }">
<div class="label">{{ item.order }}</div>
<a-tooltip color='white'>
<template #title>
<div style="color: black;">
排程数量:1000
</div>
<div style="color: black;">
完成数量:500
</div>
</template>
<div class="speedOfProgress"
:style="{ left: `${calculatePositionAndWidth(item).position}px`, width: `${calculatePositionAndWidth(item).width}px` }">
<div class="progress" :style="{ width: `${item.progress}%` }"></div>
{{ item.progress }}
</div>
</a-tooltip>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
// 基础甘特图封装
import { ref } from 'vue'
import dayjs from 'dayjs'
const data = [
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-15', progress: 50 },
{ order: 'AWM69963320-1', start: '2023-01-18', end: '2023-02-15', progress: 30 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-20', progress: 20 },
{ order: 'AWM69963320-1', start: '2023-02-18', end: '2023-02-25', progress: 10 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-15', progress: 30 },
{ order: 'AWM69963320-1', start: '2023-01-18', end: '2023-02-15', progress: 70 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-15', progress: 50 },
{ order: 'AWM69963320-1', start: '2023-01-18', end: '2023-02-15', progress: 30 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-20', progress: 20 },
{ order: 'AWM69963320-1', start: '2023-02-18', end: '2023-02-25', progress: 10 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-15', progress: 30 },
{ order: 'AWM69963320-1', start: '2023-01-18', end: '2023-02-15', progress: 70 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-15', progress: 50 },
{ order: 'AWM69963320-1', start: '2023-01-18', end: '2023-02-15', progress: 30 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-20', progress: 20 },
{ order: 'AWM69963320-1', start: '2023-02-18', end: '2023-02-25', progress: 10 },
{ order: 'AWM69963320-1', start: '2023-01-01', end: '2023-01-15', progress: 30 },
{ order: 'AWM69963320-1', start: '2023-01-18', end: '2023-02-15', progress: 70 },
]
// 日期数组
const dates = ref<string[]>([])
const itemWidth = 30
const init = () => {
const earliestStart = data.reduce((min, item) => dayjs(item.start).isBefore(dayjs(min)) ? item.start : min, data[0].start)
const latestEnd = data.reduce((max, item) => dayjs(item.end).isAfter(dayjs(max)) ? item.end : max, data[0].end)
let currentDate = dayjs(earliestStart)
let endDate = dayjs(latestEnd).add(1, 'day')
while (currentDate.isBefore(endDate)) {
dates.value.push(currentDate.format('YYYY-MM-DD'));
currentDate = currentDate.add(1, 'day');
}
}
// 计算每个任务的起始位置和宽度
const calculatePositionAndWidth = (item: any) => {
const startIndex = dates.value.findIndex((date) => date === item.start)
const endIndex = dates.value.findIndex((date) => date === item.end)
const position = startIndex * itemWidth
const width = (endIndex - startIndex + 1) * itemWidth
console.log(width, position);
return { position, width }
}
init()
</script>
<style scoped lang="scss">
.ganttChart {
--labelWidth: 150px;
height: calc(100% - 35px);
.itemList {
height: 100%;
position: relative;
.header {
position: sticky;
top: 0;
background-color: white;
z-index: 10;
}
.item {
display: grid;
box-sizing: border-box;
border-bottom: 1px solid #ccc;
grid-template-columns: var(--labelWidth) 1fr;
&:first-child {
border: none;
}
.label {
position: sticky;
left: 0;
background-color: white;
z-index: 5;
text-align: center;
}
.dates {
display: flex;
position: relative;
background-color: white;
.date {
box-sizing: border-box;
border: 1px solid #ccc;
text-align: center;
border-left: none;
&:first-child {
border-left: 1px solid #ccc;
}
}
}
.speedOfProgress {
margin-top: 4px;
height: 10px;
position: relative;
background-color: #b9d8fb;
border-radius: 5px;
overflow: hidden;
cursor: pointer;
.progress {
position: absolute;
height: 100%;
background-color: #27f;
}
}
}
.itemData {
padding: 5px 0;
}
}
}
</style>
补充滚动条样式
scrollBar(滚动条样式) 是单独封装在sass文件中的,所以没在原文中显示,补充一下滚动条样式
//滚动条
.scrollBar{
overflow-y: overlay;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #bfbfbf;
border-radius: 5px;
border: 1px solid #F1F1F1;
box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
}
&::-webkit-scrollbar-thumb:hover {
background-color: #A8A8A8;
}
&::-webkit-scrollbar-thumb:active {
background-color: #787878;
}
/*边角,即两个滚动条的交汇处*/
&::-webkit-scrollbar-corner {
background-color: #FFFFFF;
}
}
本文转载于:https://juejin.cn/post/7458107546391117887
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。