小程序弹出层/抽屉封装 (抖音小程序)

发布于:2025-05-19 ⋅ 阅读:(20) ⋅ 点赞:(0)

最近忙于开发抖音小程序,最想吐槽的就是,既没有适配的UI框架,百度上还找不到关于抖音小程序的案列,我真的很裂开啊,于是我通过大模型封装了一套代码

效果如下

在这里插入图片描述

介绍

可以看到 这个弹出层是支持关闭和标题显示的,同时这个出场肯定是从下到上的,当然它肯定是支持任意方向弹出的 效果类似于element ui 的抽屉效果

代码

在 components 新建组件 drawer

抽屉的结构部分

<!-- drawer.wxml -->
<view class="drawer-container" tt:if="{{isVisible}}">
    <view class="drawer-mask" style="{{maskStyle}}" bindtap="onMaskClick" tt:if="{{mask}}"></view>
    <view class="drawer-content" style="{{drawerStyle}}" catchtouchmove="stopPropagation">
        <!-- 头部区域 -->
        <view class="drawer-header" tt:if="{{showTitle || showClose}}">
            <text class="drawer-title" tt:if="{{showTitle && title}}">{{title}}</text>
            <view class="header-spacer" tt:if="{{!title && showClose}}"></view>
            <view class="drawer-close-btn" tt:if="{{showClose}}" bindtap="onCloseClick">
                <text class="close-icon">×</text>
            </view>
        </view>

        <!-- 内容区域 -->
        <view class="drawer-body">
            <slot></slot>
        </view>
    </view>
    </drawer-container>

抽屉逻辑处理部分

// drawer.js
Component({
  properties: {
    show: {
      type: Boolean,
      value: false,
      observer: 'toggleDrawer'
    },
    direction: {
      type: String,
      value: 'bottom', // left, right, top, bottom
      observer: 'updatePosition'
    },
    width: {
      type: String,
      value: '100%'
    },
    height: {
      type: String,
      // value: '100%'
    },
    mask: {
      type: Boolean,
      value: true
    },
    maskClosable: {
      type: Boolean,
      value: true
    },
    showClose: {
      type: Boolean,
      value: true
    },
    title: {
      type: String,
      value: ''
    },
    showTitle: {
      type: Boolean,
      value: true
    },
    duration: {
      type: Number,
      value: 300
    }
  },
  
  data: {
    drawerStyle: '',
    maskStyle: '',
    isVisible: false,
    isTransitioning: false
  },
  
  methods: {
    toggleDrawer(newVal) {
      if (newVal) {
        this.openDrawer();
      } else {
        this.closeDrawer();
      }
    },
    
    updatePosition() {
      if (!this.data.isVisible) {
        this.setInitialPosition();
      }
    },
    
    setInitialPosition() {
      const { direction, width, height } = this.properties;
      let style = `position: fixed; z-index: 10000;`;
      
      switch(direction) {
        case 'left':
          style += `width: ${width}; height: ${height}; top: 0; left: 0; transform: translateX(-100%);`;
          break;
        case 'right':
          style += `width: ${width}; height: ${height}; top: 0; right: 0; transform: translateX(100%);`;
          break;
        case 'top':
          style += `width: ${width}; height: ${height}; top: 0; left: 0; transform: translateY(-100%);`;
          break;
        case 'bottom':
          style += `width: ${width}; height: ${height}; bottom: 0; left: 0; transform: translateY(100%);`;
          break;
      }
      
      this.setData({
        drawerStyle: style
      });
    },
    
    openDrawer() {
      if (this.data.isVisible || this.data.isTransitioning) return;
      
      const { direction, duration } = this.properties;
      
      // 先设置初始位置并显示
      this.setInitialPosition();
      this.setData({
        isVisible: true,
        maskStyle: 'opacity: 0;'
      });
      
      // 强制重排后应用动画
      setTimeout(() => {
        let drawerStyle = this.data.drawerStyle;
        const transformProp = direction.includes('left') || direction.includes('right') ? 'translateX' : 'translateY';
        drawerStyle = drawerStyle.replace(`${transformProp}(-100%)`, `${transformProp}(0)`);
        drawerStyle = drawerStyle.replace(`${transformProp}(100%)`, `${transformProp}(0)`);
        drawerStyle += `transition: transform ${duration}ms cubic-bezier(0.25, 0.8, 0.25, 1);`;
        
        this.setData({
          drawerStyle,
          maskStyle: `opacity: 0.7; transition: opacity ${duration}ms cubic-bezier(0.25, 0.8, 0.25, 1);`
        });
        
        this.data.isTransitioning = true;
        
        setTimeout(() => {
          this.data.isTransitioning = false;
          this.triggerEvent('open');
        }, duration);
      }, 10);
    },
    
    closeDrawer() {
      if (!this.data.isVisible || this.data.isTransitioning) return;
      
      const { direction, duration } = this.properties;
      let drawerStyle = this.data.drawerStyle;
      const transformProp = direction.includes('left') || direction.includes('right') ? 'translateX' : 'translateY';
      
      // 添加关闭动画
      drawerStyle = drawerStyle.replace(`${transformProp}(0)`, `${transformProp}(${direction.includes('left') || direction.includes('top') ? '-100%' : '100%'})`);
      drawerStyle += `transition: transform ${duration}ms cubic-bezier(0.25, 0.8, 0.25, 1);`;
      
      this.setData({
        drawerStyle,
        maskStyle: `opacity: 0; transition: opacity ${duration}ms cubic-bezier(0.25, 0.8, 0.25, 1);`
      });
      
      this.data.isTransitioning = true;
      
      // 动画结束后隐藏
      setTimeout(() => {
        this.setData({
          isVisible: false
        });
        this.data.isTransitioning = false;
        this.triggerEvent('close');
      }, duration);
    },
    
    onMaskClick() {
      if (this.properties.maskClosable && !this.data.isTransitioning) {
        this.closeDrawer();
      }
    },
    
    onCloseClick() {
      if (!this.data.isTransitioning) {
        this.closeDrawer();
      }
    },
    
    stopPropagation() {}
  },
  
  attached() {
    this.setInitialPosition();
  }
});

抽屉的基本样式

/* drawer.wxss */
.drawer-container {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 9999;
}

.drawer-mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.6);
    backdrop-filter: blur(2px);
}

.drawer-content {
    position: fixed;
    background-color: #fff;
    box-shadow: -2px 0 15px rgba(0, 0, 0, 0.1);
    overflow: hidden;
    border-top-left-radius: 20rpx;
    border-top-right-radius: 20rpx;
    -webkit-overflow-scrolling: touch;
}

/* 右侧抽屉的阴影 */
.drawer-content[style*="right: 0"] {
    box-shadow: -2px 0 15px rgba(0, 0, 0, 0.1);
}

/* 左侧抽屉的阴影 */
.drawer-content[style*="left: 0"][style*="width"] {
    box-shadow: 2px 0 15px rgba(0, 0, 0, 0.1);
}

/* 顶部抽屉的阴影 */
.drawer-content[style*="top: 0"][style*="height"] {
    box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
}

/* 底部抽屉的阴影 */
.drawer-content[style*="bottom: 0"][style*="height"] {
    box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.1);
}



.drawer-title {
    font-size: 32rpx;
    font-weight: 500;
    color: #333;
}


.drawer-close-btn:hover {
    background-color: rgba(0, 0, 0, 0.1);
}

.close-icon {
    font-size: 36rpx;
    color: #333;
}

/* 内容区域 */
.drawer-body {
    height: calc(100% - 100rpx);
    /* 减去头部高度 */
    overflow-y: auto;
    padding: 40rpx;
    box-sizing: border-box;
}


/* drawer.wxss 新增样式 */
.drawer-header {
    position: relative;
    padding: 30rpx 40rpx;
    min-height: 100rpx;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-bottom: 1rpx solid #eee;
}

.header-spacer {
    flex-grow: 1;
}

.drawer-close-btn {
    position: absolute;
    top: 30rpx;
    right: 40rpx;
    width: 48rpx;
    height: 48rpx;
    border-radius: 50%;
    background-color: rgba(0, 0, 0, 0.05);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.2s;
    z-index: 10;
}

对应文件的json引入使用

{
    "navigationBarTitleText": "商品详情",
    "usingComponents": {
        "Drawer": "/components/Drawer/Drawer"
    },
    "allowsBounceVertical": "NO"
}

界面部分


<Drawer show="{{dateShow}}" showTitle="{{true}}" showClose="{{true}}" title="账单详情" class="detail-popup" bind:close="handlePopupClose">
    <view class="repayment-popup">
        <view class="repayment-popup-item">
            <text>账期</text>
            <text>第{{repaymentTime.periods}}期</text>
        </view>
        <view class="repayment-popup-item">
            <text>账单日</text>
            <text>{{util.formatDateToDay(repaymentTime.date)}}</text>
        </view>
        <view class="repayment-popup-item">
            <text>账单总额</text>
            <text>{{util.moneyFormatMinZero(repaymentTime.waitPay)}}</text>
        </view>
        <view class="repayment-popup-item">
            <text>待还本金</text>
            <text>{{util.moneyFormatMinZero(repaymentTime.waitPrincipalMoney)}}</text>
        </view>
        <view class="repayment-popup-item">
            <text>待还滞纳金</text>
            <text>{{util.moneyFormatMinZero(repaymentTime.waitOverdueMoney)}}</text>
        </view>
    </view>
</Drawer>

界面逻辑部分

const app = getApp();
Page({
	data: {
		dateShow: false,
	},
	 appreciationEvent() {
        this.setData({
            dateShow: true,
        });
    },

    handlePopupClose() {
        this.setData({
            dateShow: false
        });
    }
		

完结啦 🎉 🎉 🎉


网站公告

今日签到

点亮在社区的每一天
去签到