小时甘特图来袭!支持组拖拽、单个拖拽、左右拉伸

3,638 阅读5分钟

点击查看在线Demo

Timeline 拖拽技术概述

Timeline 拖拽技术是一种交互式可视化工具,它允许用户通过拖动时间轴上的标记来展示、编辑和分享项目的时间线。这种技术广泛应用于各种领域,包括项目管理、视频编辑、音乐制作、教育、科学研究等。本文将详细介绍 Timeline 拖拽技术的实现原理、设计步骤和最佳实践,并提供一个基于 HTML5 和 JavaScript 的简单示例。

Timeline 拖拽技术实现原理

Timeline 拖拽技术的实现主要依赖于 HTML5 中的拖放 API 和 JavaScript。在 HTML5 中,每个元素都可以被拖动,这包括文本、图像、链接等。通过使用 JavaScript,我们可以捕获用户的拖动事件,并在拖动过程中更新时间轴上的标记位置。

具体实现步骤如下:

  1. 创建一个 HTML 时间轴结构,包括标记和时间段。
  2. 使用 JavaScript 监听鼠标的拖动事件,记录鼠标位置和时间戳。
  3. 根据鼠标位置和时间戳计算标记的新位置,并将其更新到时间轴上。
  4. 如果需要,还可以在标记上添加额外的信息或交互元素。

Timeline 拖拽技术设计步骤

  1. 确定时间轴的用途和目标用户。这将有助于确定时间轴的外观和功能需求。
  2. 设计时间轴的外观和交互元素。这包括标记、时间段、颜色、字体等。
  3. 使用 HTML 和 CSS 创建时间轴的静态版本。这可以帮助您更好地理解时间轴的结构和布局。
  4. 使用 JavaScript 实现拖动功能。您需要编写代码来捕获鼠标的拖动事件,并在拖动过程中更新时间轴上的标记位置。
  5. 测试和优化时间轴的功能和性能。这包括在不同浏览器和设备上的测试,以确保时间轴在各种情况下都能正常工作。
  6. 添加其他功能和交互元素,如缩放、滚动、标记编辑等。
  7. 最后,根据用户反馈和需求变化进行迭代和优化。

Timeline 拖拽技术最佳实践

  1. 设计易于使用的时间轴界面,避免过多的复杂功能和交互元素。
  2. 在设计时间轴时考虑可读性和可访问性,确保用户可以轻松地阅读和理解时间轴上的信息。
  3. 使用标准的 HTML5 和 CSS3 特性,避免使用不兼容或过时的技术。
  4. 在实现时间轴时考虑性能和可扩展性,避免在处理大量数据时出现性能问题。
  5. 提供灵活的时间轴配置选项,允许用户自定义时间轴的外观和功能。
  6. 在发布时间轴之前进行充分的测试和验证,确保时间轴的功能和性能符合预期。
  7. 提供有用的文档和示例,以便其他开发人员可以轻松地使用和扩展您的代码。

基于 vue3 示例

1.注册依赖项

yarn add vis-timeline vis-data moment 点击查看vis-timeline文档

  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";
2.初始化

构造函数接受四个参数:

  • container 是在其中创建时间轴的 DOM 元素。
  • items 是一个包含项的数组。的属性 项目在 数据格式、项目 一节中描述。
  • groups 是一个包含组的数组。的属性 组在 数据格式,组 一节中描述。
  • options 是包含名称-值映射的可选对象 有选项。也可以使用该方法设置选项 setOptions.
 state.timeline = new Timeline(timelineRef.value as unknown as HTMLElement, dataList, 
      {
        locale: 'zh-cn', //moment.locale('zh-cn'), // 时间轴国际化
        orientation: 'top',
        // height: '60.8vh', // 高度
        min:'2023-05-12 08:00', // 设置时间轴可见范围的最小日期
        max:'2023-05-12 19:00', // 设置时间轴可见范围的最大日期
        onDropObjectOnItem: function(objectData, item, callback) {
        if (!item) { return; }
        alert('dropped object with content: "' + objectData.content + '" to item: "' + item.content + '"');
        },
        editable: props.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,   
         } : false,
        template: (sourceData:any, targetElement:any, parsedData:any)=> {
            let start = moment(parsedData.start).format('HH:mm')
            let end = moment(parsedData.end).format('HH:mm')
            targetElement.className = 'custom-item-template-class'; // 将自定义class写在className属性中
            return ` <div class="item-top">
                    </div>
                    <div class="item-content">
                        <span>
                        ${start}
                        -${end}
                      </span> 
                  </div>
                  <div class="item-bottom">
                    
                </div>
                  `;
        },
        timeAxis: {
            scale: 'minute', 
            step: 30
        },
        autoResize:false,
        type:'range',
        margin: {
          item: 10, // minimal margin between items
          axis: 5  // minimal margin between items and the axis
        },
        stack: true, // ture则不重叠
        zoomMax: 1000 * 60 * 60 * 28,
        zoomMin: 1000 * 60 * 1,
        verticalScroll:  props.editable, // 竖向滚动
        moveable: props.editable, // 禁止缩放
        // zoomFriction:500,
        horizontalScroll:  props.editable,
        moment: function(date:any) {
          return moment(date).locale('zh-cn'); //moment(date).utcOffset('+08:00');
        },
        // 显式将此选项设置为true以完全禁用Timeline的XSS保护
        xss: {
          disabled: true,
        },
        //可以提供模板处理程序。(或许可以直接放插槽?待测试)
        //此处理程序是一个函数,接受项的数据作为第一个参数,项元素作为第二个参数,编辑后的数据作为第三个参数,并输出格式化的HTML:
        tooltipOnItemUpdateTime: {
           template: (originalItemData:any) => {
             return `<div>
                       <p>
                         <span>开始时间:</span>
                         <span>${moment(originalItemData.start).format('HH:mm')}</span>
                       </p>
                       <p>
                         <span>结束时间:</span>
                         <span>${moment(originalItemData.end).format('HH:mm')}</span>
                       </p>
                     </div>`
           }
         },
        // onAdd(item, callback)在将要添加新项时触发。如果未实现,将使用默认文本内容添加该项。
        onAdd: (originalItemData:any, callback:any) => {
        //   console.log('新增originalItemData: ', originalItemData);
          if (originalItemData.id) {
            originalItemData.customClassName = 'un-submit'; // 未提交状态的样式
            callback(originalItemData); // 成功返回 这行相当于调用了dataList.add(originalItemData)
          }
          else {
            callback(null); // 失败取消
          }
        },
        onUpdate: function (item:any, callback:any) {
          if (item.id) {
           dblclickBar(item); // 打开弹窗
          }
          else {
            callback(null); // cancel updating the item
          }
        },
        onMove: function (item:any, callback:any) {
            //   item.moving = true;
            item.start =   fomartTime( item.start,5)
            item.end =  fomartTime( item.end,5)
          callback(item);
        },
        // 当项目被移动时重复触发的回调函数。仅在selectable和editable.updateTime或editable.updateGroup选项都设置为true时才适用
        onMoving: function (item:any, callback:any) {
            // console.log(item,'移动中')
        //    state.timeline.setItems(...item);
          item.moving = true;
          callback(item);
        },
      }
       nextTick(async () => {
        // if(state.timeline){
        //     state.timeline.setItems([], { clearNetwork: false });
        //     state.timeline.destroy(); // 销毁时间轴
        // } 
        // //   await getOperationRoom();
          await renderTimeLine(); // 渲染时间轴
          state.timeline.redraw();
       });
3.更新数据
 const data = await getAccountList({page:1,pageSize:16})
    // 开始  
   const min = data.records[0].startTime
    // 结束         
   const max = data.records[0].endTime
    data.records[0].timeline.forEach(item=>{
      dataList.add({
        start: item.myBeginDate,
        end:item.myEndDate,
        id:item.ganttBarConfig.id,
        group:item.group,
        info:item.info,
        editable:item.editable,
      })
    })
    let groups = new DataSet()
    const gourpList = ['0','31001','31002','31003','9','1','2','3']
    for (var i = 0; i < gourpList.length; i++) {
      const item = gourpList[i]
      groups.add({
          id:item,
          value: i + 1,
          content: `<div style="width:80px;">
                      ${item.length  > 2 ?  item : ' '}
                     </div>`
      })
    }
    // 更新配置选项
    state.timeline.setOptions({
      // min, // 设置时间轴可见范围的最小日期
      // max, // 设置时间轴可见范围的最大日期
      groupEditable: props.editable,
      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;
      },
      groupTemplate: (groupData:any, element:any) => {
        // console.log(groupData)
        element.className = 'custom-group-template-class'; // 将自定义class写在className属性中
        return `<div class="group"  style="width:80px;" >
                 ${groupData.content}
                </div>`;
      },
    });
    // 设置分组
    state.timeline.setGroups(groups);
    state.timeline.setItems(dataList);