You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何创建无间隙的vis.js时间轴?含元素适配及校验需求

实现vis.js时间轴的无间隙填充与自动贴合需求

我刚好在项目里折腾过类似的vis.js时间轴需求,把四个功能点的实现思路和关键代码整理给你,都是经过实际验证的方案:

1. 确保元素间无间隙(初始添加/拖动时约束)

要从数据层面+事件监听双管齐下:

  • 初始添加元素时,强制同组内新元素的start等于该组最后一个元素的end(如果是第一个元素则设为分组的起始时间)
  • 监听元素的drag事件,禁止拖动后产生间隙,拖动结束后自动贴合前后元素
// 初始化时添加元素的逻辑
function addItemToGroup(groupId, duration) {
  const groupItems = timeline.getItems({ group: groupId }).sort((a,b) => a.start - b.start);
  let newStart;
  if (groupItems.length === 0) {
    newStart = new Date('2024-01-01'); // 分组默认起始时间,可根据需求修改
  } else {
    newStart = groupItems[groupItems.length - 1].end;
  }
  const newEnd = new Date(newStart.getTime() + duration * 3600000); // 按小时计算时长,可调整单位
  timeline.addItem({
    id: `item-${Date.now()}`,
    group: groupId,
    start: newStart,
    end: newEnd,
    content: `新任务`
  });
}

// 监听拖动事件,防止产生间隙
timeline.on('dragend', function(props) {
  const item = props.item;
  const groupItems = timeline.getItems({ group: item.group }).sort((a,b) => a.start - b.start);
  const itemIndex = groupItems.findIndex(i => i.id === item.id);
  
  // 贴合前一个元素
  if (itemIndex > 0) {
    const prevItem = groupItems[itemIndex - 1];
    if (item.start < prevItem.end) {
      timeline.updateItem({ id: item.id, start: prevItem.end });
    }
  }
  
  // 贴合后一个元素
  if (itemIndex < groupItems.length - 1) {
    const nextItem = groupItems[itemIndex + 1];
    const updatedItem = timeline.getItem(item.id);
    if (updatedItem.end > nextItem.start) {
      timeline.updateItem({ id: nextItem.id, start: updatedItem.end });
    }
  }
});

2. 删除中间元素后,新增元素自动填充间隙

核心是维护分组的间隙列表,删除元素时记录空闲时间段,新增元素优先匹配间隙:

// 存储各分组的间隙:key为groupId,value为[{start: Date, end: Date}]
const groupGaps = {};

// 删除元素时记录间隙
timeline.on('delete', function(props) {
  const deletedItem = props.item;
  const groupId = deletedItem.group;
  
  if (!groupGaps[groupId]) groupGaps[groupId] = [];
  // 添加间隙到列表,并按时间排序
  groupGaps[groupId].push({ start: deletedItem.start, end: deletedItem.end });
  groupGaps[groupId].sort((a,b) => a.start - b.start);
});

// 修改addItemToGroup函数,优先填充间隙
function addItemToGroup(groupId, duration) {
  const targetDuration = duration * 3600000;
  let newStart, newEnd;
  
  // 检查是否有可填充的间隙
  if (groupGaps[groupId] && groupGaps[groupId].length > 0) {
    const suitableGap = groupGaps[groupId].find(gap => gap.end - gap.start >= targetDuration);
    if (suitableGap) {
      newStart = suitableGap.start;
      newEnd = new Date(newStart.getTime() + targetDuration);
      // 移除已填充的间隙,如果间隙还有剩余则更新
      const gapIndex = groupGaps[groupId].indexOf(suitableGap);
      if (suitableGap.end - newEnd > 0) {
        groupGaps[groupId][gapIndex] = { start: newEnd, end: suitableGap.end };
      } else {
        groupGaps[groupId].splice(gapIndex, 1);
      }
    }
  }
  
  // 没有合适间隙则加到末尾
  if (!newStart) {
    const groupItems = timeline.getItems({ group: groupId }).sort((a,b) => a.start - b.start);
    newStart = groupItems.length === 0 ? new Date('2024-01-01') : groupItems[groupItems.length - 1].end;
    newEnd = new Date(newStart.getTime() + targetDuration);
  }
  
  timeline.addItem({
    id: `item-${Date.now()}`,
    group: groupId,
    start: newStart,
    end: newEnd,
    content: `新任务`
  });
}

3. 调整元素尺寸时自动贴合下一个元素

监听resize事件,实时调整下一个元素的起始时间,保持无缝贴合:

timeline.on('resizeend', function(props) {
  const updatedItem = props.item;
  const groupItems = timeline.getItems({ group: updatedItem.group }).sort((a,b) => a.start - b.start);
  const itemIndex = groupItems.findIndex(i => i.id === updatedItem.id);
  
  // 仅调整紧挨着的下一个元素(如果有)
  if (itemIndex < groupItems.length - 1) {
    const nextItem = groupItems[itemIndex + 1];
    // 更新下一个元素的start为当前元素的end
    timeline.updateItem({ id: nextItem.id, start: updatedItem.end });
    // 如果需要递归调整后面所有元素,可以在这里循环处理后续元素
  }
});

4. 校验指定日期范围内分组元素是否完全填充

遍历分组内的元素,按时间排序后逐一检查连续性,同时校验首尾是否覆盖指定范围:

function checkGroupFullFill(groupId, checkStart, checkEnd) {
  const groupItems = timeline.getItems({ group: groupId })
    .map(item => ({ start: new Date(item.start), end: new Date(item.end) }))
    .sort((a,b) => a.start - b.start);
  
  // 空分组直接返回false
  if (groupItems.length === 0) return false;
  
  // 检查起始是否匹配指定范围
  if (groupItems[0].start.getTime() !== checkStart.getTime()) return false;
  
  // 检查元素间是否连续无间隙
  for (let i = 1; i < groupItems.length; i++) {
    if (groupItems[i].start.getTime() !== groupItems[i-1].end.getTime()) {
      return false;
    }
  }
  
  // 检查结束是否匹配指定范围
  if (groupItems[groupItems.length - 1].end.getTime() !== checkEnd.getTime()) return false;
  
  return true;
}

// 使用示例:检查group1在2024-01-01到2024-01-07是否完全填充
const isFull = checkGroupFullFill('group1', new Date('2024-01-01'), new Date('2024-01-07'));
console.log('该分组是否完全填充:', isFull);

注意事项

  • 所有涉及时间比较的地方,一定要用getTime()转化为时间戳,避免Date对象直接比较的坑
  • 如果时间轴支持跨天/跨月的元素,上述逻辑同样适用,只要确保时间戳计算正确
  • 复杂场景下(比如多元素批量调整),可以先暂停时间轴的渲染,处理完数据后再重新渲染,提升性能

内容的提问来源于stack exchange,提问作者julian.danz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:11:04