[JS] Vis-timeline usage strategy used by vis.js, vis-timeline realizes time axis and Gantt chart in vue3

vis.js is a browser-based visualization library that provides multiple components, including DataSet, Timeline, Network, Graph2d and Graph3d. The library is characterized by ease of use, ability to handle large amounts of dynamic data, and allowing data manipulation and interaction.

you want.js

1. Introduction to vis-timeline

vis-timelineTimeline is an interactive visualization for visualizing time data in real time. A data item can be associated with only a point in time, or it can have a start and end date (that is, a time range). vis-timeline can be moved and zoomed freely by dragging and scrolling the timeline. Data items can be created, edited and deleted in the timeline. The time scale on the axis is automatically adjusted, supporting scales from milliseconds to years.

The vis-time timeline uses a regular HTML DOMrendering timeline and items placed on the timeline, which has the advantage of being flexible and customizable with custom css styles.

Timeline address
vis.js official website https://visjs.org/
Vis-timeline official English document https://visjs.github.io/vis-timeline/docs/timeline/
Vis-timeline official example https://visjs.github.io/vis-timeline/examples/timeline/
The github source code of vis-timeline https://github.com/visjs/vis-timeline

The achievable effects are as follows: the vertical can be grouped, the horizontal can be the time axis, and each item can customize the content and style.
achieve effect

2. Install plug-ins and dependencies

// vis-timeline包
cnpm install -S vis-timeline

// vis.js提供的可以实现数据双向绑定的包
cnpm install -S vis-data

// 实现时间轴中文的moment.js库的包
cnpm install -S moment

3. Simple example

<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> &nbsp;
                      <span>${ 
        sourceData.idCard}</span> &nbsp;
                      <span>${ 
        sourceData.sex?'男':'女'}</span> &nbsp;
                      <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. Collection of difficult problems

1. Chinese zh-cn localization

import moment from 'moment';
// 需要引入下方这个文件
import  "moment/dist/locale/zh-cn.js";

It is said on the Internet that the introduction of the configuration item options locale: moment.locale('zh-cn')cannot achieve localization

2. The custom class style cannot be rendered

After writing a custom style, I found that the corresponding class was not rendered in the element, for two reasons:

  • In order to prevent attacks, vis-time itself xssautomatically filters the style classes you write. It needs to be configured in options to openxss: {disabled:true,},
  • The style class you write needs to be :v-deep()rendered to the interface. If you don’t write the corresponding style, you class类名can’t render it if you just write it class名字.

3. About two-way data binding

Need to uselet dataList = new DataSet([ ]);

The official website is really detailed, and I have to say that open source websites like this abroad are really powerful.
If you have any questions, you can communicate with us.

Guess you like

Origin blog.csdn.net/weixin_42960907/article/details/130213139