Javascript 滚动侦测导航

一个以前的小项目要改,新增一个根据时间线显示不同月份绩效的需求。
没有设计稿,想了下怎么做出来好看。就有了以下这个想法

左侧显示时间线,滑动会显示不同的年份和月份,
右侧显示不同月份的统计数据。

view

为了显示这个想法不是很蠢,所以需要做的很 酷炫
其实就是交互友好些,左右块都有相应的交互动作。


预想的效果

设想的是,点击左侧月份之后右侧会快速滚动到相应的月份,方便用户操作。
当然右侧下滑操作的时候左侧的时间线也会对应的滚动。

那么就要用到滚动侦测了,以前都是用的 UI 框架里边的,
所以这次也是直接去翻阅了这个项目所用到的 UI,MuseUI 的文档,
当然里边没有这个组件,不然也不会有这篇笔记了

去翻阅了一下 BootStrapScrollspy 源码,其实就是用到了内容元素的 offsetTop 和滚动条的监听


后端给的数据格式是类似这样的

1
2
3
4
5
6
7
8
9
10
{
date:'2020-01',
detail:{
a:100,
b:78,
total:178,
machines:967
},
note:'some text of 2020-01'
},

所以可以直接使用 computed 返回所有的月份,对,我使用的 Vue 作为框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
computed: {
timeline() {
let list = [];
const data = this.sourceData;
if (!data) return list;
data.forEach(item => {
const date = item.date.split("-");
const year = list.find(item => item.year === date[0]);
if (year) {
year.months.push(date[1]);
} else {
list.push({
year: date[0],
months: [date[1]]
});
}
});
return list;
},
},

尝试过直接使用对象,用年份作为字段名,但是使用 v-for 循环的时候会按照升序打印出来,
折腾了有一会放弃了,还是使用数组,记不得前段时间自己是怎么直接用对象实现的时间分组的了 😂 -> Js 对象 调整属性排序是否有意义
其实差不多只是匹配的时候稍微麻烦点需要用到 find() 方法。

然后也根据后台返回的数据来计算右侧内容部分每一个月份的 offsetTop

1
2
3
4
5
6
7
8
9
10
11
12
13
computed:{
offsetList() {
const list = this.$refs["month-detail"];
let data = [];
list.forEach(el => {
data.push({
date: el.getAttribute("date"),
offset: el.offsetTop - 100
});
});
return data;
}
}

这块其实很简单,直接在循环输出的时候注册 ref 即可,然后直接遍历 DOM 元素数组保存 offsetTop

然后是点击左侧时间轴右侧内容部分滚动到对应的月份,

1
2
3
4
5
6
7
8
9
10
11
12
13
methods:{
// 跳转到对应月份
toMonth(year, month) {
this.currTime = `${year}-${month}`;
const detailItem = this.offsetList.find(
item => item.date === this.currTime
);
this.$refs["wrap"].scrollTo({
top: detailItem.offset,
behavior: "smooth"
});
},
}

这里有一个 Js 的新东西 behavior: "smooth" 是原生滚动的一个新 API,应该是新 API 哈,以前都没有见到过,这次才发现有这个 Option,也可以考虑使用 CSS 来实现,但是听说兼容性堪忧。
最后加上右侧内容的滚动事件绑定 <div class="wrap" ref="wrap" @scroll="scrollSpyNav">

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
watch: {
currTime() {
this.scrollTimeline();
}
},
methods:{
// 滚动侦测导航
scrollSpyNav(e) {
clearTimeout(this.timer);
const offsetTop = e.target.scrollTop;
const curr = this.offsetList.find(item => item.offset >= offsetTop);
this.timer = window.setTimeout(() => {
this.currTime = curr.date;
}, 300);
},
// 滚动左侧时间线
scrollTimeline() {
const el = this.$refs["month-block"].find(
item => item.getAttribute("date") === this.currTime
);
this.$refs["timeline"].scrollTo({
top: el.offsetTop - 50,
behavior: "smooth"
});
},
}

直接使用了 watch 来侦听的了日期的改变,来触发左侧时间线的滚动,也实现了点击时间轴自动置顶当前月份的效果。
顺带写了个计时器,防止抖动….

🌰 DEMO

兼容

  • JavaScript - scrollTo [MDN]
    scrollTo
  • CSS - scroll-behavior:smooth [Can I use]
    scroll-behavior