swiper的控制!!!

如何做「时间轴」&&「tab切换」这种轮播图效果??

Posted by My on January 5, 2026

一言难尽!!! 有一种很常见的效果,即一部分是「时间轴」,另一部分是「内容区」,他们之间相互控制。这种效果也常常出现在「tab」切换中。

思考 🧐

首先就是想到了老插件 Swiper。原先我脑子里还没有「双向控制」的画面,最初的思路是 ———— 时间轴使用「swiper」,而内容区使用普通的元素,通过「激活项」切换内容。

看到了产品给的例子,慢慢发现不是这么简单。。。 不是单纯的内容切换,不是一个切换后另一个再切换,不是设置 activeIndex,是「极致」的同步。 👋 因此在文档里踏上了探索之路。。。。。。

目标 🎯

点击 「时间轴」的项、拖动、左右按钮和拖动时,「内容区」则同步滚动;拖动「内容区」,则「时间轴」也同步滚动。

在探索过程中,走了好多「弯路」,如下面的

设置活动项 ❌

思路是在「时间轴」切换后,通过 slideNextTransitionStart 回调来设置「内容区」的活动项。

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
const initSwiper = () => {
  if (!cardSwiperRef.current) return;
  cardSwiperInstance.current = new Swiper(cardSwiperRef.current, {
    grabCursor: true,
    loop: true,
  });
  if (!dateSwiperRef.current) return;
  dateSwiperInstance.current = new Swiper(dateSwiperRef.current, {
    modules: [Navigation],
    slidesPerView: 5,
    spaceBetween: 10,
    centeredSlides: true,
    slideToClickedSlide: true,
    loop: true,
    navigation: {
      prevEl: ".swiper-button-prev",
      nextEl: ".swiper-button-next",
    },
    on: {
      slideNextTransitionStart: (e) => {
        cardSwiperInstance.current?.slideToLoop(e.realIndex);
      },
    },
  });
};

这种方式确实能实现内容的切换,但是有个弊端和瑕疵

  • 弊端

    loop 模式下,slideToLoop 会寻找最近的「下标」,即当到达源数组的最后一项时(2025),下一步要进入 2015,此时「内容区」不会往下走进入 2015,而是会往后走,回到前面的 2015。这种交互体验上并不友好,当然有「邪修」的做法,就想苹果的时钟一样,做个假的循环,复制多份数据数据 🙊

  • 瑕疵

    内容区也要监听 slideNextTransitionStart 设置 「时间轴」的活动项,因此就进入了循环引用、数据不同步等问题,另外这是自身切换后,再让另一个切换,即有先后顺序,并不是期望的同步。

设置位移 ❌

通过监听自身的位移,然后使用方法如 setTranslate 设置对方的位移等,在实操过程过,普通项之间还基本能实现,但是对于「循环」时,如【2024】-> 【2025】-> 【2015】,则是实现不了。

双向控制 ❌

查阅文档,发现 双向控制 这个功能很满足要求,也是进行了试验。

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
27
28
29
const initSwiper = () => {
  if (!cardSwiperRef.current) return;
  const w = cardSwiperRef.current.clientWidth;
  const x = -w * 2;
  cardSwiperInstance.current = new Swiper(cardSwiperRef.current, {
    modules: [Controller],
    slidesPerView: 1,
    spaceBetween: 10,
    grabCursor: true,
    loop: true,
  });
  if (!dateSwiperRef.current) return;
  dateSwiperInstance.current = new Swiper(dateSwiperRef.current, {
    modules: [Navigation, Controller],
    slidesPerView: 5,
    spaceBetween: 10,
    centeredSlides: true,
    slideToClickedSlide: true,
    loop: true,
    navigation: {
      prevEl: ".swiper-button-prev",
      nextEl: ".swiper-button-next",
    },
  });
  if (cardSwiperInstance.current && dateSwiperInstance.current) {
    cardSwiperInstance.current.controller.control = dateSwiperInstance.current;
    dateSwiperInstance.current.controller.control = cardSwiperInstance.current;
  }
};

发现当两个 swiperslidesPerView 不相等的情况下,虽然能互相控制,但是「活动项」并不同步。文档中说明了 by 属性,明明是默认 slide ,即自身切换一项,被控制方也切换一项,但是实际就是不同步。

缩略图 ❌

再往下查文档,双向控制的例子是 一对一,那多对一的模式则是使用 缩略图,官网也是推荐使用这种模式。

但是在观察例子和实操过程中,发现这种方式并不是我所需要的。缩略图强调的是「激活项」同步,在点击缩略图时,缩略图并不会移动,拖动内容区时,缩略图也不会移动。同时拖动缩略图时,也不会激活某一项。

抓耳挠腮 ❓❓

说实话,我已经研究了好多天了,毫无头绪,上面几种是最突出的方式,另外还有其他瑕疵。

  • slidesPerView:7时,原数据过少导致无法下一步;
  • 开启「居中」时,左边或者右边空白等;

还有一些邪修做法,如使用最普通的模式,「时间轴」不居中,而内容区也是普通的,但是数据的顺序不同。

1
2
const list1 = ["2018", "2019", "2020", "2021", "2022", "2023", "2024"];
const list2 = ["2020", "2021", "2022", "2023", "2024", "2018", "2019"];

在视图上让他们看起来是居中的,但是时间轴的激活项还是在左边第一个,但是弊端也多,需要做个假的激活项,让中间项高亮,另外第一个无法点击,点击其他的项无法滚动到中间等等。。。

😫😫😫 难道就到此为止了吗?????

最后的方案 ✅

还是使用 Controller 这个模式,依然遵循 slidesPerView 相同的原则,不过有两个属性至关重要: slidesOffsetBeforeslidesOffsetBefore设定预设偏移量

这是我不小心发现的

一样的结构,使用「居中」,对内容区使用偏移量的效果

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
27
28
29
30
const initSwiper = () => {
  if (!cardSwiperRef.current) return;
  cardSwiperInstance.current = new Swiper(cardSwiperRef.current, {
    modules: [Controller],
    slidesPerView: 5,
    spaceBetween: 10,
    grabCursor: true,
    centeredSlides: true,
    loop: true,
    slidesOffsetBefore: 100, // 偏移量
    slidesOffsetAfter: 100, // 偏移量
  });
  if (!dateSwiperRef.current) return;
  dateSwiperInstance.current = new Swiper(dateSwiperRef.current, {
    modules: [Navigation, Controller],
    slidesPerView: 5,
    spaceBetween: 10,
    centeredSlides: true,
    slideToClickedSlide: true,
    loop: true,
    navigation: {
      prevEl: ".swiper-button-prev",
      nextEl: ".swiper-button-next",
    },
  });
  if (cardSwiperInstance.current && dateSwiperInstance.current) {
    cardSwiperInstance.current.controller.control = dateSwiperInstance.current;
    dateSwiperInstance.current.controller.control = cardSwiperInstance.current;
  }
};

那如果偏移量足够大,把两边的内容都「挤走」,只留下中间的一项呢??

通过调试后发现,可视数量是 5 张,两边的张数是 2,而当中间只有一张时(整个 swiper 的宽度),则需要偏移 2 个 swiper 的宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const initSwiper = () => {
  if (!cardSwiperRef.current) return;
  const w = cardSwiperRef.current.clientWidth; // 一个内容区的宽度
  const x = -w * 2; // 偏移量
  cardSwiperInstance.current = new Swiper(cardSwiperRef.current, {
    modules: [Controller],
    slidesPerView: 5,
    spaceBetween: 10,
    grabCursor: true,
    centeredSlides: true,
    loop: true,
    slidesOffsetBefore: x,
    slidesOffsetAfter: x,
  });
  // ...其他代码
};

说实话,这也算是「邪修」做法了,但至少能很好地实现需求。我查过很多社区,包括ai等,都找不到「官方正统」的做法…

那这种方式,不仅仅是在「时间轴」上,在一些「tab」切换内容区的需求上,也是能很好适用的。