Vis-Timeline-Nutzungsstrategie, die von vis.js verwendet wird, vis-Timeline implementiert Zeitachse und Gantt-Diagramm
vis.js ist eine browserbasierte Visualisierungsbibliothek, die mehrere Komponenten bereitstellt, darunter DataSet, Timeline, Network, Graph2d und Graph3d. Die Bibliothek zeichnet sich durch Benutzerfreundlichkeit, die Fähigkeit zur Handhabung großer Mengen dynamischer Daten und die Möglichkeit der Datenmanipulation und -interaktion aus.
1. Einführung in die Vis-Zeitleiste
vis-timeline
Timeline ist eine interaktive Visualisierung zur Visualisierung von Zeitdaten in Echtzeit. Ein Datenelement kann nur einem Zeitpunkt zugeordnet werden oder es kann ein Start- und Enddatum (d. h. einen Zeitbereich) haben. Die vis-Zeitleiste kann frei verschoben und gezoomt werden, indem die Zeitleiste gezogen und gescrollt wird. Datenelemente können in der Timeline erstellt, bearbeitet und gelöscht werden. Die Zeitskala auf der Achse wird automatisch angepasst und unterstützt Skalen von Millisekunden bis zu Jahren.
Die Vis-Time-Zeitleiste verwendet eine reguläre HTML DOM
Rendering-Zeitleiste und auf der Zeitleiste platzierte Elemente, was den Vorteil hat, dass sie flexibel und mit benutzerdefinierten CSS-Stilen anpassbar ist.
Zeitleiste | Adresse |
---|---|
vis.js offizielle Website | https://visjs.org/ |
Offizielles englisches Dokument von Vis-Timeline | https://visjs.github.io/vis-timeline/docs/timeline/ |
Vis-Timeline offizielles Beispiel | https://visjs.github.io/vis-timeline/examples/timeline/ |
Der Github-Quellcode von vis-timeline | https://github.com/visjs/vis-timeline |
Die erzielbaren Effekte sind wie folgt: Die Vertikale kann gruppiert werden, die Horizontale kann die Zeitachse sein und jedes Element kann den Inhalt und Stil anpassen.
2. Plug-Ins und Abhängigkeiten installieren
// vis-timeline包
cnpm install -S vis-timeline
// vis.js提供的可以实现数据双向绑定的包
cnpm install -S vis-data
// 实现时间轴中文的moment.js库的包
cnpm install -S moment
3. Einfaches Beispiel
<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. Sammlung schwieriger Probleme
1. Chinesische zh-cn-Lokalisierung
import moment from 'moment';
// 需要引入下方这个文件
import "moment/dist/locale/zh-cn.js";
Im Internet wird gesagt, dass die Einführung der Konfigurationselementoptionen locale: moment.locale('zh-cn')
keine Lokalisierung erreichen kann
2. Der benutzerdefinierte Klassenstil kann nicht wiedergegeben werden
Nachdem ich einen benutzerdefinierten Stil geschrieben hatte, stellte ich fest, dass die entsprechende Klasse aus zwei Gründen nicht im Element gerendert wurde:
- Um Angriffe zu verhindern,
xss
filtert vis-time automatisch die von Ihnen geschriebenen Stilklassen. Es muss in den Optionen zum Öffnen konfiguriert werdenxss: {disabled:true,},
- Die Stilklasse, die Sie schreiben, muss
:v-deep()
für die Schnittstelle gerendert werden. Wenn Sie den entsprechenden Stil nicht schreiben,class类名
können Sie ihn nicht rendern, wenn Sie ihn einfach schreibenclass名字
.
3. Über die bidirektionale Datenbindung
Brauchen Sie zu verwendenlet dataList = new DataSet([ ]);
Die offizielle Website ist sehr detailliert, und ich muss sagen, dass Open-Source-Websites wie diese im Ausland wirklich mächtig sind.
Wenn Sie Fragen haben, können Sie mit uns kommunizieren.