vis.js使用之vis-timeline使用攻略,vis-timeline实现时间轴、甘特图
- 1、vis-timeline简介
- 2、安装插件及依赖
- 3、简单示例
- 4、疑难问题集合
- 1. 中文zh-cn本地化
- 2. 关于自定义class样式无法被渲染
- 3. 关于双向数据绑定
vis.js是一个基于浏览器的可视化库,它提供了多个组件,包括DataSet, Timeline, Network, Graph2d和Graph3d。该库具有易用性、能够处理大量动态数据和允许数据操作和交互的特点。
1、vis-timeline简介
vis-timeline
时间轴是一个交互式可视化图表,用于实时可视化时间数据。数据项可以只与某个时间点关联,也可以有开始和结束日期(即一个时间范围)。vis-timeline可以通过拖拽和滚动时间轴自由移动和缩放。可以在时间轴中创建、编辑和删除数据项目。轴上的时间尺度是自动调整的,支持从毫秒到年的尺度。
vis-time时间轴使用常规HTML DOM
呈现时间轴和放在时间轴上的项目,这样的好处就是可以使用自定义css样式进行灵活定制。
Timeline | 地址 |
---|---|
vis.js官网 | https://visjs.org/ |
vis-timeline官方英文文档 | https://visjs.github.io/vis-timeline/docs/timeline/ |
vis-timeline官方示例 | https://visjs.github.io/vis-timeline/examples/timeline/ |
vis-timeline的github源码 | https://github.com/visjs/vis-timeline |
可实现效果如下:纵向可以分组,横向可以是时间轴,每个item项目可以自定义内容与样式。
2、安装插件及依赖
// vis-timeline包 cnpm install -S vis-timeline // vis.js提供的可以实现数据双向绑定的包 cnpm install -S vis-data // 实现时间轴中文的moment.js库的包 cnpm install -S moment
复制
3、简单示例
<template> <div class="bindNurseToRoom-container"> <!-- 时间轴-绑定元素 --> <div ref="timelineRef" id="timeline" class="bindNurseToRoom-container"></div> </div> </template> <script setup lang="ts" name="bindNurseToRoom"> import { onMounted, ref, watch, nextTick, reactive, defineAsyncComponent } from 'vue'; import "vis-timeline/styles/vis-timeline-graph2d.min.css"; import { DataSet } from 'vis-data'; // 为timeline提供双向数据绑定,加快渲染速度 import { Timeline } from "vis-timeline"; //standalone,peer不同的包装方式 import moment from 'moment'; import "moment/dist/locale/zh-cn.js"; import { ElMessage, ElMessageBox } from 'element-plus'; import { useOperatingRoomApi } from '/@/api/room/operatingRoom'; import { useOperationScheduleStore } from '/@/stores/operationScheduleStore'; const _useOperationScheduleStore = useOperationScheduleStore(); // 引入组件 const BindNurseToRoomDialog = defineAsyncComponent(() => import('./bindNurseToRoomDialog.vue')); const timelineRef = ref(null); const bindDialogRef = ref(); // 定义父组件传过来的值 const props = defineProps({ // 当前操作时间 operateTime: { type: String, default: () => '', }, // 配置项 config: { type: Object, default: () => {}, }, }); const curOperateTime = ref(''); // 当前操作日期格式化的字符串 或 undefined 或 "" let dataList:any = new DataSet([ // { // id: 1, // content: "手术1", // start: "2023-04-07 08:00", // end: "2023-04-07 10:00", // group: "5a92fde514c2c842f680885b1d31b9b8", // style: "color: white; background-color: #1abc9c;", // idCard: "123456", // patientName: "李秀莲", // doctorName: "李莲", // anaesthesiaType: "局部", // }, // { // id: 2, // content: "手术2", // start: "2023-04-06 10:00", // end: "2023-04-06 12:00", // group: "手术室1", // style: "color: white; background-color: #2ecc71;", // idCard: "12346", // patientName: "李秀", // doctorName: "李莲", // anaesthesiaType: "局部", // }, // { // id: 3, // content: "手术3", // start: "2023-04-06 08:30", // end: "2023-04-06 09:30", // group: "手术室2", // style: "color: white; background-color: #3498db;", // idCard: "1456", // patientName: "莲", // doctorName: "李", // anaesthesiaType: "局部", // }, // { // id: 4, // content: `<div style="display:block;height:100px;background:red;"> // 123123123213 // </div> `, //content接收字符串类型的文本或html // start: "2023-04-06 11:00", // end: "2023-04-06 14:00", // group: "手术室2", // style: "color: white; background-color: #9b59b6;", // idCard: "1236", // patientName: "李的", // doctorName: "李莲时", // anaesthesiaType: "局部", // }, // { // id: 5, // content: `<div style="display:block;height:100px;background:red;">手术5</div> `, // start: "2023-04-06 06:30", // end: "2023-04-06 10:10", // group: "手术室5", // className:'icu', // editable: false, // 给某个特定的设置为不可编辑 // idCard: "156", // patientName: "李秀消", // doctorName: "李莲", // anaesthesiaType: "局部", // }, ]); const state:any = reactive({ groups: null, // 手术室分组-new DataSet()格式的数据集 timeline: null, // 手术室当前排班时间轴-new DataSet()格式的数据集 }); // 监听当前操作日期变化 watch( () => props.operateTime, (newValue: any) => { if (newValue) { curOperateTime.value = newValue; // 保存当前操作日期到变量中,以便以后使用。 nextTick(async () => { if(state.timeline){ // state.timeline.setItems([], { clearNetwork: false }); // state.timeline.destroy(); // 销毁时间轴 } await getOperationRoom(); await renderTimeLine(); // 渲染时间轴 state.timeline.redraw(); }); } }, { immediate : true } //在组件初次加载的时候执行 ); onMounted(async () => { state.timeline = new Timeline( timelineRef.value as unknown as HTMLElement, //document.getElementById("timeline") as HTMLElement, dataList, { locale: 'zh-cn', //moment.locale('zh-cn'), // 时间轴国际化 editable: { add: true, // 双击添加新项-add new items by double tapping updateTime: true, // 水平拖拉项目-drag items horizontally updateGroup: true, // 从一个分组拖拽到另一个分组-drag items from one group to another remove: true, // 通过右上角按钮删除项目-delete an item by tapping the delete button top right // overrideItems: false // allow these options to override item.editable }, selectable: true, // height: '730px', // 时间轴高度 minHeight: 400, // timeline表格的最小高度 maxHeight: 750, // timeline表格的最大高度 groupHeightMode: 'fixed', // 指定分组高度: 自动auto, fixed固定, fitItems适应项目 stack: false, // ture则不重叠 verticalScroll: true, // 竖向滚动 orientation: 'top', // 时间轴位置 showCurrentTime: true, // 显示当前时间 zoomKey: "ctrlKey", // 缩放按键 zoomMax: 1000 * 60 * 60 * 24, zoomMin: 1000 * 60 * 30, moment: function(date:any) { return moment(date).locale('zh-cn'); //vis.moment(date).utcOffset('+08:00'); }, // 显式将此选项设置为true以完全禁用Timeline的XSS保护 xss: { disabled: true, }, //可以提供模板处理程序。(或许可以直接放插槽?待测试) //此处理程序是一个函数,接受项的数据作为第一个参数,项元素作为第二个参数,编辑后的数据作为第三个参数,并输出格式化的HTML: template: function (sourceData:any, targetElement:any, parsedData:any) { console.log('parsedData: ', parsedData); targetElement.className = 'custom-item-template-class'; // 将自定义class写在className属性中 return `<div class="custom-item ${sourceData.customClassName}"> <div class="top"> <span> ${moment(sourceData.start).format('YYYY-MM-DD HH:mm:ss').split(' ')[1]} -${moment(sourceData.end).format('YYYY-MM-DD HH:mm:ss').split(' ')[1]} </span> <span>${sourceData.doctorName}</span> </div> <div class="center-box"> <div class="info"> <span>${sourceData.patientName}</span> <span>${sourceData.idCard}</span> <span>${sourceData.sex?'男':'女'}</span> <span>${sourceData.age}岁</span> </div> <h3>${sourceData.content}</h3> </div> <div class="nurse-box"> <span>${ sourceData?.selectedNurse?.xshs1.name ? sourceData.selectedNurse.xshs1.name :'---' }</span> <span>${ sourceData?.selectedNurse?.xhhs1.name ? sourceData.selectedNurse.xhhs1.name :'---' }</span> </div> <div class="bottom-box"> <h4>${sourceData.anaesthesiaType}</h4> </div> </div>`; }, tooltip: { followMouse: false, template: (originalItemData:any, parsedItemData:any) => { console.log('hover-parsedItemData: ', parsedItemData); return `<div> <p> <span>开始时间:</span> <span>${moment(originalItemData.start).format('YYYY-MM-DD HH:mm:ss')}</span> </p> <p> <span>结束时间:</span> <span>${moment(originalItemData.end).format('YYYY-MM-DD HH:mm:ss')}</span> </p> <p> <span>手术内容:</span> <span>${originalItemData.content}</span> </p> </div>` } }, // onAdd(item, callback)在将要添加新项时触发。如果未实现,将使用默认文本内容添加该项。 onAdd: (originalItemData:any, callback:any) => { debugger console.log('新增originalItemData: ', originalItemData); if (originalItemData.id) { originalItemData.customClassName = 'un-submit'; // 未提交状态的样式 callback(originalItemData); // 成功返回 这行相当于调用了dataList.add(originalItemData) } else { callback(null); // 失败取消 } }, // onDropObjectOnItem(objectData,Item)在将对象放入现有时间轴项时触发。 // 当拖动数据中包含target:'item'的对象被放入时间轴项时触发回调函数。 onDropObjectOnItem: function (objectData:any, item:any) { debugger if (!item) { ElMessage({message: '请拖动护士到对应的手术项目中',type: 'warning'}) return; } onDropToItem(objectData, item); }, // onUpdate(item,callback)在项目即将更新时触发(双击item时)。此函数通常会显示用户更改项目的对话框。如果不执行,什么都不会发生。 // 示例:https://visjs.github.io/vis-timeline/examples/timeline/editing/editingItemsCallbacks.html onUpdate: function (item:any, callback:any) { if (item.id) { callback(item); // send back adjusted item bindDialogRef.value.openDialog(item); // 打开弹窗 } else { callback(null); // cancel updating the item } }, // 当项目被移动时重复触发的回调函数。仅在selectable和editable.updateTime或editable.updateGroup选项都设置为true时才适用 onMoving: function (item:any, callback:any) { console.log('item: ', item); item.moving = true; callback(item); }, // 当项目即将被删除时触发onRemove(item, callback)。如果未实现,该项将始终被删除。 onRemove: (item:any, callback:Function) => { onDeleteByItemType(item,callback); }, } ); }); // 获取医院手术室信息 const getOperationRoom = async () => { let { data } = await useOperatingRoomApi().selectAdministrativeOffice(); if(data.length){ let temp = data.map((item:any) => { return { ...item, content: item.administrativeOfficeCard, style: "color: #fff; background: #5E8DFF;", } }).sort((a:any, b:any) => {return a.content - b.content}); state.groups = new DataSet(temp); } } // 渲染时间轴timeline = new Timeline(container, items, groups, options); const renderTimeLine = async () => { // 清空数据集 // dataList.clear(); dataList = new DataSet([]); // 获取当天已经排班的数据 await getScheduledData(); // 设置setItems state.timeline.setItems(dataList); // 更新配置选项 state.timeline.setOptions({ min: moment(curOperateTime.value + ' 7:00:00').format('YYYY-MM-DD HH:mm:ss'), // 设置时间轴可见范围的最小日期 max: moment(curOperateTime.value).endOf('day').format('YYYY-MM-DD HH:mm:ss'), // 设置时间轴可见范围的最大日期 groupTemplate: (groupData:any, element:any) => { element.className = 'custom-group-template-class'; // 将自定义class写在className属性中 return `<div class="group" title="${groupData.description}"> <span class="group-id">${groupData.content}</span> <span class="group-name">${groupData.administrativeOfficeName}</span> </div>`; }, }); // 跳转到当前时间轴 state.timeline.moveTo(curOperateTime.value); // 设置分组 state.timeline.setGroups(state.groups); // 打印当前数据 dataList.forEach((element: any) => { console.log('---------dataList: ', element); }); } </script> <style scoped lang="scss"> .bindNurseToRoom-container { width: 100%; position: relative; } // vis-timeline样式 :deep(#timeline){ .vis-top{ background-color: #90e0db9c; .vis-even,.vis-odd{ border-left: 1px solid; } } // (此项目必须设置)自定义group分组样式 .custom-group-template-class{ height: 160px; width: 80px; display: flex; flex-direction: column; align-items: center; justify-content: center; .group{ display: flex; flex-direction: column; justify-content: center; align-items: center; .group-id{ font-size: 22px; } .group-name{ font-size: 14px; margin-top: 10px; } } } // (此项必须设置)自定义item样式 .custom-item-template-class{ // color: #fff; .custom-item { .top{ font-size: 18px; font-weight: bold; color: #5E8DFF; border-bottom: 1px dashed #C4C4C4; } .center-box{ padding: 5px; } .nurse-box{ display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-around; span{ width: 40%; height: 30px; padding: 5px; border: 1px dashed #5E8DFF; border-radius: 5px; text-align: center; } } .bottom-box{ padding: 5px; } } // 未提交状态的样式 .un-submit{ border: 2px solid #698df0; padding: 5px; } .ed-submit{ border: 2px solid #efb03f; padding: 5px; } } // 使用自定义class实现不同手术状态 .vis-item.icu { color: white; background-color: rgb(228, 210, 93); border-color: darkred; height: 100px; } } // groups样式 :deep(.group-icu){ background-color: rgba(244, 176, 59, 0.2); } </style> <!-- 为啥我的template中的自定义的class并没有被渲染到元素中? 你的自定义class选择器写错了:如果在template中自定义了class,但是并没有在CSS样式表中定义,那么这个class将不会生效。 请检查你的CSS样式表中是否已经定义了相应的类选择器,或者将class直接写在style属性中。 vis-timeline对class属性进行了过滤:vis-timeline默认会对content和className等属性进行过滤,以避免XSS攻击。 如果你的class名称被视为可疑字符,那么它将被自动过滤掉。你可以通过在options选项中增加content属性的设置,来打开这个过滤功能: vis-timeline缓存了渲染数据,导致更新不及时:有时候,即使你已经在代码中正确设置了class属性,但是图表仍然没有反应出来。 这可能是因为vis-timeline缓存了渲染数据,需要手动调用timeline.redraw()方法来更新图表。 你可以在修改了item对象的class之后,手动调用timeline.redraw()方法,以更新图表。 -->
复制
4、疑难问题集合
1. 中文zh-cn本地化
import moment from 'moment'; // 需要引入下方这个文件 import "moment/dist/locale/zh-cn.js";
复制
网上说的在配置项options中引入locale: moment.locale('zh-cn')
无法实现本地化
2. 关于自定义class样式无法被渲染
写了自定义样式后,发现没有在元素中渲染出对应的class,原因有两点:
- vis-time本身为了防止
xss
攻击,自动过滤了你写的样式类。需要在options中配置打开xss: {disabled:true,},
- 你写的样式类需要通过
:v-deep()
渲染到界面,不写对应的样式,只写class类名
是无法渲染class名字
的
3. 关于双向数据绑定
需要使用let dataList = new DataSet([ ]);
官网真的写的挺详细的,不得不说国外这种开源网站确实非常给力。
有啥疑问可以一起交流哦