vue3使用vis绘制甘特图制作timeline可拖动时间轴及时间轴中文化(推荐)
林啾啾 人气:0前言:参考文档文章
一、实现效果:
二、安装插件及依赖:
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中
// showResetTip 是显示一个“已回到初始状态”的提示框,可以自己封装或者使用组件,此处不展示 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) }
加载全部内容