前言
微信小程序简单易上手,只要有一些编程基础,即可快速开发基本的项目。
本项目是常见的,商品广告落地页小程序。提供商品浏览,商品列表按钮,购买,微信授权,手机号绑定,验证码校验,用户协议,消息通知,监听者模式等基本功能。
定位学习人群是刚接触微信小程序的,零基础同学。
本人刚接触微信小程序时,也是零基础,HTML和CSS都是第一次接触,经过两个星期的学习,就掌握了基本的开发技巧,并独立完成多个项目。所以即使没有这方面经验的同学也不要气馁,只要学习几个实战项目之后,应付工作基本上都是绰绰有余的。
效果图展示:
目录
创建小程序
落地页可适配长度界面
-
落地页底部栏:
- 客服按钮,购买按钮
- 微信登录授权
- 底部用户协议,用户协议同意定位
-
3 x 3 按钮组件
- 组件使用
- flex-grow
- flex-shrink
- 组件传递数据
- CSS关键帧动画
- 组件使用
拼团成功组件
-
广告轮播
- 水平广告轮播
- 消息轮播
下浮层
Notification 监听者模式
Toast 提示弹窗
总结
代码下载
创建
创建一个小程序项目,如果只是学习,那只需要下载安装 微信开发者工具。如果是商用的话,需要申请APPID,并根据自己需要开通相应的功能,例如支付接口,以及申请自己的资源CDN。
下面介绍如何创建一个小程序:
启动工具之后,点击“+” 创建小程序。
如果没有申请 APPID,可以使用测试号,就是随机生成的测试号,只是本地开发使用,不可以商用。
选择 不使用云服务,点击创建,选择编程语言。然后点击确定。
这样就来到项目界面。
--- NEXT ---
落地页可适配长度界面
本小节,我们来实现一下落地页可适配长度的滚动界面。
需要创建 page,名称就叫做 landingpage。
在app.json中,添加启动页,输入名称,按下回车,会自动在 pages/ 路径下生成文件夹,并生成 landingpage.js,landingpage.json,landingpage.wxml,landingpage.wxss 四个文件。
{
"pages":[
"pages/landingpage/landingpage",
...
]
}
我习惯先写 .wxml 文件,然后在 .wxss 文件中随时调试界面样式,涉及到引用的组件,在 .json 文件中添加即可。界面逻辑写在 .js 文件中。
分析界面结构:
- 整体结构为纵向垂直布局,可以先设置几张图片依次平铺。
先设置 landingpage 整体样式:
<view class='main-wrap'></view>
.main-wrap {
position: relative;
display: flex;
flex-direction: column;
background: #EEE;
}
使用 wx:for 设置一组图片,wx:key可以写成 *this
<block wx:for="{{bannerImgList}}" wx:for-index="index" wx:key="*this">
<image class="banner" mode="widthFix" src="{{item}}" lazy-load="true" />
</block>
图片样式为:
.banner {
width: 100%;
height: auto;
}
这里 bannerImgList 为本地一组图片资源,在 data 中声明:
bannerImgList: [
'../../images/landingpage1.jpg',
'../../images/landingpage6.jpg',
'../../images/landingpage7.jpg',
'../../images/landingpage8.jpg',
]
这样,落地页基本就有了一个简单的界面,图片从上到下铺满屏幕。
注意这里的图片在实际项目中,需要使用CDN的下载地址,不然本地资源太多,影响小程序加载速度,而且上传小程序也有尺寸限制。
Tips:
- 书写 wxml 标签快捷方式:
- 输入 view . className 回车,会自动生成 <view class="className"></view>
- 输入 view # idName 回车,会自动生成 *<view id="idName"></view>,其他标签同理。
- 微信小程序,自定义组件不支持 id 选择器,所以注意在组件中要使用类选择器。
- 图片加载方式设置为 lazy-load 表示需要显示图片时才显示,这样做能提高界面刷新效率。
--- NEXT ---
落地页底部栏:
本小节,我们布局底部栏,包含用户协议,和两个按钮。
分析界面布局
- 上下两层结构,内部为水平布局。
先创建一个bottom容器:
<view class="bottom-box"></view>
.bottom-box {
position: relative;
width: 100%;
height: 120rpx;
}
再添加两个按钮和文字:
<view id="bottom-wrap" style="padding-bottom:{{safeAreaHeight}}rpx;">
<view id="kefu" bindtap="tapKefu" hover-class="button-hover">
<image id="kefu-icon" mode="widthFix" src="../../images/kefu.png"></image>
<text id="kefu-txt">咨询</text>
</view>
<button class="button-normal" hover-class="button-hover" bindtap="getUserProfile">购 买</button>
</view>
Tips:
- bindtag 按钮点击事件的回调函数名称
- hover-class 按钮选中样式
- getUserProfile 是微信授权接口,固定写法
- safeAreaHeight 是为了适配
样式如下:
#bottom-wrap {
position: fixed;
width: 100%;
bottom: 0;
z-index: 1;
background: #fff;
display: flex;
flex-direction: row;
/* padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom); */
}
#kefu {
position: relative;
margin-top: 10rpx;
margin-left: 30rpx;
width: 200rpx;
height: 100rpx;
display: flex;
flex-direction: row;
border: 2rpx solid green;
border-radius: 100rpx;
background-color: #EEE;
justify-content: space-evenly;
align-items: center;
}
#kefu-icon {
margin: 0;
padding: 0;
width: 70rpx;
height: 70rpx;
}
#kefu-txt {
margin: 0;
padding: 0;
font-size: 30rpx;
line-height: 30rpx;
color: green;
}
.button-normal {
position: relative;
padding: 0;
margin: 10rpx;
margin-right: 30rpx !important;
width: 400rpx !important;
height: 100rpx;
display: flex;
flex-direction: row;
border-radius: 100rpx;
background-color: #FF6400;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: #fff;
}
.button-hover {
opacity: 0.75;
}
Tips:
- constant(safe-area-inset-bottom) 和 env(safe-area-inset-bottom)
是适配 iPhoneX 底部 "Dock" 栏的方法。但是适配的高度偏高,这里还是使用自定义高度。 - position 是标签需要经常使用的定位属性,一般常用的是
- position: relative; 相对位置
- position: absolute; 绝对位置,常用于浮动在父级节点上,不会撑起父级容器。
- position: fixed; 固定位置,常用于固定在界面的下方或者上方,不会随着窗体滚动而变化位置。
自定义适配高度,具体计算规则在如下代码中:
const system = wx.getSystemInfoSync();
const windowHeight = Math.round(system.windowHeight);
const safeArea = system.safeArea && system.safeArea.top > 20 ? system.safeArea : { top: 0 };
const safeAreaHeight = safeArea.top / 2;
- 底部栏需要始终固定在屏幕最下方,所以使用 position: fixed
客服按钮,购买按钮
- 点击客服按钮的回调函数是 tapKefu
- 点击购买按钮的回调函数是 getUserProfile
微信登录授权
-
getUserProfile 是微信官方提供的接口,用于唤起用户微信授权。
可以获得用户的用户名,微信昵称,头像地址等个人信息。
如果用户不同意,那么不会获得相应的数据。
并且该 API 无法主动调起,必须通过绑定点击事件。
wx.getUserProfile({
lang: 'zh_CN',
desc: '用于完善用户资料',
success: res => {
...
},
fail: err => {
...
},
complete: param => {
...
}
})
这里包含成功,失败,还有完成(无论成功失败都会走的),三个处理函数。可以在这里实现业务逻辑。
底部用户协议,用户协议同意定位
一般界面都会设计诸如 “用户协议”,”个人信息保护声明“,“电信业务经营许可证”,之类的信息。
如下:
<!-- 备案信息 -->
<view class="question-wrap">
<view class="record-wrap">
<image class="choose-record"
src="{{chooseRecord ? chooseRecordImg : unChooseRecordImg}}"
catchtap="tapChoose" />
<text>我同意</text>
<text class="record" catchtap="tapRecord">《个人信息授权及保护声明》</text>
<text>和</text>
<text class="record" catchtap="tapRecord">《用户协议》</text>
</view>
<text class="question-desc">XXXXXXXX公司 京ICP备123456789号</text>
</view>
其实就是几个文字和URL组成的。
这里点击授权信息,跳转到一个内嵌的 webView 界面,显示 H5 链接。我就暂时写成 bai du 地址了,可以替换成真实业务地址。
const h5 = 'www.baidu.com';
const url = `../../pages/commonWebView/commonWebView?
url=${encodeURIComponent(h5)}&share=false`;
wx.navigateTo({ url });
commonWebView页面也很简单,只需要实现对应的回调函数即可,详细代码实现,可下载代码包,仔细查看,这里篇幅有限不再占用。
<web-view src="{{url}}" bindmessage="webViewObserverMessage"
bindload="webViewLoadSuccess" binderror="webViewLoadError" />
--- NEXT ---
3 x 3 按钮组件
在落地页经常需要实现一个可以点击的按钮列表,为用户提供直观的可选产品。
组件使用
- 创建组件,在根目录下创建 components 路径。
- 在 components 下创建组件文件夹,右键文件夹创建组件,文件夹名称和组件名称尽量一致。
- 在需要引用组件的 wxml 中,以标签形式使用组件。
- 在需要引用组件的 json 文件 usingComponents 数组下,添加组件相对路径。
这里新建一个 <Fruits></Fruits> 水果按钮列表组件。
分析界面结构:
- 纵向三层,整体居中。
- 第一层是标题 “请选择水果”,外加两个小手动画。内部是水平布局。
- 第二层是 3 x 3 按钮列表,里面有文字,有点击事件,有按钮样式,整体居中布局。
- 第三层是一个居中布局的文字。
结构很简单,开始动手写 wxml
<view class="title-wrap">
<image class="finger" src="{{fingerImg}}"/>
<text>请选择要购买的水果</text>
<image class="finger" src="{{fingerImg}}"/>
</view>
<view class="fruits-list">
<view class="fruit" wx:for="{{fruitsList}}" wx:key="index" data-index="{{index}}" catchtap="tapFruit">
<text>{{item}}</text>
<button class="fruit-btn" bindtap="getUserProfile" data-index="{{index}}"></button>
</view>
</view>
<view class="tips">{{'*购买成功记得五星好评哦'}}</view>
样式:
.title-wrap {
margin: 40rpx auto;
padding: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-grow: 1;
flex-shrink: 1;
font-size: 34rpx;
color: #000;
font-family: PingFangSC-Semibold,PingFang SC;
}
.finger {
width: 44rpx;
height: 50rpx;
}
.fruits-list {
position: relative;
margin: 0 50rpx;
display: flex;
flex-flow: row wrap;
}
.fruit {
position: relative;
margin: 0 8rpx 24rpx;
width: 200rpx;
background: #fff;
border: 2rpx solid rgba(255, 107, 44, 1);
border-radius: 10rpx;
font-size: 28rpx;
line-height: 72rpx;
font-weight: 600;
color: rgba(255, 98, 3, 1);
text-align: center;
}
.fruit:nth-child(3n+1) {
margin-left: 0;
}
.fruit:nth-child(3n) {
margin-right: 0;
}
.fruit-btn {
background: transparent;
width: 100% !important;
height: 100%;
z-index: 1;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.tips {
margin: 12rpx 30rpx 38rpx;
font-size: 24rpx;
font-weight: 300;
color: #999999;
line-height: 24rpx;
letter-spacing: 1rpx;
text-align: center;
}
Tips:
- 小手指是上下移动的,可以使用CSS关键帧动画实现
- 按钮列表中,需要绑定按钮序号,使用 data-index 绑定循环中的序号。
- 节点自适应左右居中常用的方法就是设置 margin: 0 auto 。
这里面用到了 flex-grow 和 flex-shrink。着重简单介绍一下,因为这两个属性经常会用到。
flex-grow
flex-grow 处理父元素在还有剩余空间时的分配规则,分为两种情况。
即:所有元素的 flex-grow 值之和大于1,和小于1。
-
大于1时,例如:
父元素宽600,子元素A和B,宽分别为200,300。还剩余100。
此时A,B的 flex-grow 分别为2,3。则剩余100,分给A 2/5,分给B 3/5。
A,B宽度为:200 + 40 = 240 300 + 60 = 360
-
小于1时,作为分母的总和会引入1来处理。例如:
上例中,A,B flex-grow 分别为 0.2,0.3。则分给A 0.2/1,分给B 0.3/1。
A,B宽度为:200 + 20 = 220 300 + 30 = 330
还剩50没有分配给任何子元素扩张。
另外,flex-grow 还会受到父元素的 max-width 影响。如果grow后的结果超出 max-width,max-width 会优先生效。
flex-shrink
和 flex-grow 处理父元素剩余空间相对应的,是 flex-shrink 处理父元素空间不足时,子元素的收缩规则。
同样分为两种情况,所有元素的 flex-shrink 值之和大于1,和小于1。
-
大于1时,例如:
父元素宽度为600,子元素宽度为400,300。超出100。
A,B flex-shrink 分别为 1,2。总权重为400 + 300 * 2 = 1000
A收缩 -100 * 1 * 400 / 1000 = -40 B收缩 -100 * 2 * 300 / 1000 = -60
A,B实际宽度为:
400 - 40 = 360 300 - 60 = 240
-
小于1时,例如,
A,B flex-shrink 分别为 0.1,0.2。总权重为400 * 0.1 + 300 * 0.2 = 100
子元素收缩总和为100 * 0.3 / 1 = 30
A收缩 -30 * 0.1 * 400 / 100 = -12 B收缩 -30 * 0.2 * 300 / 100 = -18
A,B实际宽度为:
400 - 12 = 388 300 - 18 = 282
多出70没有分配给任何子元素收缩。
同样,也会受到min-width的限制。
组件传递数据
父组件向子组件传递数据
在组件的属性列表中新增参数字段:
properties: {
option: {
type: Boolean,
value: true
}
}
这个属性需要在使用组件的位置赋值,并作为参数传递下去:
<Fruits option="{{true}}"></Fruits>
子组件向父组件传递数据
在子组件内 trigger 一个事件,然后在子组件被引用的位置 bind 事件。并且在事件响应函数中,使用传递过来的数据。
Trigger:
this.triggerEvent('eventName', { index });
可以在后面夹带参数。Bind:
<Fruits option="{{true}}" bindeventName="callBack"></Fruits>
-
CallBack:
callBack: function (e) { // 事件传递过来的参数 const index = e.detail.index; }
CSS关键帧动画
为了实现手指向下的小动画,使用关键帧处理。
如果在 @keyframes 规则中指定了 CSS 样式,动画将在设定时间逐渐从当前样式更改为新样式。
.finger:first-child {
margin-right: 10rpx;
animation: moveDownLeft .9s infinite;
}
.finger:last-child {
margin-left: 10rpx;
animation: moveDownRight .9s infinite;
}
- 左右手各使用一个动画,因为如果使用同一个动画,再Y轴翻转一下也可以。但是会出现左右动画不同时运动的问题。
- 0.9s 是持续时间,infinite 是无限循环。
keyframes :
@keyframes moveDownLeft {
0% {
transform: translateY(0rpx);
}
50% {
transform: translateY(9rpx);
}
100% {
transform: translateY(0rpx);
}
}
@keyframes moveDownRight {
0% {
transform: translateY(0rpx) scale(-1, 1);
}
50% {
transform: translateY(9rpx) scale(-1, 1);
}
100% {
transform: translateY(0rpx) scale(-1, 1);
}
}
左手动作设置了从开始到一半,再到结束时的Y轴位移。右手Y轴动作一致,只不过水平翻转一下。
--- NEXT ---
拼团成功组件
在落地页中加入拼团成功动画。同样也是使用组件实现。
动画效果设计为,开始显示两个人已在团内,另有一个人的头像在拼团成功时飞入第三个头像框,表示拼团成功。同时文字由“即将成团”变成“拼团成功”。并且倒计时持续刷新。拼团成功会有一个标志章显示出来,然后头像和拼团文字整体向上滚动,最后刷新出下一组拼团头像。
最右侧是一个去拼团的点击按钮。
写动画的难点不是动作怎样写,而是整体的节奏感是否协调。
分析界面布局:
纵向布局分三层
- 第一层标题,里面水平结构,包含三个文字。
- 第二层头像,文字,按钮,另外还有一个飞动的图片。这里头像又可以做成组件。
- 第三层是一个拼团成功的图片。
PinTuan组件
<view class="wrap">
<view class="text-wrap">
<text>还差</text>
<text class="persion-num">{{personNum}}人</text>
<text>成团,可直接参与</text>
</view>
<view class="pintuan-content">
<PinTuanHead class="pin-tuan" headUrls="{{headUrls}}" animation="{{pinTuanAni}}" bindtransitionend="pinTuanAniEnd" isNeedLogin="{{isNeedLogin}}"></PinTuanHead>
<view class="join" animation="{{pinTuanAni}}" bindtransitionend="pinTuanAniEnd">
<text class="join-text">{{joinText}}</text>
<text class="clock">还剩{{clockText}}</text>
</view>
<image animation="{{headAniData}}" bindtransitionend="headAniEnd" class="move-head" src="{{moveHead}}"></image>
<button class="goGroup" bindtap="getUserProfile">去参团</button>
</view>
<image wx:if="{{pinTuanSuccess}}" class="successed" src="{{successedImg}}" mode="widthFix"
animation="{{successAni}}" bindtransitionend="successAniEnd" style="transform: scale(0.3) opacity(0)"></image>
<view class="bottom-border"></view>
</view>
animation动画
使用animation动画,可以实现复杂的动作流程。动画的开始和结束都需要处理逻辑。
创建动画后,需要导出一下,代码实现如下:
let ani = wx.createAnimation({
delay: 0,
duration: 500,
timingFunction: 'ease'
});
ani.opacity(0).translateY(-30).step();
this.setData({
pinTuanAni: ani.export()
});
先设置动画属性,再设计动画运动轨迹,最后导出:
-
timingFunction: 'ease'
设置缓动效果。 -
ani.opacity(0).translateY(-30).step();
先透明度为0,然后Y轴坐标。 -
step()
表示一组动画完成。可以在一组动画中调用任意多个动画方法,一组动画中的所有动画会同时开始,一组动画完成后才会进行下一组动画。 -
bindtransitionend
是设置动画结束时的回调函数。
代码和样式请下载资源包,对应 PinTuan 文件夹下,因篇幅有限,这里不列出详细代码
PinTuanHead组件
分析界面布局:
三个头像,分为头像背景图,和真实头像图。并且需要动态控制头像显示。
<view class="wrap">
<image wx:if="{{person1Show}}" class="icon" src="{{headUrls[0].avatar}}" mode="widthFix"></image>
<image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
<image wx:if="{{person2Show}}" class="icon" src="{{headUrls[1].avatar}}" mode="widthFix"></image>
<image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
<image wx:if="{{person3Show}}" class="icon" src="{{headUrls[2].avatar}}" mode="widthFix"></image>
<image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
</view>
代码和样式在资源包 PinTuanHead 文件夹
Tips:
- 样式中使用 :nth-child 表示同类标签的第几个标签。这类伪标签可以节省 wxml 空间,减少 document 渲染的节点数量。
--- NEXT ---
广告轮播
微信提供轮播图组件,可以设置轮播间隔,提示点,循环等属性。
水平广告轮播
设置一组图片水平方向循环轮播
<swiper class="banner-scroll" indicator-dots="{{true}}" indicator-active-color="skyblue" indicator-color="#fff" autoplay="{{true}}" interval="{{5000}}" circular="{{true}}" duration="{{500}}">
<block wx:for="{{swiperList}}" wx:key="*this">
<swiper-item>
<image class="box-image" src="{{item}}" mode="widthFix"/>
</swiper-item>
</block>
</swiper>
消息轮播
左上角设置纵向消息轮播。
分析界面布局:
- 水平布局,左边是用户头像,右边是文字。
- 文字显示分两种情况,如果是带拼团的,就随机显示“刚刚拼团成功”和“刚刚参团成功”文字。如果不带拼团,就显示“刚刚抢单成功”。
<view class="recent-payment-list-wrap">
<swiper class="recent-payment-list" vertical="{{true}}" autoplay="{{true}}" interval="{{3000}}" circular="{{true}}"
duration="{{500}}" capture-catch:touchmove='preventTouchMove'>
<block wx:for="{{recentPaymentUsers}}" wx:for-index="index" wx:key="index">
<swiper-item>
<view class="recent-payment-cell">
<view class="recent-payment-cell-content">
<image class="recent-payment-avatar" src="{{item.avatar}}" mode="aspectFill" />
<view class="recent-payment-name">{{item.name}}</view>
<view wx:if="{{showPinTuan}}">
<view wx:if="{{item.pinTuanRandom}}" style="flex-shrink: 0;">刚刚拼团成功</view>
<view wx:else style="flex-shrink: 0;">刚刚参团成功</view>
</view>
<view wx:else style="flex-shrink: 0;">刚刚抢单成功</view>
</view>
<view style="flex-grow: 1;"></view>
</view>
</swiper-item>
</block>
</swiper>
</view>
代码和样式在资源包,landingpage 文件夹下
--- NEXT---
下浮层
点击水果按钮,弹出注册手机号下浮层。如果已经注册手机号,弹出订单详情弹窗。
切换下浮层显示通过 promptStatus 值为0或者1决定。
下浮层封装为组件 FruitPrompt,自定义组件的显隐,不能通过设置 hidden 实现。可以设置 wx:if 条件判断显示。
为了方便处理下浮层的显示,设置一个浮层基类组件 Prompt,FruitPrompt 继承自 Prompt
Prompt:
<view class='prompt {{slowDown?"hideOpacity":""}}' data-type="mask" catchtap='closeCallback' catchtouchmove='touchMove'>
<view class='container {{slowDown?"slowDown":""}}' catchtap='catchEvent'>
<view class='title'>{{title}}</view>
<image src='{{iconClose}}' class='icon' catchtap='closeCallback' data-type="button" />
<slot></slot>
<view wx:if="{{showButton}}" class='btnContainer'>
<button class="menuBtn" bindtap="btnCallback">{{btnText}}</button>
</view>
</view>
</view>
注册手机号,获取验证码,验证码倒计时
分析界面布局
纵向布局:
- 手机号和验证码用到 input 标签。
- 输入手机号和验证码之后,“获取验证码” 按钮高亮,并可以点击。
<block wx:if="{{status === 1}}">
<view class="section-title">注册手机号</view>
<view class="phone-cell">
<text>*</text>
<input type="number" maxlength="11" placeholder="点击输入手机号" class="input" value="{{phoneNumber}}"
focus="{{phoneNumberFocus}}" bindinput="phoneInput" />
</view>
<view class="phone-cell">
<text>*</text>
<view class="input-wrap">
<input type="number" maxlength="6" placeholder="请输入验证码" placeholder-class="input-placeholder" class="input"
value="{{verifyCode}}" bindinput="verifyInput" />
<view class="verify-button" hover-class="verify-btn-hover" hover-stay-time="100"
style="{{inputCodeButtonStyle}}" catchtap="tapGetVerifyCode">{{inputCodeButtonTitle || '获取验证码'}}</view>
</view>
</view>
<view class="section-title">商品信息</view>
<view class="order-info-wrap">
<view class="order-info-name">自定义文字内容</view>
<view class="order-info-time">2021:01:01 00:00-2021:12:31 00:00</view>
</view>
</block>
注册手机号,需要实现验证码功能,点击获取验证码,校验手机号输入合法性。合法则申请验证码,并且进入 60s 倒计时。
验证码倒计时部分,利用 setInterval 封装一个公共的倒计时函数,提供异步回调函数。
function initCountdown({
isCheck: isCheck = false,
name: name,
timeTotal: timeTotal,
timeInterval: timeInterval,
checkCallback: checkCallback,
timeChangedCallback: timeChangedCallback,
endCallback: endCallback
}) {
if (typeof name !== 'string' || !name) {
return;
}
const countdownInterval = countdownMap[name];
if (countdownInterval) {
clearInterval(countdownMap[name].interval);
} else if (isCheck) {
if (typeof checkCallback === 'function') {
checkCallback();
}
return;
} else {
countdownMap[name] = {
timeTotal: timeTotal,
timeInterval: timeInterval
};
}
if (typeof timeChangedCallback === 'function') {
timeChangedCallback(countdownMap[name].timeTotal);
}
countdownMap[name].interval = setInterval(() => {
if (countdownMap[name].timeTotal <= 0) {
clearInterval(countdownMap[name].interval);
delete countdownMap[name];
if (typeof endCallback === 'function') {
endCallback();
}
return;
}
countdownMap[name].timeTotal -= countdownMap[name].timeInterval;
if (typeof timeChangedCallback === 'function') {
timeChangedCallback(countdownMap[name].timeTotal);
}
}, countdownMap[name].timeInterval);
}
在点击验证码按钮时,触发倒计时。
initCountdownManager(isCheck) {
countdownManager.initCountdown({
isCheck: isCheck,
name: 'bindPhone.verifyCode',
timeTotal: 60000,
timeInterval: 1000,
checkCallback: () => {
this.data.countingdown = false;
},
timeChangedCallback: countdown => {
this.setData({
inputCodeButtonTitle: `重新发送(${parseInt(countdown / 1000)}s)`,
inputCodeButtonStyle: 'color: #CCCCCC;'
});
},
endCallback: () => {
this.setData({
inputCodeButtonTitle: '重新发送',
inputCodeButtonStyle: 'color: #FF8134;'
});
this.data.countingdown = false;
}
});
}
Tips:
- setData 函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data 的值(同步)。
- 如果更新数据之后,没有使用 setData 函数
例如:this.data.countingdown = false
则只是将数据写入 this.data,不能刷新界面显示。
订单详情
设计显示售罄标记,先到先得标记。
分析界面布局:
纵向结构;
- 水果按钮列表,3 x 3 列表。
- 按钮右上角设置标签。
- 订单介绍,保质期时长。介绍按钮右上角有标签。
<block wx:if="{{status === 0}}">
<view class="choose-wrap">
<view class="section-title">请选择要购买的水果</view>
<view class="choose-fruit-list">
<view
class="choose-fruit-item {{fruit.soldout ? 'item-soldout' : '' }} {{fruit.disable ? 'item-disable' : '' }}"
wx:for="{{fruits}}" wx:for-item="fruit" wx:for-index="index" wx:key="*this" data-index="{{index}}"
catchtap="tapChooseFruit">
{{fruit.title}}
</view>
</view>
<block wx:if="{{!currentFruit.soldout}}">
<view class="section-title">
<text>选择水果发货时间</text>
<text class="choose-fruit-tips">新鲜水果,好吃不贵</text>
</view>
<scroll-view enable-flex="true" scroll-y class="choose-term-list">
<view class="choose-term-item {{term.selected ? 'item-selected' : ''}}" wx:for="{{chooseTerms}}"
wx:for-item="term" wx:for-index="termIndex" wx:key="*this" data-index="{{termIndex}}"
catchtap="tapChooseTerm">
<text>{{term.name}}</text>
<view class="choose-term-item-tips">{{term.tip}}</view>
</view>
</scroll-view>
</block>
<block wx:else>
<view class="soldout-title">该水果已售罄</view>
<view class="soldout-desc">到货第一时间联系您</view>
</block>
</view>
</block>
代码和样式请下载资源包,对应 FruitPrompt 文件夹下,因篇幅有限,这里不列出详细代码
Notification 监听者模式
处理逻辑,经常需要用到监听者模式。实际原理很简单,只需一个数组,将需要监听的对象和钩子函数压栈,然后在捕获到钩子时,在出栈。
const observerList = [];
// 添加观察者
function addObserver(notificationName, selector, target) {
const observer = {
name: notificationName,
target: target,
selector: selector
}
observerList.push(observer);
}
// 发送通知
function postNotification(notificationName, data = {}) {
for (let i = 0; i < observerList.length; i++) {
const observer = observerList[i];
if (notificationName === observer.name) {
observer.selector(data);
}
}
}
--- NEXT ---
Toast 提示弹窗
封装各种提示弹窗。使用 wx.showToast 我们再封装一层,可以提示各种自定义信息,也可以加自定义 icon。
// 文字提示框
function showTextToast(title, cb, seconds, mask = true) {
showToast({
title: title,
icon: 'none',
mask: mask,
callback: cb,
seconds: seconds
})
}
// 加载提示框
function showLoadingToast(title, cb, seconds) {
showToast({
title: title,
icon: 'loading',
mask: true,
callback: cb,
seconds: seconds
})
}
// 成功提示框
function showSuccessToast(title, cb, seconds) {
showToast({
title: title,
icon: 'success',
mask: true,
callback: cb,
seconds: seconds
})
}
// 错误提示框
function showErrorToast(title, cb, seconds) {
showToast({
title: title,
image: 'XXXX',
icon: 'none',
mask: true,
callback: cb,
seconds: seconds
})
}
// 文字提示框
function showToast({
title: title,
icon: icon,
image: image,
mask: mask,
callback: callback,
seconds: seconds
}) {
if (!title) {
if (callback) {
callback()
}
return;
}
if (!seconds) {
seconds = 1.7;
}
wx.showToast({
title: title,
icon: icon,
image: image,
mask: mask,
duration: seconds * 1000
});
setTimeout(function () {
if (callback) {
callback()
}
}, seconds * 1000);
}
总结
微信小程序开发,常用标签和 style 样式并不多,很容易掌握。
常用 wxml 标签:
- view 当作节点使用
- image 图片
- text 文字
- block 不占位标签
- swiper 轮播
- scroll-view 滚动层
- web-view H5内嵌
- input 输入框
- button 按钮
常用 style 样式
- position: relative, absolute, fixed 设定节点坐标
- margin, padding 设定节点边距,margin是外边距,padding是内边距
- display 设定元素的显示类型
- width, height 宽高
- top, bottom, left, right 设定 absolute 坐标后设定上,下,左,右,间距
- background 背景,可以设定颜色,背景图片,背景尺寸
- z-index Z 轴优先级
- font 字体,可以设定字体库,字体颜色,阴影,描边,字体大小,字间距,行间距
- border 边框,圆角,自定义边角
- animation 动画,可以设定逐帧动画,也可以绑定动画事件
常用技巧
居中:
- 左右居中:父节点需要设置 display:flex; 然后子节点设置 margin: 0 auto; 子节点可以水平左右居中。
- 节点内容居中:同样,也需要父节点设置 display:flex; 然后父节点再设置 align-items: center; 可以实现内部元素水平和垂直都居中。
- 文字水平居中:设置父节点 text-align: center; 可以实现内部文字水平居中。
- 文字垂直居中:text 标签 font-size 和 line-height 设置一致时,文字垂直居中。
- 依靠元素显示类型居中:
display: flex; justify-content: center; // 水平居中 vertical-align: middle; // 垂直居中
--- NEXT ---