Vue3使用vis绘制甘特图制作timeline可拖动时间轴,时间轴中文化
创作时间:
作者:
@小白创作中心
Vue3使用vis绘制甘特图制作timeline可拖动时间轴,时间轴中文化
引用
CSDN
1.
https://blog.csdn.net/qq_39981639/article/details/126699918
本文将详细介绍如何使用Vue3和vis插件制作可拖动的甘特图时间轴,并实现时间轴的中文化显示。文章将按照以下顺序进行讲解:效果展示、子组件封装、父组件传值。本文仅实现基本的撤销(上一步)功能,样式部分需要根据实际需求自行调整。
前言
在项目开发中,甘特图常用于展示任务的时间安排和进度。本文将介绍如何使用Vue3和vis插件制作一个可拖动的甘特图时间轴,并实现时间轴的中文化显示。
一、实现效果
二、安装插件及依赖
在开始之前,需要安装以下依赖:
cnpm install -S vis-linetime
cnpm install -S vis-data
cnpm install -S moment
三、封装组件
下端时间轴单独封装成组件
1. HTML部分
<template>
<div class="visGantt" ref="visGanttDom"></div>
</template>
2. 引入依赖
import { DataSet } from 'vis-data/peer'
import { dateFormat } from '@/util' // 封装的时间格式化函数,如下所示
import { Timeline } from 'vis-timeline/peer'
import 'vis-timeline/styles/vis-timeline-graph2d.css'
const moment = require('moment')
时间格式化函数:
export function dateFormat(date, fmt) { // date是日期,fmt是格式
let o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'H+': date.getHours(), // 小时
'h+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
S: date.getMilliseconds() // 毫秒
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (var k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
}
}
return fmt
}
3. 父组件传入数据
let props = defineProps({
ganntData: { // 初始传入数据
type: Object,
default: () => {}
},
ganntHistoryData: { // 全部的历史数据,为了实现撤销上一步
type: Object,
default: () => {}
}
})
4. JS部分全部配置
配置项参考官方文档,仅注释解释个别方法。
<script setup>
import { ref, defineProps, watch, nextTick, defineEmits } from 'vue'
import { DataSet } from 'vis-data/peer'
import { dateFormat } from '@/util'
import { Timeline } from 'vis-timeline/peer'
import 'vis-timeline/styles/vis-timeline-graph2d.css'
const moment = require('moment')
let props = defineProps({
ganntData: {
type: Object,
default: () => {}
},
ganntHistoryData: {
type: Object,
default: () => {}
}
})
let timeline = ref(null)
watch(
props.ganntData,
(newVal) => {
if (newVal && newVal[0].trackTimeWindows && newVal[0].trackTimeWindows.length > 0) {
nextTick(() => {
initChart()
checkTimeConflict()
})
}
},
{
immediate: true,
deep: true
}
)
const computedData = () =>{
const trackTimeWindows = []
const timeWindows = []
props.ganntData[0].trackTimeWindows.forEach(
(trackTimeWindow, trackTimeWindowIndex) => {
// 项目类别数组
trackTimeWindows.push({
content: trackTimeWindow.deviceId,
id: `${trackTimeWindow.deviceId}-${trackTimeWindowIndex}-trackTimeWindows`,
value: trackTimeWindowIndex + 1,
className: `visGantt-item${trackTimeWindowIndex % 10}`
})
// 项目时间数组
trackTimeWindow.timeWindows.forEach((timeWindow, timeWindowIndex) => {
timeWindows.push({
start: new Date(timeWindow.startTime),
startTime: timeWindow.startTime,
end: new Date(timeWindow.stopTime),
stopTime: timeWindow.stopTime,
topTime: timeWindow.topTime,
group: `${trackTimeWindow.deviceId}-${trackTimeWindowIndex}-trackTimeWindows`,
className: `visGantt-item${trackTimeWindowIndex % 10}`,
id: `${trackTimeWindow.deviceId}`,
deviceId: trackTimeWindow.deviceId
})
})
}
)
return {
trackTimeWindows,
timeWindows
}
}
const visGanttDom = ref(null)
let historyDataArray = ref([])
const emit = defineEmits()
// 选择某个item
let onSelect = (properties) => {
emit('selectItem', properties.items[0])
}
const initChart = ()=> {
const { timeWindows, trackTimeWindows } = computedData()
const groups = new DataSet(trackTimeWindows)
const items = new DataSet(timeWindows)
let container = visGanttDom.value
if (container.firstChild) {
container.removeChild(container.firstChild)
}
// 甘特图配置
const options = {
groupOrder: function(a, b) {
return a.value - b.value
},
groupOrderSwap: function(a, b, groups) {
var v = a.value
a.value = b.value
b.value = v
},
height: '23.8vh', // 高度
verticalScroll: false, // 竖向滚动
orientation: 'top', // 时间轴位置
margin: {
axis: 1,
item: {
horizontal: 0,
vertical: 20
}
},
editable: {
updateTime: true,
updateGroup: false
},
multiselect: true,
moment: function(date) {
return moment(date).utc('+08:00')
},
groupHeightMode: 'fixed',
// min: new Date(startTime.value), // 最小可见范围
tooltip: {
followMouse: true,
overflowMethod: 'cap',
template: (originalItemData, parsedItemData) => {
// 鼠标hover时显示样式
return `<div>
<p>
<span>项目名称:</span>
<span>${originalItemData.deviceId}</span>
</p><br/>
<p>
<span>开始时间:</span>
<span>${dateFormat(parsedItemData.start, 'yyyy-MM-dd')}</span>
</p><br/>
<span>结束时间:</span>
<span>${dateFormat(parsedItemData.end, 'yyyy-MM-dd')}</span>
</p>
</div>`
}
},
tooltipOnItemUpdateTime: {
template: (item) => {
// 鼠标拖动时显示样式
return `<div>
<span>开始时间:${dateFormat(item.start, 'yyyy-MM-dd')}</span>
<span>\n</span>
<span>结束时间:${dateFormat(item.end, 'yyyy-MM-dd')}</span>
</div>`
}
},
locale: moment.locale('zh-cn'), // 时间轴国际化
showCurrentTime: false,
selectable: true,
zoomMin: 1728000000,
zoomMax: 315360000000,
// showTooltips: false,
// autoResize: false,
snap: function(date, scale, step) {
var day = 60 * 60 * 1000 * 24
return Math.round(date / day) * day
},
// 移动时返回函数
onMove: function(item, callback) {
// 深拷贝一下,不能直接修改父组件数据
historyDataArray.value = JSON.parse(JSON.stringify(props.ganntHistoryData))
let historyData = []
// props.ganntHistoryData是全部的历史数据,historyData 是取上一步的数据
historyData = JSON.parse(JSON.stringify(props.ganntHistoryData[props.ganntHistoryData.length - 1]))
// 更改一下格式
historyData[0].trackTimeWindows.forEach((eachItem)=>{
if (eachItem.deviceId === item.deviceId) {
if (!item.start || !item.end) {
return
}
eachItem.timeWindows[0].startTime = item.start
eachItem.timeWindows[0].stopTime = item.end
}
})
historyDataArray.value.push(historyData)
// 更新一下ganntHistoryData历史数据
emit('update:ganntHistoryData', historyDataArray.value)
callback(item)
},
onMoving: function(item, callback) {
// 移动时间轴时不显示tooltip提示框
let tooltipDom = document.getElementsByClassName('vis-tooltip')
tooltipDom[0].style.visibility = 'hidden'
callback(item)
}
}
timeline.value = new Timeline(container)
timeline.value.redraw()
timeline.value.setOptions(options)
timeline.value.setGroups(groups)
timeline.value.setItems(items)
timeline.value.on('select', onSelect)
}
</script>
四、父组件调用
1. 引入子组件
<div v-loading="loading"> // loading是为了有个加载效果,为了美观
<time-line
:ganntData="ganntData" // 原始数据
v-model:ganntHistoryData="ganntHistoryData" // 历史数据
@selectItem="timelineSelected" //选择事件
>
</time-line>
</div>
import TimeLine from '@/components/modules/planControl/TimeLine'
2. 初始数据
let props = defineProps({
// 因为这个父组件是通过点击进来的,所以有传入的数据,也可以直接赋值ganntData 数据,可以省略watch里面的转格式
conflictList: {
type: Array,
default: null
}
})
const ganntData = reactive([
{
name: 'confilct',
trackTimeWindows: [
]
}
])
const ganntHistoryData = ref([])
// 传入数据变化时为ganntData和ganntHistoryData赋值。
watch(
() => props.conflictList, (newValue) => {
ganntData[0].trackTimeWindows.length = 0
newValue.forEach(element => {
ganntData[0].trackTimeWindows.push({
deviceId: element.content,
timeWindows: [
{
startTime: element.startTime,
stopTime: element.stopTime
}
]
})
})
// 记录操作历史
ganntHistoryData.value.length = 0
ganntHistoryData.value.push(ganntData)
},
{
deep: true,
immediate: true
}
)
原数据(省略部分未使用参数):
[
{
"id": 1,
"content": "xxxxxxxxxxxxxx计划1",
"time": "2023.08~10",
"startTime": "2023-08-09",
"stopTime": "2023-10-20"
},
{
"id": 2,
"content": "xxxxxxxxxxxxxx计划2",
"time": "2023.09~11",
"startTime": "2023-09-09",
"stopTime": "2023-11-1"
},
{
"id": 3,
"content": "xxxxxxxxxxxxxx计划3",
"time": "2023.08~10",
"startTime": "2023-08-20",
"stopTime": "2023-10-1"
}
]
3. 父组件按钮及事件
仅展示原始图、撤销事件。
<div>
<div>
<el-button @click="reset()">原始图</el-button>
<el-button @click="preNode()">撤销</el-button>
<el-button>一键调整</el-button>
</div>
<div>
<el-button>取消</el-button>
<el-button>保存并退出</el-button>
</div>
</div>
回归原始图事件:
大致思路:先把ganntData清空,将拿到的props.conflictList里的数据赋值给ganntData,再把ganntData的数据push进ganntHistoryData中
const showResetTip = ref(false)
const loading = ref(false)
const reset = () => {
// loading是加载效果
loading.value = true
ganntData[0].trackTimeWindows.length = 0
props.conflictList.forEach(element => {
ganntData[0].trackTimeWindows.push({
deviceId: element.content,
timeWindows: [
{
startTime: element.startTime,
stopTime: element.stopTime
}
]
})
})
showResetTip.value = true
ganntHistoryData.value.splice(0)
ganntHistoryData.value.push(ganntData)
setTimeout(() => {
showResetTip.value = false
loading.value = false
}, 1000)
}
撤销事件:
大致思路:拿到子组件返回的ganntHistoryData历史数据数组,删掉最后一组数据后:
如果历史数据数组的长度<= 1,代表再撤销就回到原始状态了,那就直接调用reset()回到原始图;
否则,将ganntHistoryData删掉最后一组数据后的ganntHistoryDataClone最后一组值赋给ganntData,
const preNode = () => {
// loading是加载效果
loading.value = true
let ganntHistoryDataClone = []
ganntHistoryDataClone = JSON.parse(JSON.stringify(ganntHistoryData.value))
ganntHistoryDataClone.splice(ganntHistoryDataClone.length - 1, 1)
if (ganntHistoryDataClone.length <= 1) {
reset()
} else {
ganntData[0] = ganntHistoryDataClone[ganntHistoryDataClone.length - 1][0]
ganntHistoryData.value = JSON.parse(JSON.stringify(ganntHistoryDataClone))
}
setTimeout(() => {
loading.value = false
}, 1000)
}
热门推荐
走心年会策划指南:如何让感恩与激励贯穿始终
海底猫教你打造刷屏级年会:从主题设定到全网传播
年终狂欢!教你策划一场走心年会
王者荣耀2024铠瞬秒流出装攻略:一刀秒人,所向披靡
汽车万用表20A功能详解:从入门到精通
万用表20A功能,电工维修神器!
万用表20A功能教你搞定电路故障
万用表20A功能实操指南:电流测量不再难
弗吉尼亚理工大学薪资调整:608美元如何影响个人财务?
“紧日子”下的降薪潮:从公务员到金融从业者,这场财政紧缩如何影响你我?
《传家》造型师解读:双襟旗袍与复古发型的现代演绎
传家热播带火民国风,四大派系旗袍创新演绎传统美
农业遥感技术在玉米种植中的应用:从监测到管理全程智能化
科学防治玉米白化苗:缺锌是关键,这些方法要记牢
洗衣机排水故障频发?这份实用维修指南请收好!
北京专家号预约指南:高效挂号,轻松就医
30亿,武汉敏声高端射频滤波器项目主体封顶
海外华人注意!支付宝微信跨境转账新规来了
新中式旗袍成时尚新宠,传统工艺与现代设计完美融合
武汉东湖自驾游全攻略:秋日绝美湖景与美食的完美邂逅
王者荣耀铠英雄瞬秒流出装攻略
隔代教育:94%家庭的选择,如何实现爱与智慧的传承?
隔代教育中的育儿理念如何平衡?
隔代育儿,如何实现和平共处?
义乌市妇联“家事和姐”教你搞定隔代育儿难题
从可可豆到可可液、可可脂、可可粉:巧克力原料的制作工艺
武汉三大文化地标:从历史到现代的文明传承
武汉四天三晚自由行攻略:打卡网红景点,领略江城魅力
汽车四轮定位五步指南:专业检测调整确保行车安全
朱兴明:以技术创新驱动汇川技术跻身全球工控巨头