固定宽高 + 露出下一张图片的一角+ 视觉效果:“分段阅读”体验

22 阅读4分钟

固定宽高 + 露出下一张图片的一角

参考:antdv.com/components/…

yangyunhe369.github.io/jQuery-Appl…

SegmentedSwiper

<script setup lang="ts">
import { ref } from 'vue';
import { Swiper, SwiperSlide } from 'swiper/vue';
import { Autoplay, Pagination } from 'swiper/modules';

import 'swiper/css';
import 'swiper/css/pagination';

const props = defineProps({
  list: { type: Array, default: () => [] },
  gap: { type: Number, default: 10 },
  loop: { type: Boolean, default: true },
  autoplay: { type: Boolean, default: true },
  delay: { type: Number, default: 3000 }
});

const modules = [Pagination, Autoplay];

// 用于存储当前的进度百分比 (0 - 100)
const progressPercentage = ref(0);

// 1. 监听倒计时:计算进度并存入 CSS 变量
const onAutoplayTimeLeft = (s: any, time: number, progress: number) => {
  // progress 是从 1 减少到 0,我们需要从 0 增加到 100
  progressPercentage.value = (1 - progress) * 100;
};

// 2. 自定义指示器的渲染函数
// index: 索引, className: Swiper 默认生成的类名 (swiper-pagination-bullet)
const renderCustomBullet = (index: number, className: string) => {
  // 返回自定义的 HTML 结构:一个底座(span),里面包一个填充层(i)
  return `<span class="${className} custom-dash-bullet">
            <i class="bullet-fill"></i>
          </span>`;
};
</script>

<template>
  <!-- 
    将进度百分比绑定到 CSS 变量 --p 上 
    这样 CSS 就可以实时读取这个变量来改变宽度
  -->
  <div class="swiper-box" :style="{ '--p': progressPercentage + '%' }">
    <swiper
      :modules="modules"
      :slides-per-view="'auto'"
      :space-between="gap"
      :loop="loop"
      :autoplay="autoplay ? { 
        delay: delay, 
        disableOnInteraction: false 
      } : false"
      :pagination="{
        clickable: true,
        renderBullet: renderCustomBullet  /* 使用自定义渲染 */
      }"
      @autoplayTimeLeft="onAutoplayTimeLeft"
      class="my-auto-swiper"
    >
      <swiper-slide 
        v-for="(item, index) in list" 
        :key="index"
        class="auto-slide-item"
      >
        <slot name="item" :item="item" :index="index"></slot>
      </swiper-slide>
    </swiper>
  </div>
</template>

<style scoped>
.swiper-box {
  width: 100%;
  position: relative;
  /* 定义默认变量,防止报错 */
  --p: 0%;
}

.auto-slide-item {
  width: auto;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* --- 自定义指示器样式核心 --- */

/* 1. 穿透修改 Swiper 默认的分页器容器位置 */
:deep(.swiper-pagination) {
  bottom: 10px; /* 距离底部距离 */
  width: 100%;
  display: flex;
  justify-content: center;
  gap: 8px; /* 指示器之间的间距 */
}

/* 2. 指示器底座 (灰色的条) */
:deep(.custom-dash-bullet) {
  width: 30px; /* 每一段的宽度,根据设计图调整 */
  height: 4px; /* 高度 */
  background: rgba(255, 255, 255, 0.3); /* 未激活时的背景色 (半透明白) */
  border-radius: 2px;
  display: inline-block;
  position: relative;
  overflow: hidden; /* 保证内部填充不溢出 */
  cursor: pointer;
  opacity: 1; /* 覆盖 Swiper 默认的透明度 */
  transition: width 0.3s; /* 可选:激活时变宽一点 */
}

/* 3. 内部填充层 (白色的条) */
:deep(.bullet-fill) {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  background-color: #fff; /* 激活时的颜色 (纯白) */
  width: 0%; /* 默认宽度为 0 */
  border-radius: 2px;
}

/* 4. 关键:只有【当前激活】的指示器,其内部填充层的宽度才等于 --p */
:deep(.swiper-pagination-bullet-active .bullet-fill) {
  width: var(--p);
}

/* 可选:如果你希望走过的指示器保持全白,未走的保持灰色 */
/* 
:deep(.swiper-pagination-bullet-active ~ .custom-dash-bullet .bullet-fill) {
  width: 0% !important;
}
*/

/* --- 新增:处理已播放的进度条 --- */

/* 
  原理:利用 CSS 的通用兄弟选择器 (~)
  逻辑:
  1. 找到当前激活的 bullet (.swiper-pagination-bullet-active)
  2. 选中它【后面】所有的兄弟元素 (即未播放的)。
  3. 但是我们要反过来思考:
     Swiper 的 HTML 结构是 [1, 2, 3(active), 4, 5]。
     CSS 很难直接选“前一个兄弟”。
     
     所以我们通常有个更简单的 hack 方法:
     让所有 bullet 默认是全白的 (width: 100%),
     然后让 active 及其后面的 bullet 变成灰色或动态。
*/

/* 方案 B:简单粗暴法 (推荐) */
/* 默认所有芯都是满的 (白色) */
:deep(.bullet-fill) {
  width: 100%; 
  transition: width 0s; /* 瞬间变白 */
}

/* 激活状态:宽度由变量控制 */
:deep(.swiper-pagination-bullet-active .bullet-fill) {
  width: var(--p);
}

/* 激活状态【之后】的所有兄弟:宽度为 0 (变灰) */
:deep(.swiper-pagination-bullet-active ~ .custom-dash-bullet .bullet-fill) {
  width: 0%;
}
</style>

父组件

<script setup lang="ts">
import { ref } from 'vue';
// 1. 引入你刚才封装的组件
import SegmentedSwiper from '@/components/SegmentedSwiper.vue';

// 2. 准备数据
const bannerList = ref([
  { id: 1, title: '热门活动', image: 'https://placehold.co/600x400/1989fa/FFF?text=Hot' },
  { id: 2, title: '新用户礼包', image: 'https://placehold.co/600x400/07c160/FFF?text=New' },
  { id: 3, title: '限时折扣', image: 'https://placehold.co/600x400/ee0a24/FFF?text=Sale' },
  { id: 4, title: '会员专享', image: 'https://placehold.co/600x400/7232dd/FFF?text=VIP' },
]);
</script>

<template>
  <div class="home-page">
    <h2>倒计时进度条轮播</h2>

    <!-- Nuxt 中建议包裹 ClientOnly 防止服务端渲染导致的水合不匹配 -->
    <ClientOnly>
      <SegmentedSwiper 
        :list="bannerList" 
        :gap="12" 
        :autoplay="true" 
        :delay="3000"
        loop
      >
        <!-- 
           3. 使用插槽 (#item) 自定义每一页的内容 
           解构出 { item, index } 供使用
        -->
        <template #item="{ item, index }">
          
          <!-- 这里的 div 决定了轮播卡片的大小 -->
          <div class="banner-card">
            <img :src="item.image" :alt="item.title" />
            <div class="info">
              <span>{{ index + 1 }}</span>
              <p>{{ item.title }}</p>
            </div>
          </div>
          
        </template>
      </SegmentedSwiper>
    </ClientOnly>
  </div>
</template>

<style scoped>
.home-page {
  padding: 20px;
  background-color: #000; /* 配合白色进度条,深色背景更好看 */
  min-height: 100vh;
  color: #fff;
}

/* 
  【核心关键点】
  必须指定卡片的宽和高!
  Swiper 设置了 slidesPerView: 'auto',它会根据这个类的宽度来排版。
*/
.banner-card {
  width: 300px;  /* 固定宽度,实现露出下一张的效果 */
  height: 180px; /* 固定高度 */
  border-radius: 12px;
  overflow: hidden;
  position: relative;
  background-color: #333;
}

.banner-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.banner-card .info {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  padding: 10px;
  background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
  pointer-events: none; /* 防止遮挡点击 */
}
</style>