<div id="app">
  <!--  时间线  -->
  <div class="timeline" ref="timeline">
    <div v-for="date in timeline" class="time" :key="date.year">
      <div
        v-for="month in date.months"
        :class="[
          'month',
          currTime === `${date.year}-${month}` ? 'active' : ''
        ]"
        :key="date.year + '-' + month"
        ref="month-block"
        :date="date.year + '-' + month"
        @click="toMonth(date.year, month)"
      >
        {{ month }}
      </div>
      <div class="year">{{ date.year }}</div>
    </div>
  </div>
  <!--  右侧内容  -->
  <div class="main" ref="main">
    <div class="wrap" ref="wrap" @scroll="scrollSpyNav">
      <div
        class="month-detail"
        ref="month-detail"
        v-for="month in sourceData"
        :key="month.date"
        :date="month.date"
      >
        <h3>{{ month.date.split("-")[1] }}月</h3>
        <div class="container">
          <div class="row">
            <div class="col col-12">
              签约数:
              <span>{{ month.detail.total }}</span>
            </div>
            <div class="col col-6">
              A类:
              <span>{{ month.detail.a }}</span>
            </div>
            <div class="col col-6">
              B类:
              <span>{{ month.detail.b }}</span>
            </div>
            <div class="col col-6 offset-6">
              总设备数:
              <span>{{ month.detail.machines }}</span>
            </div>
          </div>
        </div>
        <a class="more" href="#">查看当月明细</a>
      </div>
    </div>
  </div>
</div>
.timeline
  width 80px
  height 80%
  text-align center
  background #f6f6f6
  border-top-left-radius 5px
  border-top-right-radius 25px
  display flex
  flex-direction column
  position absolute
  left 0
  bottom 0
  z-index 10
  overflow-x hidden
  box-sizing border-box
  &::-webkit-scrollbar-thumb, &::-webkit-scrollbar
    display none
  .time
    &:first-child
      padding-top 50px
    div
      width 80%
      height 60px
      line-height @height
      font-size 25px
      font-weight bold
      transition all .4s
    .year
      width 100%
      color #969696
      background #e0e0e0
      position sticky
      bottom 0
    .month
      color #dcdcdc
      border-top #dadada 1px solid
      margin 0 auto
      position relative
      cursor pointer
      &:first-child
        border-top 0
      &.active
        color #656565
        &:after
          content 'month'
          width 100%
          height 20px
          line-height @height
          font-size 12px
          font-weight normal
          text-transform uppercase
          display block
          position absolute
          left 0
          bottom 4px
// 右侧主体内容
.main
  width 100%
  height 100%
  background white
  padding 50px 0 0 80px
  border-top-left-radius 45px
  border-top-right-radius 5px
  margin-left auto
  .wrap
    width 100%
    height 100%
    padding-bottom 50px
    overflow-x hidden
  .month-detail
    padding 10px 0
    h3
      height 45px
      line-height @height
      font-size 24px
      font-weight bold
      border-bottom #dfdfdf 1px solid
      margin-bottom 15px
    div
      line-height 30px
      color #ababab
      span
        color #333
    .more
      font-size 12px
      color #4c6486
      text-align center
      text-decoration none
      padding 15px 0
      margin -10px auto
      display block
/*
* defalut
*/
#app
  width 100%
  max-width 600px
  height 100%
  background #333
  padding 80px 0 0 35px
  margin 0 auto
  position relative
  box-sizing border-box
html,body
  width 100%
  height 100%
  background gray
::-webkit-scrollbar-thumb 
  background #333
  border-radius 3px
::-webkit-scrollbar
  width 5px
  background none
::-webkit-scrollbar-track 
::-webkit-scrollbar-button 
::-webkit-scrollbar-track-piece 
  width 0
  height 0
  background none
View Compiled
var app = new Vue({
  el: '#app',
  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;
    },
    offsetList() {
      const list = this.$refs["month-detail"];
      let data = list.map(el => {
        return {
          date: el.getAttribute("date"),
          offset: el.offsetTop - 100
        };
      });
      return data;
    }
  },
  watch: {
    currTime() {
      this.scrollTimeline();
    }
  },
  mounted() {
    this.currTime = this.sourceData[0].date;
  },
  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"
      });
    },
    // 滚动左侧时间线
    scrollTimeline() {
      const el = this.$refs["month-block"].find(
        item => item.getAttribute("date") === this.currTime
      );
      this.$refs["timeline"].scrollTo({
        top: el.offsetTop - 50,
        behavior: "smooth"
      });
    },
    // 滚动侦测导航
    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);
    }
  },
  data() {
    return {
      sourceData: [
        {
          date: "2020-01",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2020-01"
        },
        {
          date: "2019-12",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-12"
        },
        {
          date: "2019-11",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-11"
        },
        {
          date: "2019-10",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-10"
        },
        {
          date: "2019-09",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-09"
        },
        {
          date: "2019-08",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-08"
        },
        {
          date: "2019-07",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-07"
        },
        {
          date: "2019-06",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-06"
        },
        {
          date: "2019-05",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-05"
        },
        {
          date: "2019-04",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-04"
        },
        {
          date: "2019-03",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-03"
        },
        {
          date: "2019-02",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-02"
        },
        {
          date: "2019-01",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2019-01"
        },
        {
          date: "2018-12",
          detail: {
            a: 100,
            b: 78,
            total: 178,
            machines: 967
          },
          note: "some text of 2018-12"
        }
      ],
      timer: null,
      currTime: ""
    };
  }
})

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css
  2. https://unpkg.com/bootstrap@4.4.1/dist/css/bootstrap-grid.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js