小程序巧妙实现电商平台「换一换」功能, 速来~

引言

在电商平台的商城装修功能中,"换一换"组件是提升用户体验和商品曝光的重要元素。这个看似简单的功能------展示9个商品,点击按钮后切换为另外9个商品------实际实现起来却有不少技术考量。本文将分享我在开发这一功能时的思路和技术实现方案。

功能效果展示

"换一换"组件的基本效果是:页面上初始展示9个商品,用户点击"换一换"按钮后,当前展示的商品会以一种流畅的动画效果切换为另外9个商品

实现难点分析

初看这个需求,很容易想到最直接的方案:点击按钮后,简单替换数据源并刷新视图。然而,这种实现方式会导致生硬的切换效果,用户体验欠佳。

为了实现流畅的视觉效果,我们需要考虑以下几点:

  1. 无缝切换:如何让商品切换时看起来自然流畅
  2. 性能优化:如何避免频繁的DOM操作导致的性能问题
  3. 数据管理:如何高效管理多组商品数据

技术实现方案

经过分析,我采用了"卡片翻转"的实现思路:

1. 双面卡片设计

每个商品展示格子实际上是一个具有正反两面的卡片:

  • 正面:展示第一组商品(1-9号商品)
  • 背面:展示第二组商品(10-18号商品)

当用户点击"换一换"按钮时,我们不是直接替换数据,而是触发卡片的翻转动画,将背面的内容展示出来。

2. 分析完毕,直接上干货
html 复制代码
<!-- 换一换组件 -->
<view>
  <view class="huan-wrapper">
    <view class="huan-main">
      <view wx:for="{{renderImgList}}" wx:key="index" class="huan-cell" style="{{item.boxStyle}}">
        <view class="huan-img" style="background-image: url({{item.image_url}});" />
      </view>
      <!-- /9宫格 -->

      <view class="huan-btn" style="{{screenBtn.style}}" bindtap="handleFlip">
        <image class="btn-img" src="/btn.png" mode="widthFix" />
      </view>
      <!-- /按钮 -->
    </view>
  </view>
</view>
css 复制代码
/*盒子样式*/
.huan-wrapper {
  padding: 0 24rpx;
  overflow: hidden;
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
  padding-top: 24rpx;
  height: 656px;
  background-color: #FFFFFF;
}

.huan-main {
  position: relative;
}

.huan-main .huan-cell {
  position: absolute;
  transition-property: all;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  backface-visibility: hidden;  /*关键代码*/
  will-change: transform;
}

/*每个格子样式*/
.huan-cell .huan-img {
  width: 100%;
  height: 160px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/*按钮样式*/
.huan-btn {
  position: absolute;
  z-index: 9;
  top: 546px;
  left: 50%;
  transform: translateX(-50%);
  text-align: center;
  width: 167px;

}

.huan-btn .btn-img {
  width: 100%;
  display: block;
}
js 复制代码
Component({


  ready() {
  
    // 初始化渲染列表
    this.initRenderList();
  },
 


  data: {
  
    TEM_HEIGHT: 0, // 图片高度
    GAPRIGHT: 0, // 图片左右间距
    GAPBOTTOM: 0, // 图片底部间距
    COLS: 3,  // 3列
    SCREENWIDTH: 750, // 屏幕宽度
    imagesPerScreen: 9, // 每屏图片数量默认9
    isFlipped: false, // 控制反转状态
    isFlipping: false, // 是否正在翻转中
    currentIndex: 0, // 当前显示的数据起始索引
  
  
    // 所有图片列表
    allImageList: [
      { image_url: '1.png' },
      { image_url: '2.png' },
      { image_url: '3.png' },
      // ...假设有27张图片
    ],
    renderImgList: [], // 渲染图片列表
   
  },
  
  
   methods: {
    // 点击按钮换一换
    handleFlip() {
      // 如果正在翻转中,则不处理
      if (this.data.isFlipping) return;

      console.log('点击了换一换');
      // 切换翻转状态
      const isFlipped = !this.data.isFlipped;

      // 标记正在翻转
      this.setData({
        isFlipped,
        isFlipping: true
      });

      // 应用翻转效果
      this.applyFlipEffect();

      // 等待翻转动画完成后,再更新背面的图片内容
      setTimeout(() => this.updateBackfaceImages(), 800);

    },
    
    
    // 应用翻转效果
    applyFlipEffect() {
      // 重新计算每个图片的样式,应用翻转效果
      const renderImgList = this.data.renderImgList.map((item, index) => {
        item.boxStyle = this.calcBoxStye(index);
        return item;
      });

      // 更新渲染列表,实现翻转效果
      this.setData({ renderImgList });
    },
    
    
    // 更新背面图片
    updateBackfaceImages() {
      const { allImageList, currentIndex, renderImgList, imagesPerScreen, isFlipped } = this.data;

      // 确定哪些图片现在在背面(即将在下次翻转时显示)
      const backfaceIndices = this.getBackfaceIndices(isFlipped, imagesPerScreen);

      // 获取当前渲染列表的副本
      let newRenderImgList = [...renderImgList];

      // 加载新的图片替换背面的图片
      if (allImageList.length) {
        // 计算新数据的起始位置,循环使用所有图片
        let newIndex = currentIndex % allImageList.length;

        // 替换背面的图片
        for (let i = 0; i < backfaceIndices.length; i++) {
          const replaceIndex = backfaceIndices[i];
          // 确保不超出数组范围,循环使用数据
          // 这里是关键:当i超过可用图片数量时,我们从头开始取
          const sourceIndex = (newIndex + i) % allImageList.length;
          // 复制新图片数据
          const newImage = { ...allImageList[sourceIndex] };
          // 保留原始位置信息,但更新图片内容
          newRenderImgList[replaceIndex] = {
            ...newImage,
            boxStyle: newRenderImgList[replaceIndex].boxStyle, // 保留原样式
            
          };
        }

        // 更新下一次加载的起始位置
        newIndex = (newIndex + imagesPerScreen) % allImageList.length;

        // 更新数据,但不触发翻转动画
        this.setData({
          renderImgList: newRenderImgList,
          currentIndex: newIndex,
          isFlipping: false // 重置翻转状态
        });
      } else {
        // 如果图片不足,只重置翻转状态
        this.setData({
          isFlipping: false
        });
      }
    },
    
    
    // 获取背面图片索引
    getBackfaceIndices(isFlipped, imagesPerScreen) {
      const backfaceIndices = [];
      for (let i = 0; i < imagesPerScreen * 2; i++) {
        if (isFlipped && i < imagesPerScreen) {
          backfaceIndices.push(i); // 前一屏在背面
        } else if (!isFlipped && i >= imagesPerScreen) {
          backfaceIndices.push(i); // 后一屏在背面
        }
      }
      return backfaceIndices;
    },

    
    
     // 计算盒子样式
    calcBoxStye(index) {
      let { COLS, GAPRIGHT, GAPBOTTOM, SCREENWIDTH, isFlipped, ITEM_HEIGHT, imagesPerScreen } = this.data;
      // 计算行列位置(从0开始)
      const row = Math.floor(index / COLS);
      const col = index % COLS;
      // 计算基础位置
      const baseRow = row % (imagesPerScreen / 3);
      // 计算样式
      const top = baseRow * (ITEM_HEIGHT + GAPBOTTOM);
      const boxWidth = (SCREENWIDTH - GAPRIGHT * 2 - 48) / COLS;
      const left = col * (boxWidth + GAPRIGHT);
      // 根据翻转状态计算角度
      let deg = 0;
      if (isFlipped) {
        deg = index < imagesPerScreen ? 180 : 0;
      } else {
        deg = index >= imagesPerScreen ? 180 : 0;
      }

      // 设置第1,5,6 格子 过渡慢点,使整体效果看起来不会太死板
      let duration = 500; // 30%概率是800ms,70%概率是500ms
      if ([1, 5, 6].includes(index % imagesPerScreen)) {
        duration = 800;
      }
      
      return `top: ${top}rpx; left: ${left}rpx; transition-duration: ${duration}ms; transform: rotateY(${deg}deg);width: ${boxWidth}rpx`;

    },
    
     // 初始化渲染列表
     initRenderList() {
      const { allImageList, imagesPerScreen } = this.data;
      // 假设需要18张图片
      const totalNeeded = 18;
      let renderImgList = [];

      // 填充图片列表,如果图片不足,则循环使用
      for (let i = 0; i < totalNeeded; i++) {
        const sourceIndex = i % allImageList.length;
        const imgItem = { ...allImageList[sourceIndex] };
        renderImgList.push(imgItem);
      }
      console.log('renderImgList', renderImgList);

      // 计算样式
      renderImgList.forEach((e, i) => {
        e.boxStyle = this.calcBoxStye(i);
      });

      this.setData({
        renderImgList,
      });
    },
   }

})

最终实现效果✔️

相关推荐
患得患失94924 分钟前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json
飛_26 分钟前
解决VSCode无法加载Json架构问题
java·服务器·前端
YGY Webgis糕手之路3 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔3 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang3 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔3 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任4 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴4 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔4 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js
德育处主任4 小时前
p5.js 矩形rect绘制教程
前端·数据可视化·canvas