一个响应式时间轴功能

280 阅读5分钟

一个响应式时间轴功能

效果图:

image.png

image.png

Timeline.vue组件

template:

<section id="timeline" class="timeline bs-timeline">
    <div class="prev" @click="handlePrev">&lt;</div>
    <div class="line"></div>
    <div class="time-main">
        <div class="time-content">
            <div v-for="(item,index) in timeList"
                 :key="new Date(item.Date).getTime()"
                 :data-date="item.Date"
                 :data-index="index"
                 :style="'margin-left:' + marginLeft + 'px'"
                 :class="item.className"
                 v-html="item.contentText">
            </div>
        </div>
    </div>
    <div class="next" @click="handleNext">&gt;</div>
</section>

script:

主要使用 ResizeObserver API, ResizeObserver 是浏览器提供的一种新的 API,用于监听一个元素内容区域或盒模型大小的变化,当元素大小发生变化时会触发回调函数。

// ResizeObserver 使用示例代码:
const ro = new ResizeObserver(entries => {
  for (let entry of entries) {
      // 通过 contentRect 属性获取元素的新尺寸
    const { width, height } = entry.contentRect;
    console.log(`Size changed to ${width} x ${height}`);
  }
});
ro.observe(document.body);

在老旧浏览器上使用 ResizeObserver,则可以借助 polyfill 实现兼容,如resize-observer-polyfill 库。

其中script代码

<script>
export default {
  name: "TimeLine",
  data() {
    return {
      dataList: [
        {
          Code: "hospital",
          Text: "(☛)",
          Date: "2023-04-10"
        },
        {
          Code: "hospital",
          Text: "(☚)",
          Date: "2023-04-15"
        }
      ],
      // 时间轴日期列表
      timeList: [{ Date: "2022-10-10 10:10:10" }, { Date: "2022-10-04" }],
      // 当前日期
      curDate: new Date(),
      dayNum: 1,
      marginLeft: 5,
      color: {
        outpatient: "rgba(255,154,38,1)", 
        hospital: "rgba(255,61,64,1)"
      },
      end_date: "", // 截止日期
      first_date: "", // 开始日期
      max_date: "", // 最大日期
      min_date: "", // 最小日期
      resizeObserver: null,
    };
  },
  mounted() {
    let me = this,
      el = document.querySelector("#timeline");
    me.mainEl = el.querySelector(".time-main");
    me.prevEl = el.querySelector(".prev");
    me.nextEL = el.querySelector(".next");
    me.renderTimeline();
  },
  beforeDestroy() {
    let me = this;
    me.resizeObserver.unobserve(me.mainEl);
  },
  methods: {
    handlePrev(e) {
      let me = this;
      if (me.compareTwoDate(me.first_date, me.min_date) <= 0) {
        e.target.classList.remove("active");
        return;
      }
      me.curDate = me.getNeighbDate(me.curDate, -1);
      me.renderTimeline();
    },
    handleNext(e) {
      let me = this;
      if (me.compareTwoDate(me.end_date, me.max_date) >= 0) {
        e.target.classList.remove("active");
        return;
      }
      me.curDate = me.getNeighbDate(me.curDate, +1);
      me.renderTimeline();
    },
    // 创建渲染时间轴
    renderTimeline() {
      let me = this;
      me.dataList = me.dateReverseOrder(me.dataList, "Date");
      if (me.dataList.length <= 0) {
        me.contentResize();
        return;
      }
      let len = me.dataList.length;
      me.max_date = me.formatDate(me.getNMonth(new Date(), 1));
      // 计算传入数据中日期最小的一天
      me.min_date = me.formatDate(me.getNMonth(me.dataList[len - 1].Date, 0));

      me.contentResize();
    },
    // 监听时间轴元素对象resize
    contentResize: function() {
      let me = this;
      me.calcContent();
      me.resizeObserver = new ResizeObserver(me.handleResize)
      me.resizeObserver.observe(me.mainEl)
    },
    handleResize: function() {
      let me = this;
      me.throttle(me.calcContent());
    },
    // 计算
    calcContent: function() {
      let me = this;
      let content_width = me.mainEl.clientWidth;
      let mon_width = 54 + 30 * me.marginLeft;
      let r = Math.floor(content_width / mon_width);
      me.dayNum = r - 1;
      me.render_func();
    },
    render_func() {
      let me = this;
      let date = new Date(me.curDate);
      // 截止到当前日期的下个月
      me.end_date = me.formatDate(me.getNMonth(date, +1));
      // 开始日期当月
      me.first_date = me.formatDate(me.getNMonth(date, -me.dayNum));

      me.hasArrowActive();

      let result = me.getBetweenDateStr(me.first_date, me.end_date);
      me.timeList = result;
      me.renderTag(result);
    },
    // 节流
    throttle: function(fn) {
      let t1 = 0;
      return function() {
        let t2 = new Date();
        if (t2 - t1 > 2000) {
          fn.apply(this, arguments);
          t1 = t2;
        }
      };
    },
    renderTag(result) {
      let me = this;
      result.forEach(item => {
        let curr_date = item.Date;
        let tmp_month = me.getMonth(curr_date);
        let date_day = curr_date.slice(8, 10);

        item.className = "axis";
        item.contentText = "";
        // 1号
        if (date_day === "01") {
          if (tmp_month === 1 || tmp_month === 6) {
            let tmp_year = me.getYear(curr_date);
            tmp_month = tmp_year + "年" + tmp_month;
          }
          item.className = "circleAxis";
          item.contentText = `<div class="fontAxis boldFont">${tmp_month}月</div>`;
        }
        // 10号 20号
        if (date_day === "10" || date_day === "20") {
          item.className = "axis boldAxis";
          item.contentText = `<div class="fontAxis">${date_day}</div>`;
        }
        // 判断传入的数据是否可视时间轴内
        let curDataItem = me.filterData(curr_date);
        if (curDataItem && Object.keys(curDataItem).length > 0) {
          item.className = "axis axisHeight";
          let color = me.color[curDataItem.Code],
            bgColor = color.replace(",1)", ",0.3)");
          item.contentText += `<div class="bubbleAxis" 
                            data-id="${curr_date}" 
                            style="background-color: ${color}">
                        <div class="bubbleAxis-inner">${date_day}</div>
                    </div>`;
          item.contentText += `<div class="bubbleText" data-id="${curDataItem.Text}"
                    style="color: ${color}; background-color: ${bgColor};" >
                    ${curDataItem.Text}</div>`;
        }
      });
    },
    // 添加上下按钮添加active状态
    hasArrowActive: function() {
      let me = this;
      if (me.compareTwoDate(me.first_date, me.min_date) > 0) {
        me.prevEl.classList.add("active");
      }
      if (me.compareTwoDate(me.end_date, me.max_date) < 0) {
        me.nextEL.classList.add("active");
      }
    },
    // 获取指定日期的上下n月的日期
    getNeighbDate: function(date, n) {
      let nowdate = new Date(date);
      nowdate.setMonth(nowdate.getMonth() + n);
      let y = nowdate.getFullYear();
      let m = nowdate.getMonth() + 1;
      let d = nowdate.getDate();
      return y + "-" + m + "-" + d;
    },

    // 传入的对象数组根据日期倒叙
    dateReverseOrder: function(data, type) {
      return data.sort(function(a, b) {
        return a[type] < b[type] ? 1 : -1;
      });
    },

    // 比较两个日期的大小
    compareTwoDate: function(date1, date2) {
      let date1_s = new Date(date1).toLocaleDateString();
      let date2_s = new Date(date2).toLocaleDateString();
      return Date.parse(date1_s) - Date.parse(date2_s);
    },

    getMonth: function(date) {
      date = new Date(date);
      return date.getMonth() + 1;
    },

    getYear: function(date) {
      date = new Date(date);
      return date.getFullYear();
    },

    // 过滤数据
    filterData: function(param) {
      let me = this;
      let idx = me.dataList.findIndex(function(e) {
        return me.isSameDay(e.Date, param);
      });
      let result = me.dataList[idx];

      return result;
    },

    // 判断两个日期是否是同一天
    isSameDay: function(start, end) {
      let startMs = new Date(start).setHours(0, 0, 0, 0);
      let endMs = new Date(end).setHours(0, 0, 0, 0);
      return startMs === endMs;
    },

    // 获取当前日期的上、下n个月的第一天
    getNMonth: function(date, n) {
      date = new Date(date);
      return new Date(date.getFullYear(), date.getMonth() + n, 1);
    },

    // 生成两个日期之间每天的日期的对象数组
    getBetweenDateStr(start, end) {
      let me = this;
      if (me.isSameDay(start, end)) {
        return [{ Date: start }];
      }
      let result = [];
      let beginDay = start.split("-");
      let endDay = end.split("-");
      let diffDay = new Date();
      let dateList = new Array();
      let i = 0;
      diffDay.setDate(beginDay[2]);
      diffDay.setMonth(beginDay[1] - 1);
      diffDay.setFullYear(beginDay[0]);
      result.push({
        Date: start
      });
      while (i == 0) {
        let countDay = diffDay.getTime() + 24 * 60 * 60 * 1000;
        diffDay.setTime(countDay);
        dateList[2] = diffDay.getDate();
        dateList[1] = diffDay.getMonth() + 1;
        dateList[0] = diffDay.getFullYear();
        if (String(dateList[1]).length == 1) {
          dateList[1] = "0" + dateList[1];
        }
        if (String(dateList[2]).length == 1) {
          dateList[2] = "0" + dateList[2];
        }
        result.push({
          Date: dateList[0] + "-" + dateList[1] + "-" + dateList[2]
        });
        if (
          dateList[0] == endDay[0] &&
          dateList[1] == endDay[1] &&
          dateList[2] == endDay[2]
        ) {
          i = 1;
        }
      }
      return result;
    },

    // 格式化日期为xxxx-yy-mm
    formatDate: function(date) {
      let fmtNum = function(n) {
        n = n.toString();
        return n[1] ? n : "0" + n;
      };
      if (date) {
        date = new Date(date);
        let year = date.getFullYear();
        let month = date.getMonth() + 1;
        let day = date.getDate();
        return [year, month, day].map(fmtNum).join("-");
      }
    }
  }
};
</script>

style:

vue中动态字符串模板中的样式

可以通过给样式添加 /deep/、>>> 或者 ::v-deep 等关键字,来强制样式穿透到子组件中去

<style scoped>
.bs-timeline {
  width: 100%;
  height: 100%;
  position: relative;
}
.bs-timeline .prev {
  left: 10px;
}
.bs-timeline .next {
  right: 10px;
}
.bs-timeline .prev,
.bs-timeline .next,
.bs-timeline .line,
.bs-timeline .time-main {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
.bs-timeline .prev,
.bs-timeline .next {
  width: 20px;
  height: 20px;
  line-height: 20px;
  border-radius: 5px;
  color: #bbb;
  background-color: #fff;
  text-align: center;
  box-shadow: 0px 0px 3px #d8d3d3;
  cursor: pointer;
  user-select: none;
}

.bs-timeline .prev.active,
.bs-timeline .next.active {
  color: #0d99e4;
  transform: scale(1.1) translateY(-50%);
}

.bs-timeline /deep/ .line {
  height: 1px;
  width: calc(100% - 60px);
  background-color: #cae8f7;
  left: 30px;
}

.bs-timeline /deep/ .time-main {
  width: calc(100% - 70px);
  height: 80px;
  margin: 0 auto;
  left: 35px;
}

.bs-timeline /deep/ .time-content {
  width: 100%;
  height: 100%;
  display: flex;
  flex-wrap: nowrap;
  justify-content: space-between;
}

.bs-timeline /deep/ .axis {
  height: 20px;
  margin-top: 30px;
  border-right: 1px solid #cae8f7;
  position: relative;
  font-size: 14px;
}

.bs-timeline /deep/ .axisHeight {
  height: 50px !important;
}

.bs-timeline /deep/ .circleAxis {
  width: 4px !important;
  height: 4px !important;
  margin-top: 34px;
  border-radius: 50%;
  border: 4px solid #0d99e4;
  position: relative;
  box-sizing: content-box;
}

.bs-timeline /deep/ .circleAxis .fontAxis {
  top: 12px !important;
}
.bs-timeline /deep/ .boldFont {
  font-weight: bold;
}
.bs-timeline /deep/ .fontAxis {
  position: absolute;
  font-size: 12px;
  white-space: nowrap;
  position: absolute;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
}
.bs-timeline /deep/ .boldAxis {
  border-right: 2px solid #cae8f7 !important;
}

.bs-timeline /deep/ .bubbleAxis {
  position: absolute;
  top: -15px;
  width: 18px;
  height: 18px;
  text-align: center;
  background: rgb(247, 84, 9);
  border-radius: 50% 50% 2% 50%;
  transform: rotate(45deg) translateX(-65%);
  left: 50%;
  cursor: pointer;
}
.bs-timeline /deep/ .bubbleAxis .bubbleAxis-inner {
  color: #ffffff;
  transform: rotate(-45deg);
}

.bs-timeline /deep/ .bubbleText {
  padding: 0 2px;
  position: absolute;
  bottom: -20px;
  transform: translateX(-35%);
  left: 50%;
  cursor: pointer;
  text-align: center;
  border-radius: 5px;
  white-space: nowrap;
  pointer-events: none;
}

.bs-timeline .prev:hover,
.bs-timeline .next:hover {
  transform: scale(1.1) translateY(-50%) scale(1.1);
  box-shadow: #333 0px 0px 3px;
}

.bs-timeline /deep/ .axisHeight:hover {
  height: 68px !important;
  transform: translateX(-65%);
  z-index: 9;
}

.bs-timeline .prev,
.bs-timeline .next,
.bs-timeline .time-main,
.bs-timeline .line {
  margin-top: -10px;
}
</style>

优化后的代码:

<template>
    <section id="timeline" class="timeline bs-timeline">
        <div class="prev" @click="handlePrev">&lt;</div>
        <div class="line"></div>
        <div class="time-main">
            <div class="time-content">
                <div
                    v-for="(item, index) in timeList"
                    :key="new Date(item.Date).getTime()"
                    :data-date="item.Date"
                    :data-index="index"
                    :style="'margin-left:' + marginLeft + 'px'"
                    :class="item.className"
                    v-html="item.contentText"></div>
            </div>
        </div>
        <div class="next" @click="handleNext">&gt;</div>
    </section>
</template>
<script>
import './TimelineResize';
export default {
    name: 'TimeLine',
    data() {
        return {
            dataList: [
                {
                    Code: 'hospital',
                    Text: '(☛)',
                    Date: '2023-04-10'
                },
                {
                    Code: 'hospital',
                    Text: '(☚)',
                    Date: '2023-04-15'
                }
            ],
            // 时间轴日期列表
            timeList: [{Date: '2022-10-10 10:10:10'}, {Date: '2022-10-04'}],
            // 当前日期
            curDate: new Date(),
            dayNum: 1,
            marginLeft: 5,
            color: {
                outpatient: 'rgba(255,154,38,1)',
                hospital: 'rgba(255,61,64,1)'
            },
            endDate: '',
            firstDate: '',
            maxDate: '',
            minDate: '',
            resizeObserver: null
        };
    },
    mounted() {
        let me = this;
        console.log('this.$el--', this.$el);
        let el = this.$el;
        me.mainEl = el.querySelector('.time-main');
        me.prevEl = el.querySelector('.prev');
        me.nextEL = el.querySelector('.next');
        me.renderTimeline();
    },
    beforeDestroy() {
        let me = this;
        me.resizeObserver.unobserve(me.mainEl);
    },
    methods: {
        handlePrev(e) {
            let me = this;
            if (me.compareTwoDate(me.firstDate, me.minDate) <= 0) {
                e.target.classList.remove('active');
                return;
            }
            me.curDate = me.getNeighbDate(me.curDate, -1);
            me.renderTimeline();
        },
        handleNext(e) {
            let me = this;
            if (me.compareTwoDate(me.endDate, me.maxDate) >= 0) {
                e.target.classList.remove('active');
                return;
            }
            me.curDate = me.getNeighbDate(me.curDate, +1);
            me.renderTimeline();
        },
        // 创建渲染时间轴
        renderTimeline() {
            let me = this;
            me.dataList = me.dateReverseOrder(me.dataList, 'Date');
            if (me.dataList.length <= 0) {
                me.contentResize();
                return;
            }
            let len = me.dataList.length;
            me.maxDate = me.formatDate(me.getNMonth(new Date(), 1));
            // 计算传入数据中日期最小的一天
            me.minDate = me.formatDate(me.getNMonth(me.dataList[len - 1].Date, 0));

            me.contentResize();
        },
        // 监听时间轴元素对象resize
        contentResize() {
            let me = this;
            me.calcContent();
            me.resizeObserver = new ResizeObserver(me.handleResize);
            me.resizeObserver.observe(me.mainEl);
        },
        handleResize() {
            let me = this;
            me.throttle(me.calcContent());
        },
        // 计算
        calcContent() {
            let me = this;
            let contentWidth = me.mainEl.clientWidth;
            let monWidth = 54 + 30 * me.marginLeft;
            let r = Math.floor(contentWidth / monWidth);
            me.dayNum = r - 1;
            me.renderFunc();
        },
        renderFunc() {
            let me = this;
            let date = new Date(me.curDate);
            // 截止到当前日期的下个月
            me.endDate = me.formatDate(me.getNMonth(date, +1));
            // 开始日期当月
            me.firstDate = me.formatDate(me.getNMonth(date, -me.dayNum));

            me.hasArrowActive();

            let result = me.getBetweenDateStr(me.firstDate, me.endDate);
            me.timeList = result;
            me.renderTag(result);
        },
        // 节流
        throttle(fn) {
            let t1 = 0;
            // eslint-disable-next-line space-before-function-paren
            return function () {
                let t2 = new Date();
                if (t2 - t1 > 2000) {
                    // eslint-disable-next-line prefer-rest-params
                    fn.apply(this, arguments);
                    t1 = t2;
                }
            };
        },
        renderTag(result) {
            let me = this;
            result.forEach(item => {
                let currDate = item.Date;
                let tmpMonth = me.getMonth(currDate);
                let dateDay = currDate.slice(8, 10);

                item.className = 'axis';
                item.contentText = '';
                // 1号
                if (dateDay === '01') {
                    if (tmpMonth === 1 || tmpMonth === 6) {
                        let tmpYear = me.getYear(currDate);
                        tmpMonth = tmpYear + '年' + tmpMonth;
                    }
                    item.className = 'circleAxis';
                    item.contentText = `<div class="fontAxis boldFont">${tmpMonth}月</div>`;
                }
                // 10号 20号
                if (dateDay === '10' || dateDay === '20') {
                    item.className = 'axis boldAxis';
                    item.contentText = `<div class="fontAxis">${dateDay}</div>`;
                }
                // 判断传入的数据是否可视时间轴内
                let curDataItem = me.filterData(currDate);
                if (curDataItem && Object.keys(curDataItem).length > 0) {
                    item.className = 'axis axisHeight';
                    let color = me.color[curDataItem.Code];
                    let bgColor = color.replace(',1)', ',0.3)');
                    item.contentText += `<div class="bubbleAxis" 
                            data-id="${currDate}" 
                            style="background-color: ${color}">
                        <div class="bubbleAxis-inner">${dateDay}</div>
                    </div>`;
                    item.contentText += `<div class="bubbleText" data-id="${curDataItem.Text}"
                    style="color: ${color}; background-color: ${bgColor};" >
                    ${curDataItem.Text}</div>`;
                }
            });
        },
        // 添加上下按钮添加active状态
        hasArrowActive() {
            let me = this;
            if (me.compareTwoDate(me.firstDate, me.minDate) > 0) {
                me.prevEl.classList.add('active');
            }
            if (me.compareTwoDate(me.endDate, me.maxDate) < 0) {
                me.nextEL.classList.add('active');
            }
        },
        // 获取指定日期的上下n月的日期
        getNeighbDate(date, n) {
            let nowdate = new Date(date);
            nowdate.setMonth(nowdate.getMonth() + n);
            let y = nowdate.getFullYear();
            let m = nowdate.getMonth() + 1;
            let d = nowdate.getDate();
            return y + '-' + m + '-' + d;
        },

        // 传入的对象数组根据日期倒叙
        dateReverseOrder(data, type) {
            return data.sort((a, b) => (a[type] < b[type] ? 1 : -1));
        },

        // 比较两个日期的大小
        compareTwoDate(date1, date2) {
            let date1S = new Date(date1).toLocaleDateString();
            let date2S = new Date(date2).toLocaleDateString();
            return Date.parse(date1S) - Date.parse(date2S);
        },

        getMonth(date) {
            date = new Date(date);
            return date.getMonth() + 1;
        },

        getYear(date) {
            date = new Date(date);
            return date.getFullYear();
        },

        // 过滤数据
        filterData(param) {
            let me = this;
            let idx = me.dataList.findIndex(e => me.isSameDay(e.Date, param));
            let result = me.dataList[idx];

            return result;
        },

        // 判断两个日期是否是同一天
        isSameDay(start, end) {
            let startMs = new Date(start).setHours(0, 0, 0, 0);
            let endMs = new Date(end).setHours(0, 0, 0, 0);
            return startMs === endMs;
        },

        // 获取当前日期的上、下n个月的第一天
        getNMonth(date, n) {
            date = new Date(date);
            return new Date(date.getFullYear(), date.getMonth() + n, 1);
        },

        // 生成两个日期之间每天的日期的对象数组
        getBetweenDateStr(startDate, endDate) {
            let start = new Date(startDate);
            let end = new Date(endDate);
            for (var arr = [], dt = new Date(start); dt <= end; dt.setDate(dt.getDate() + 1)) {
                arr.push({
                    Date: this.formatDate(new Date(dt))
                });
            }
            return arr;
        },

        // 格式化日期为xxxx-yy-mm
        formatDate(date) {
            // eslint-disable-next-line space-before-function-paren
            let fmtNum = function (n) {
                n = n.toString();
                return n[1] ? n : '0' + n;
            };
            if (date) {
                date = new Date(date);
                let year = date.getFullYear();
                let month = date.getMonth() + 1;
                let day = date.getDate();
                return [year, month, day].map(fmtNum).join('-');
            }
        }
    }
};
</script>
<style lang="scss" scoped>
.bs-timeline {
    width: 100%;
    height: 100%;
    position: relative;
}
.bs-timeline .prev {
    left: 10px;
}
.bs-timeline .next {
    right: 10px;
}
.bs-timeline .prev,
.bs-timeline .next,
.bs-timeline .line,
.bs-timeline .time-main {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
}
.bs-timeline .prev,
.bs-timeline .next {
    width: 20px;
    height: 20px;
    line-height: 20px;
    border-radius: 5px;
    color: #bbb;
    background-color: #fff;
    text-align: center;
    box-shadow: 0px 0px 3px #d8d3d3;
    cursor: pointer;
    user-select: none;
}

.bs-timeline .prev.active,
.bs-timeline .next.active {
    color: #0d99e4;
    transform: scale(1.1) translateY(-50%);
}

.bs-timeline ::v-deep .line {
    height: 1px;
    width: calc(100% - 60px);
    background-color: #cae8f7;
    left: 30px;
}

.bs-timeline ::v-deep .time-main {
    width: calc(100% - 70px);
    height: 80px;
    margin: 0 auto;
    left: 35px;
}

.bs-timeline ::v-deep .time-content {
    width: 100%;
    height: 100%;
    display: flex;
    flex-wrap: nowrap;
    justify-content: space-between;
}

.bs-timeline ::v-deep .axis {
    height: 20px;
    margin-top: 30px;
    border-right: 1px solid #cae8f7;
    position: relative;
    font-size: 14px;
}

.bs-timeline ::v-deep .axisHeight {
    height: 50px !important;
}

.bs-timeline ::v-deep .circleAxis {
    width: 4px !important;
    height: 4px !important;
    margin-top: 34px;
    border-radius: 50%;
    border: 4px solid #0d99e4;
    position: relative;
    box-sizing: content-box;
}

.bs-timeline ::v-deep .circleAxis .fontAxis {
    top: 12px !important;
}
.bs-timeline ::v-deep .boldFont {
    font-weight: bold;
}
.bs-timeline ::v-deep .fontAxis {
    position: absolute;
    font-size: 12px;
    white-space: nowrap;
    position: absolute;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
}
.bs-timeline ::v-deep .boldAxis {
    border-right: 2px solid #cae8f7 !important;
}

.bs-timeline ::v-deep .bubbleAxis {
    position: absolute;
    top: -15px;
    width: 18px;
    height: 18px;
    text-align: center;
    background: rgb(247, 84, 9);
    border-radius: 50% 50% 2% 50%;
    transform: rotate(45deg) translateX(-65%);
    left: 50%;
    cursor: pointer;
}
.bs-timeline ::v-deep .bubbleAxis .bubbleAxis-inner {
    color: #ffffff;
    transform: rotate(-45deg);
}

.bs-timeline ::v-deep .bubbleText {
    padding: 0 2px;
    position: absolute;
    bottom: -20px;
    transform: translateX(-35%);
    left: 50%;
    cursor: pointer;
    text-align: center;
    border-radius: 5px;
    white-space: nowrap;
    pointer-events: none;
}

.bs-timeline .prev:hover,
.bs-timeline .next:hover {
    transform: scale(1.1) translateY(-50%) scale(1.1);
    box-shadow: #333 0px 0px 3px;
}

.bs-timeline ::v-deep .axisHeight:hover {
    height: 68px !important;
    transform: translateX(-65%);
    z-index: 9;
}

.bs-timeline .prev,
.bs-timeline .next,
.bs-timeline .time-main,
.bs-timeline .line {
    margin-top: -10px;
}
</style>