自定义Timeline组件(基于Element-Plus风格)

概要

由于个人项目是基于Element-Plus风格进行开发,在写前端代码时候使用官方组件总感觉达不到心里的预期,索性就自己写了一个Timeline组件。

效果展示

整体效果展示:

Timeline组件展示录屏

使用语言

  1. Vue 3
  2. Css
  3. JavaScript
  4. Html

实现代码

HTML:

 <div
        style="
          position: relative;
          width: 100%;
          display: flex;
          justify-content: center;
          align-items: center;
        "
      >
        <div style="display: flex" class="timeline">
          <div
            class="timeline-card"
            v-for="(timeline, index) in timelines"
            :key="timeline"
            :ref="index"
          >
            <div
              class="milestone-card"
              @mouseover="TimelineAnimateIn(index)"
              @mouseleave="TimelineAnimateOut(index)"
            >
              <img
                :src="timeline.picture"
                style="width: 100%; height: 100%; border-radius: 5px"
              />
              <div class="dark-mask" ref="darkMask"></div>
              <div class="milestone-time">
                <span class="milestone-data">{{ timeline.data }}</span>
                <div
                  :ref="timeline.name"
                  class="animate__animated"
                  style="opacity: 0"
                >
                  <p>{{ timeline.text }}</p>
                </div>
                <div
                  :ref="timeline.name"
                  class="animate__animated"
                  style="margin-top: 30%; margin-bottom: 8%; opacity: 0"
                >
                  <el-button type="primary" size="small">了解更多</el-button>
                </div>
              </div>
            </div>
            <div
              style="position: relative; display: flex; justify-content: center"
            >
              <div class="line-node"></div>
              <div class="grey-line"></div>
            </div>
          </div>
        </div>
      </div>

CSS:

:root {
  --timeline-card-height: 300px;
}

.milestone-card {
  width: var(--timeline-card-height);
  height: 375px;
  position: relative;
  margin-bottom: 30px;
  overflow: hidden;
  border-radius: 5px;
}

.milestone-time {
  position: absolute;
  display: flex;
  align-items: center;
  width: 100%;
  bottom: 10px;
  color: white;
  /* z-index: 1; */
  line-height: 18px;
  transition: all 0.1s linear;
  flex-direction: column;
}

.milestone-animate-expand {
  animation: expand 0.2s forwards ease-in;
}

@keyframes expand {
  100% {
    transform: translateY(50%);
  }
}

.grey-line {
  --margin-width: 20px;
  background-color: #e4e7ed;
  border-radius: 5px;
  position: absolute;
  height: 2px;
  left: 50%;
  bottom: 6px;
}

.line-node {
  border-radius: 50%;
  height: 12px;
  width: 12px;
  background-color: #e4e7ed;
  position: absolute;
  bottom: 0px;
  transition: all 0.5 linear;
  cursor: pointer;
  z-index: 10;
}

.line-node:hover {
  transform: scale(1.1);
}
.timeline {
  position: relative;
}

.timeline .timeline-card {
  position: absolute;
  transform: translate(-50%, -50%);
  z-index: 1;
}

.timeline .timeline-card:last-child .grey-line {
  display: none;
}

.dark-mask {
  position: absolute;
  background: rgba(101, 101, 101, 0.6);
  height: 0%;
  width: 100%;
  opacity: 0;
  bottom: 0px;
  transition: all 0.5s linear;
}

.milestone-card:hover .dark-mask {
  height: 100%;
  opacity: 1;
}

JavaScript:

data() {
    return {
      timelines: [
        {
          name: "c1",
          data: "2022.12.12",
          picture: require("@/assets/image/c-1.jpg"),
          text: "但行前路,不负韶华!",
        },
        {
          name: "c2",
          data: "2022.02.06",
          picture: require("@/assets/image/c-2.jpg"),
          text: "既然选择远方,当不负青春,砥砺前行。",
        },
        {
          name: "c3",
          data: "2022.04.25",
          picture: require("@/assets/image/c-3.jpg"),
          text: "以蝼蚁之行,展鸿鹄之志。",
        },
        {
          name: "c4",
          data: "2022.06.29",
          picture: require("@/assets/image/c-4.jpg"),
          text: "抱怨身处黑暗,不如提灯前行",
        },
      ],
    };
  },

  mounted() {
    this.ResizeTimeline();
    window.onresize = (val) => {
      this.ResizeTimeline();
    };
methods: {
    ResizeTimeline() {
      var _screenWidth =
        window.innerWidth ||
        document.documentElement.clientWidth ||
        document.body.clientWidth;
      let timeline_list = document.getElementsByClassName("timeline-card");
      var _card_width_px = getComputedStyle(
        document.documentElement
      ).getPropertyValue("--timeline-card-height");
      var _card_width = _card_width_px.substr(0, _card_width_px.length - 2);
      var valid_area = _screenWidth * 0.75;

      var offset = (valid_area - _card_width) / (this.timelines.length - 1);
      var is_odd = this.timelines.length % 2 == 1 ? true : false;

      if (is_odd) {
        var middle_index = parseInt(this.timelines.length / 2);
        for (var i = 0; i < timeline_list.length; i++) {
          timeline_list[i].style.left =
            String((i - middle_index) * offset) + "px";
          timeline_list[i].getElementsByClassName("grey-line")[0].style.width =
            String(offset) + "px";
        }
      } else {
        var middle_index = parseInt(this.timelines.length / 2);
        for (var i = 0; i < timeline_list.length; i++) {
          timeline_list[i].style.left =
            String((i - middle_index + 0.5) * offset) + "px";
          timeline_list[i].getElementsByClassName("grey-line")[0].style.width =
            String(offset) + "px";
        }
      }
    },
    TimelineAnimateIn(index) {
      var c_list = this.$refs[this.timelines[index].name];
      for (var i = 0; i < c_list.length; i++) {
        c_list[i].classList.add("animate__fadeInUp");
        c_list[i].classList.remove("animate__fadeOutDown");
      }
    },
    TimelineAnimateOut(index) {
      var c_list = this.$refs[this.timelines[index].name];
      for (var i = 0; i < c_list.length; i++) {
        c_list[i].classList.remove("animate__fadeInUp");
        c_list[i].classList.add("animate__fadeOutDown");
      }
    },}
  },

小结

这个组件灵感也是参考了几个codepen的大神作品,结合自己的想法进行实现(有一说一,codepen的作品挺好,但是代码搬运太麻烦了,有能力还是自己写免得改来改去)。该组件还是有个缺点,在窗口缩小到一点程度时候,图片底部的spot不可选,虽然有思路要怎么改,但是想想改起来有点麻烦,索性算了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现InfiniteScroll和Timeline时间线结合,可以通过以下步骤来实现: 1. 使用element-plus的InfiniteScroll组件实现无限滚动加载更多数据。 2. 在滚动到底部时,通过接口获取更多的时间线数据。 3. 将获取到的时间线数据与之前的数据合并,并按照时间排序。 4. 使用element-plus的Timeline组件展示时间线数据。 下面是一个简单的代码示例: ```html <template> <el-timeline> <el-timeline-item v-for="(item, index) in timelineData" :key="index"> {{ item.content }} </el-timeline-item> <div v-infinite-scroll="loadMore" infinite-scroll-disabled="loading" infinite-scroll-distance="10"> <div v-if="loading">Loading...</div> </div> </el-timeline> </template> <script> import { ref } from 'vue' import { Timeline, TimelineItem, InfiniteScroll } from 'element-plus' export default { components: { [Timeline.name]: Timeline, [TimelineItem.name]: TimelineItem }, directives: { InfiniteScroll }, setup() { const timelineData = ref([]) const loading = ref(false) const loadMore = () => { if (loading.value) return loading.value = true // 调用接口获取更多数据 fetchData().then(data => { timelineData.value = [...timelineData.value, ...data] timelineData.value.sort((a, b) => b.timestamp - a.timestamp) loading.value = false }) } const fetchData = () => { // 模拟异步接口调用 return new Promise(resolve => { setTimeout(() => { const data = [ { content: 'New message', timestamp: Date.now() }, { content: 'Another message', timestamp: Date.now() + 1000 }, { content: 'More messages', timestamp: Date.now() + 2000 } ] resolve(data) }, 1000) }) } // 初始化加载数据 loadMore() return { timelineData, loading, loadMore } } } </script> ``` 在上面的示例中,我们使用了`v-infinite-scroll`指令来监听滚动事件,并在滚动到底部时触发`loadMore`方法。`loading`变量用于控制是否正在加载数据,避免重复请求数据。`fetchData`方法模拟异步接口调用,返回一个包含时间线数据的Promise对象。在`loadMore`方法中,我们调用接口获取更多数据,将其与之前的数据合并,并按照时间进行排序。最后,我们在模板中使用element-plus的Timeline组件展示时间线数据。 需要注意的是,由于element-plus的Timeline组件需要对数据进行排序,因此我们需要确保数据中包含一个可以用于比较的时间戳字段,例如上面示例中的`timestamp`字段。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值