微信小程序:背包商城(二) 首页 仿京东商城 遮盖层对话框的实现

实现目标

1. 商品卡片点击右上角的时候,弹出遮罩层以及对话框
2. 当点击遮罩层/滑动窗口时的时候,对话框隐藏
实现目标.jpg

实现效果

实现效果.jpg

具体实现思路和需要注意的点

1. 气泡对话框的实现主要分为三角形元素和矩形元素,通过y轴偏移拼接在一起形成对话框
2. 由于气泡对话框可能为上方弹出,或者下方弹出,因此三角形会存在上下两个。
对话框原理.png
3.左右两侧的对话框,由于右侧靠近屏幕边缘,因此矩形的X轴偏移量不一样。
右列.png
左列.jpg
    // 三角形元素样式
    .triangle {
      width: 0rpx;
      height: 0rpx;
      border: 25rpx solid transparent;
      border-bottom: 25rpx solid #fff;
    }
    // 对话框矩形样式
    .dialgo-div {
      height: 350rpx;
      width: 200rpx;
      background: #fff;
      transform: translate(var(--translateX),-2rpx); // 左右两列矩形的X轴偏移量不同,因此需要通过计算动态传入
      border-radius: 10rpx;
      overflow: hidden;
    }
    .bottom-triangle {
      width: 0rpx;
      height: 0rpx;
      border: 25rpx solid transparent;
      border-top: 25rpx solid #fff;
      transform: translate(-15rpx, -5rpx);//偏移Y轴重合对话框,x偏移量,使三角形对准 "x" 图标
    }
  1. 通过点击事件获取点击右上角x后获得event.detail内的x,以及y变量是相对页面元素整体的偏移量,并不是相对于屏幕的偏移量(页面元素高度可能会大于屏幕高度,因此获得的y可能会大于屏幕高度)。由于我事先弹出对话框时基于fixed布局,top和left变量时基于屏幕坐标,因此需要通过计算得出基于屏幕的top偏移量(x轴不溢出屏幕,因此可以直接应用x轴偏移量)。
    计算点击时相对于屏幕的偏移量,可以通过监听页面滚动方法onPageScroll获取当前页面滚动顶部部的y轴量,使用点击处的y轴偏移量 - 当前页面顶部的y轴偏移量就可以得出当前点击元素相对于屏幕的y轴偏移量,在Page下填入
 // debounce为防抖函数
 onPageScroll:function(event) {
    const that = this;
   debounce(function(){
     that.setData({
       scrollTop: Math.ceil(event.scrollTop)
     });
   }, 100)();
  },

debounce为防抖函数,由于onPageScroll会被频繁触发,为了避免拖动时频繁触发setData函数更新造成页面卡顿,而我们实际只需要拖动结束后获取这个值就行,所以引入了防抖函数,具体实现为

/**
 * 防抖函数
 * @param {*} fun 需要进行防抖的函数 
 */
export function debounce(fun, delay = 500, immediate= false) {
  let timer = null; // 保存定时器
  return function(args) {
    let that = this;
    let _args = args;
    if(timer) clearTimeout(timer);
    if(immediate) {
      if(!timer) fun.apply(that,_args); // 定时器为空表示可以执行
      timer = setTimeout(function() {
        timer = null;// 到时间后设置定时器为空
      },delay);
    }
    else {
      // 如非立即执行,则重设定时器
      timer = setTimeout(function() {
        fun.call(that,_args);
      },delay);
    }
  }
}

Page页面获取到scrollTop后通过prop传入到组件中在点击事件中进行计算。

  1. 计算对话框从上弹出还是下弹出, 通过第四点,我们已经计算出了当前的对话框需要偏移的lefttop值。如果top值加上对话框的高度大于整个屏幕的高度时,表示对话框溢出屏幕,此时就需要在上方弹出。
    获取屏幕信息的接口是wx.getSystemInfoSync()

  2. 底部的导航栏如果为系统原生,属于是最顶层元素,手写的遮罩层无法遮盖住导航栏,因此需要引入wx.hideTabBar() 接口,当点击时隐藏底部导航栏,遮罩层消失时,重新显示,接口为wx.showTabBar。(有遮罩层置顶的处理方法欢迎提出)

  3. 当页面进行滚动时,对话框及遮罩层也隐藏,因此在Page的滚动事件中再引入,当滚动时设置isScrollingtrue,传入组件,在组件中监听obeserver,当变量改变为true的时候隐藏遮罩层以及对话框。

  4. 遮罩层使用简单的fixed布局,设置z-index来进行遮盖。点击对话框选项后,显示减少推荐,使用简单absolute布局。

减少推荐.jpg

组件完整代码

页面的onPageScroll函数

onPageScroll:function(event) {
    // 滚动时隐藏对话框和遮罩层
    if(!this.data.isScrolling) {
        this.setData({
          isSrolling: true
        });
    }
   const that = this;
   debounce(function(){
     that.setData({
       scrollTop: Math.ceil(event.scrollTop),
       isSrolling: false
     });
   }, 100)();
 }

组件wxml

<!--pages/index/components/suggestCard/suggestCard.wxml-->
<view style="top: {{top}}; left: {{left}};height: {{realHeght}};" class="{{ index % 2 ? 'odd-card' : 'even-card' }}">
  <van-image src="{{ itemData.imageUrl }}" fit="widthFix" width="325rpx">
  </van-image>
  <view class="info-panel">
    <view class="info-title">
      <van-tag class="new-tag" custom-class="new-tag" color="#95d475">上新</van-tag>
      时尚百搭双肩奶爸包 多功能两用妈咪包防水休闲学生包 可定制
    </view>
    <view class="price-tag">
      <view class="price-signal-container">
        <text class="price-signal">¥</text>
        <text>999.86</text>
      </view>

    </view>
  </view>
  <view class="close-icon-container">
    <view bindtap="closeTap" class="close-icon-inside-container">
      <van-icon name="cross" color="#C0C4CC" />
      <!--van-transition name="fade" show="{{show}}"  -->
      <view catchtap="wrapperTap" class="{{ show ? 'popover-wrapper-active' : 'popover-wrapper'}}">
      </view>
      <view style="top: {{dialogTop}}; left: {{dialogleft}};--translateX: {{translateX}}" class="{{ show ? 'buble-dialog-open' :'buble-dialog' }}">
        <view class="{{dialogDirection == 'bottom' ? 'triangle' : 'hidden-triangle'}}">
        </view>
        <view class="dialgo-div">
          <view catchtap="menueTap" wx:for="{{menuItems}}" wx:key="unique" wx:for-item="item" class="dialog-menu-item">
            <view>
              {{ item.text }}
            </view>
          </view>
        </view>
        <view class="{{dialogDirection == 'up' ? 'bottom-triangle':'hidden-triangle'}}">
        </view>
      </view>
      <!--/van-transition-->
    </view>
  </view>
  <view class="{{ isCanceled ?  'cancel-cover ' : 'cancel-cover-deactive'}}">
    <view class="cancel-text-container">
      <text>您的反馈已收到</text>
      <view>会减少此类内容的推荐</view>
    </view>
  </view>
</view>

组件wxss

.odd-card {
  width: 350rpx;
  height: 0rpx;
  background: #fff;
  border: 1px solid #E4E7ED;
  border-radius: 4px;
  position: absolute;
  left: 375rpx;
  top: 0rpx;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  transition: 2s all ease-in-out;
  overflow: hidden;
}

.even-card {
  width: 350rpx;
  height: 0rpx;
  background: #fff;
  border: 1px solid #E4E7ED;
  border-radius: 4px;
  position: absolute;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  transition: 2s all ease-in-out;
  overflow: hidden;
}

.info-panel {
  font-size: 28rpx;
  display: flex;
  flex-direction: column;
  height: 100rpx;
}

.info-title {
  padding-left: 2.5%;
  padding-right: 2.5%;
  width: 95%;
  font-size: 25rpx;
  overflow: hidden;
  text-overflow:clip;
  display: -webkit-box;
  -webkit-line-clamp: 2; /*限制文本行数*/
  -webkit-box-orient: vertical;
  word-break: break-all;
}
.new-tag {
  font-size: 20rpx !important;
}

.price-tag {
  height: 0rpx;
  flex-grow: 1;
  font-size: 25rpx;
  display: flex;
  align-items: center;
}
.price-signal-container {
  width: 50%;
  color: red;
  text-align: center;
}

.price-signal {
  font-size: 20rpx;
}


.close-icon-container {
  position: absolute;
  top: 15rpx;
  left: 310rpx;
  font-size: 15rpx;
  text-align: center;
}
.close-icon-inside-container {
  height: 25rpx;
  width: 25rpx;
}

.popover-wrapper {
  position: fixed;
  width: 750rpx;
  height: 100vh;
  top: 0px;
  left: 0px;
  background: rgba(0, 0, 0, 0.5);
  z-index: -1;
  display: none;
  opacity: 0;
  animation-name: hide;
  animation-duration: .3s;
}
.popover-wrapper-active {
  position: fixed;
  width: 750rpx;
  height: 100vh;
  top: 0px;
  left: 0px;
  opacity: 1;
  animation-name: show;
  animation-duration: .3s;
  animation-fill-mode: forwards;
  z-index: 1999;
  background: rgba(0, 0, 0, 0.5);
}

.cancel-cover {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0px;
  left: 0px;
  background-color: rgba(256, 256, 256, 0.8);
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 25rpx;
}

.cancel-cover-deactive {
  display: none;
}

.cancel-text-container {
  width: 80%;
}

@keyframes show {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes hide {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

.buble-dialog {
  display: none;
  animation: bubleShow .3s ease-in-out;
  animation-fill-mode: forwards;
  animation-direction: reverse;
}

.buble-dialog-open {
  --translateX: '0rpx';
  position: fixed;
  z-index: 2000;
  display: block;
  animation: bubleShow .3s ease-in-out;
  animation-fill-mode: forwards;
}

@keyframes bubleShow {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.dialgo-div {
  height: 350rpx;
  width: 200rpx;
  background: #fff;
  transform: translate(var(--translateX),-2rpx);
  border-radius: 10rpx;
  overflow: hidden;
}

.triangle {
  width: 0rpx;
  height: 0rpx;
  border: 25rpx solid transparent;
  border-bottom: 25rpx solid #fff;
  transform: translateX(-15rpx);
}

.bottom-triangle {
  width: 0rpx;
  height: 0rpx;
  border: 25rpx solid transparent;
  border-top: 25rpx solid #fff;
  transform: translate(-15rpx, -5rpx);
}

.hidden-triangle {
  display: none;
}

.dialog-menu-item {
  color: #323233;
  height: 69rpx;
  font-size: 20rpx;
  width: 100%;
  border-bottom: 1px solid #ebedf0;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: .2s background ease-in-out;
}
.dialog-menu-item:hover {
  background:  #e1f3d8;
  transition: .2s background ease-in-out;
}
.dialog-menu-item:last-child {
  border-bottom: none;
}

组件js

Component({
  /**
   * 组件的属性列表
   */
  lifetimes: {
    attached() {
      let val = '-80rpx';
      if(this.properties.index % 2) {
        val = '-148rpx'
      }
      this.setData({
        translateX: val
      })
    }
  },
  properties: {
    index: {
      type: Number,
      value: 0,
      /* observer: function(newVal, oldVal) {
         let top = `${(newVal / 2 ) * 460}rpx`;
         let left = `25rpx`;
         if(newVal % 2) {
           top = `${(Math.floor(newVal / 2))* 410}rpx`;
           if(newVal == 1) {
            // console.log(top);
           }
           left = `375rpx`;
         }
         this.setData({
           top: top,
           left: left
         });
       }*/
      observer: function (newValue, oldValue) {
        if ((newValue % 2)) {
          this.setData({
            left: "375rpx"
          })
        }
      }
    },
    itemData: {
      type: Object,
      value: {
        imageUrl: '',
        top: '0rpx',
        left: '0rpx',
        realHeght: '450rpx'
      },
      observer: function (newValue, oldValue) {
        this.setData({
          top: newValue.top,
          left: newValue.left,
          realHeght: newValue.realHeight
        })
      }
    },
    scrollTop: {
      type: Number,
      value: 0,
      observer: function (newValue) {
        // console.log(newValue);
      }
    },
    isSrolling: {
      type: Boolean,
      value: false,
      observer: function (newValue) {
        if (newValue && this.data.show) {
          wx.showTabBar({
            animation: true,
          })
          this.setData({
            show: false
          })
        }
      }
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    left: `25rpx`,  // 组件left值
    top: '0rpx',  //组件top值
    realHeght: `450rpx`,  // 组件真实高度
    show: false,  // 用于控制点击组件右上角x后,遮罩层和对话框是否显示
    dialogTop: '300rpx',  // 对话框的top坐标
    dialogLeft: '350rpx', // 对话框的left坐标
    dialogDirection: 'bottom',  // 对话框显示的位置
    translateX: '-80rpx',  // 对话框的x偏移值,右边列显示对话框时需要向左偏移更多(-148rpx)
    isCanceled: false,
    menuItems: [
      {
        text: '不感兴趣'
      },
      {
        text: '品类不喜欢'
      },
      {
        text: '已经买了'
      },
      {
        text: '图片引起不适'
      },
      {
        text: '涉及隐私'
      }
    ]
  },

  /**
   * 组件的方法列表
   */
  methods: {
    // 组件右上角 x ,关闭点击事件
    closeTap: function (event) {
      // 重复点击也关闭遮罩层
      if(this.data.show) {
        wx.showTabBar({
          animation: true,
        });
        this.setData({
          show: false,
        });
        return ;
      }
      let result = wx.getSystemInfoSync(); // 获取系统信息
      let windowHeight = Math.floor(result.windowHeight); // 获取系统窗户高度
      let windowWidth = Math.floor(result.windowWidth); // 获取系统窗户宽度
      let x = Math.floor(event.detail.x); // 获取点击的x坐标
      let y = Math.floor(event.detail.y); // 获取点击的y坐标
      // 计算当前元素(y坐标 - scrollTop) 获取当前元素在屏幕处的Y坐标,再加上 弹出对话框的高度
      // 如果所得数值大于当前屏幕的高度(或再减去81(底部导航栏高度)),证明数值溢出,则显示为顶部对话框,否则则显示为底部对话框。
      let dialogDirection = (y - this.properties.scrollTop + windowWidth / 750 * 400) >= windowHeight - 81 ? 'up' : 'bottom';
      //console.log({windowHeight})
      //console.log({y:y - this.properties.scrollTop})
      //console.log({event})
      wx.hideTabBar();  // 隐藏底部
      // 根据顶部对话框或底部对话框计算top的值
      // 底部对话框的top值为当前窗口的绝对y坐标,计算方式为(点击坐标y值 - 当前页面的scrollTop)
      // 顶部对话框top值为底部对话框top值的基础上 - 对话框的高度;
      let tempTop = dialogDirection == 'up' ? `calc(${y - this.properties.scrollTop}px - 400rpx)` : `${y - this.properties.scrollTop}px`;
      this.setData({
        dialogTop: tempTop,
        dialogLeft: `${x}px`,
        show: true,
        dialogDirection: dialogDirection
      })
    },
    // 遮罩层点击事件
    wrapperTap: function (event) {
      this.setData({
        show: false
      });
      wx.showTabBar({
        animation: true,
      })
    },
    // 菜单点击事件
    menueTap: function() {
      /*this.triggerEvent('deleteItem',this.properties.index);
      this.setData({
        show: false
      })*/
      this.setData({
        show: false,
        isCanceled: true
      })
    }
  }
})

可优化

遮罩层使用Vant组件自带的遮罩层,提供更优化的动画。完善点击取消后的动画。欢迎交流学习。创作不易,点个赞吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容