最近忙于开发抖音小程序,最想吐槽的就是,既没有适配的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
});
}
完结啦 🎉 🎉 🎉