发布于 2026-01-06 2 阅读
0

制作滚动卡牌列表 - WotW

制作滚动卡牌列表 - WotW

欢迎来到“每周小部件”系列,在这里我会选取一些很棒的 UI/UX 组件的 GIF 或视频,并用代码将它们变成现实。

今天我们将制作一个滚动时会动画展开的卡片列表。这个组件的灵感来源于Hiwow在Dribbble上创建的第一个部分,它的样子如下:

参考

准备工作

今天的组件我们将只使用Vue.js,不使用任何动画库,这意味着我们将大量使用 Vue 的功能。

如果你想跟着做,可以 fork 这个已经包含所有依赖项的codepen 模板。

初始标记

为了使我们的应用正常运行,我们需要一个带有指定 id 的主 div,appVue.js 将挂载到该 div 上。完成之后,我们就可以开始创建卡片了。这里我只创建一个卡片,因为稍后我们将通过编程方式创建其余的卡片。
每张卡片都会有一个占位图片,图片旁边会有一个div我称之为卡片内容的元素。卡片内容会显示标题、描述和评分数据。

<div id="app">
  <div class="card">
    <img class="card__image" src="https://placeimg.com/100/140/animals">      
    <div class="card__content">
      <h3>title</h3>
      <p>description</p>
      <div class="card__rating">
        <span>8.0 </span>
        <span class="card__stars--active">★★★</span>
        <span class="card__stars--inactive">★★</span>
      </div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

您可能已经注意到,我在类的命名中使用了BEM,这将有助于在下一步中设置卡片样式。

造型

现在我们有一张带有难看测试文字的图片,让我们来修改一下。首先,我们将直接在图片中添加一个浅灰色背景body

body {
  background-color: #FEFEFE;
}
Enter fullscreen mode Exit fullscreen mode

然后,我们将为卡片声明一个预定义的高度,使其与图像高度相匹配140px。此外,我们还通过设置内边距、更改字体和添加阴影来添加一些细节,从而营造出卡片悬浮的效果。

.card {
  height: 140px;
  background-color: white;
  padding: 5px;
  margin-bottom: 10px;
  font-family: Helvetica;
  box-shadow: 0px 3px 8px 0px rgba(0,0,0,0.5);
}
Enter fullscreen mode Exit fullscreen mode

更近
我们正在接近目标,现在轮到内部元素进行造型设计了。

卡片图片和卡片内容应并排显示display: inline-block。图片宽度为[此处应填写100px图片宽度],并留有少量边距以将其与文本分隔开,因此卡片内容将占据卡片剩余的宽度。

卡片内容的内部文本需要顶部对齐,否则显示效果会不理想。标题的默认边距h3过大,因此我们将对其进行调整0
卡片评分容器需要底部对齐,我们将使用 `--align- items` 属性position: absolute来实现这一点。最后,星形span元素将根据星形是否处于“激活”状态而显示不同的颜色。

.card__img {
  display: inline-block;
  margin-right: 10px;
}

.card__content {
  display: inline-block;
  position: relative;
  vertical-align: top;
  width: calc(100% - 120px);
  height: 140px;
}

.card__content h3 {
  margin: 0;
}

.card__rating {
  position: absolute;
  bottom: 0;
}

.card__stars--active {
  color: #41377C;
}
.card__stars--inactive {
  color: #CCCCCC;
}
Enter fullscreen mode Exit fullscreen mode

它应该看起来更像运球了:
造型完成

如果你足够细心,可能会注意到活跃恒星和非活跃恒星之间存在一定的间距差异。这是由于两个跨度元素之间的间距造成的,可以通过以下方式消除:

...
      <div class="card__rating">
        <span>8.0 </span>
        <span class="card__stars--active">★★★</span><!-- I'm removing the space
     --><span class="card__stars--inactive">★★</span>
      </div>
...
Enter fullscreen mode Exit fullscreen mode

这种行为

现在,在我们的 Vue 实例中,我们将开始声明组件需要使用的数据。我们需要很多卡片,但我没有逐个创建,而是只创建了三个,然后多次复制:

const cardsData = [
  {
    img:'https://placeimg.com/100/140/animals',
    title: 'Title 1',
    description: 'Tempora quam ducimus dolor animi magni culpa neque sit distinctio ipsa quos voluptates accusantium possimus earum rerum iure',
    rating: 9.5,
    stars: 4
  },
  {
    img:'https://placeimg.com/100/140/arch',
    title: 'Title 2',
    description: 'Tempora quam ducimus dolor animi magni culpa neque sit distinctio ipsa quos voluptates accusantium possimus earum rerum iure',
    rating: 8.4,
    stars: 5
  },
  {
    img:'https://placeimg.com/100/140/people',
    title: 'Title 3',
    description: 'Tempora quam ducimus dolor animi magni culpa neque sit distinctio ipsa quos voluptates accusantium possimus earum rerum iure',
    rating: 7.234,
    stars: 2
  },
  // copy and paste those three items as many times as you want
]
Enter fullscreen mode Exit fullscreen mode

然后,在我们的 Vue 实例中,我们可以将该数组设置到 data 属性中,以便开始跟踪它。

new Vue({
  el: '#app',
  data: {
    cards: cardsData
  }
})
Enter fullscreen mode Exit fullscreen mode

让我们将这些数据与 HTML 模板绑定。v-for我们将使用指令遍历卡片数据数组并渲染每个属性。

<div id="app">
  <div class="card" 
    v-for="(card, index) in cards"
    :key="index">

    <img class="card__image" :src="card.img">      
    <div class="card__content">
      <h3>{{card.title}}</h3>
      <p>{{card.description}}</p>
      <div class="card__rating">
        <span>{{card.rating}} </span>
        <span class="card__stars--active">{{card.stars}}</span>
        <span class="card__stars--inactive">{{5 - card.stars}}</span>
      </div>
    </div>

  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

不错,我们有很多卡片,可惜评分和星级与我们预期的不符。

有事吗

正如你所看到的,星级评分的显示方式与数字相同,而且最后一个评分会显示多位小数。幸运的是,Vue.js 提供了一种名为过滤器(filters)的功能,可以帮助我们以想要的方式解析任何数据。

让我们回到 Vue 实例,声明两个过滤器,一个用于限制数字,另一个用于将任何数字转换为星号:

  // ... data
  filters: {
    oneDecimal: function (value) {
      return value.toFixed(1)
    },
    toStars: function (value) {
      let result = ''
      while(result.length < value) {
        result+='' 
      }
      return result
    }
  },
  // ... 
Enter fullscreen mode Exit fullscreen mode

有了这些筛选条件,我们就可以回到模板,将它们添加到需要筛选的数据中:

  <!-- ... card markup -->
  <span>{{card.rating | oneDecimal}} </span>
  <span class="card__stars--active">{{card.stars | toStars }}</span><!--
  --><span class="card__stars--inactive">{{5 - card.stars | toStars}}</span>
Enter fullscreen mode Exit fullscreen mode

就这么简单{{ value | filter }},数据会在渲染前进行转换。

滚动

到目前为止,我们还没有为卡片列表添加任何行为,只是处理了它的外观和渲染方式。现在是时候添加动画效果了!
首先,我们需要以某种方式跟踪应用程序的滚动,为此我们将使用 Vue 的另一个特性:自定义指令

这个滚动指令直接取自Vue.js 文档,当我们将其添加到我们的 JS 代码中时,就可以使用该v-scroll指令了:

Vue.directive('scroll', {
  inserted: function (el, binding) {
    let f = function (evt) {
      if (binding.value(evt, el)) {
        window.removeEventListener('scroll', f)
      }
    }
    window.addEventListener('scroll', f)
  }
})
Enter fullscreen mode Exit fullscreen mode

然后,只需在 HTML 代码中快速修改应用程序的 div 元素,即可使用它:

<div id="app" v-scroll="onScroll">
  <!-- ... rest of the markup -->
Enter fullscreen mode Exit fullscreen mode

现在我们应该能够创建onScroll用于跟踪滚动位置的方法了:

  data: {
    cards: cardsData,
    scrollPosition: 0
  },
  methods: {
    onScroll () {
      this.scrollPosition = window.scrollY
    }
  },
Enter fullscreen mode Exit fullscreen mode

请注意,我们添加了scrollPosition用于跟踪window.scrollY属性的参数。这有助于 Vue 在属性发生变化时重新计算。

动画卡片

在原版 Dribbble 中,卡片在移动到屏幕顶部时会有消失的scrollPosition效果。为了实现这个效果,我们需要在每次更新时重新计算每张卡片的样式。

接下来的两种方法会进行所有计算以生成样式。一开始可能会有点令人困惑,但我会尽力解释清楚。

首先,我们设置一个cardHeight常量,其值包含卡片的内边距和外边距。然后,根据卡片的索引,我们将其设置为positionY卡片的位置,第一个卡片对应0第二个位置160,然后是第三个位置320,依此类推。

之后我们需要知道卡片距离屏幕顶部的距离,我们计算出来并将值赋给变量deltaY。我们需要在卡片到达屏幕顶部时开始动画,所以我们只需要关注 deltaY 小于某个阈值的情况0。我将其限制在某个阈值和某个值之间-1600因为当 deltaY 小于-160某个阈值时,卡片已经超出屏幕范围了。

dissapearingValue最后,我们创建一个yValue依赖zValuedY值的元素。dissapearingValue顾名思义,该元素会使卡片淡入淡出,因此我们将其绑定到 CSS 的 opacity 属性。另外两个值将用于 transform 属性,使卡片看起来像是位于其他卡片的后面。

  // ... methods
    calculateCardStyle (card, index) {
      const cardHeight = 160 // height + padding + margin

      const positionY = index * cardHeight
      const deltaY = positionY - this.scrollPosition

      // constrain deltaY between -160 and 0
      const dY = this.clamp(deltaY, -cardHeight, 0)

      const dissapearingValue = (dY / cardHeight) + 1
      const zValue = dY / cardHeight * 50
      const yValue = dY / cardHeight * -20

      card.style = {
        opacity: dissapearingValue,
        transform: `perspective(200px) translate3d(0,${yValue}px, ${zValue}px)`
      }
      return card
    },
    clamp (value, min, max) {
      return Math.min(Math.max(min, value), max)
    }
Enter fullscreen mode Exit fullscreen mode

现在只需将每张卡片传递给该方法,并将结果作为名为styledCards的计算属性公开即可:

  computed: {
    styledCards () {
      return this.cards.map(this.calculateCardStyle)
    }
  },
Enter fullscreen mode Exit fullscreen mode

差不多完成了,让我们把新创建的样式绑定到卡片的HTML代码中:

  <div class="card" 
    v-for="(card, index) in styledCards"
    :style="card.style"
    :key="index">
Enter fullscreen mode Exit fullscreen mode

现在是最终结果(记得向下滚动):

本周的小工具介绍就到这里

如果你还想了解更多,可以查看其他 WotW:

如果您希望下周看到某个特定的小部件,请在评论区留言。

文章来源:https://dev.to/ederchrono/making-a-scrolling-card-list---wotw-57ml