实战零基础小程序落地页项目

前言

微信小程序简单易上手,只要有一些编程基础,即可快速开发基本的项目。
本项目是常见的,商品广告落地页小程序。提供商品浏览,商品列表按钮,购买,微信授权,手机号绑定,验证码校验,用户协议,消息通知,监听者模式等基本功能。
定位学习人群是刚接触微信小程序的,零基础同学。
本人刚接触微信小程序时,也是零基础,HTML和CSS都是第一次接触,经过两个星期的学习,就掌握了基本的开发技巧,并独立完成多个项目。所以即使没有这方面经验的同学也不要气馁,只要学习几个实战项目之后,应付工作基本上都是绰绰有余的。


效果图展示:

效果图.png



目录

  1. 创建小程序

  2. 落地页可适配长度界面

  3. 落地页底部栏:

    • 客服按钮,购买按钮
    • 微信登录授权
    • 底部用户协议,用户协议同意定位
  4. 3 x 3 按钮组件

    • 组件使用
      • flex-grow
      • flex-shrink
    • 组件传递数据
    • CSS关键帧动画
  5. 拼团成功组件

  6. 广告轮播

    • 水平广告轮播
    • 消息轮播
  7. 下浮层

  8. Notification 监听者模式

  9. Toast 提示弹窗

  10. 总结

  11. 代码下载


创建

创建一个小程序项目,如果只是学习,那只需要下载安装 微信开发者工具。如果是商用的话,需要申请APPID,并根据自己需要开通相应的功能,例如支付接口,以及申请自己的资源CDN



下面介绍如何创建一个小程序:

create-1.png


启动工具之后,点击“+” 创建小程序。

create-2.png


如果没有申请 APPID,可以使用测试号,就是随机生成的测试号,只是本地开发使用,不可以商用。
create-3.png
选择 不使用云服务,点击创建,选择编程语言。然后点击确定。
create-4.png
这样就来到项目界面。

--- 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 标签快捷方式:
    1. 输入 view . className 回车,会自动生成 <view class="className"></view>
    2. 输入 view # idName 回车,会自动生成 *<view id="idName"></view>,其他标签同理。
  • 微信小程序,自定义组件不支持 id 选择器,所以注意在组件中要使用类选择器。
  • 图片加载方式设置为 lazy-load 表示需要显示图片时才显示,这样做能提高界面刷新效率。

--- NEXT ---


落地页底部栏:

本小节,我们布局底部栏,包含用户协议,和两个按钮。

bottom.png


分析界面布局
  • 上下两层结构,内部为水平布局。


先创建一个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


客服按钮,购买按钮

  1. 点击客服按钮的回调函数是 tapKefu
  2. 点击购买按钮的回调函数是 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 按钮组件

fruits-list.png


在落地页经常需要实现一个可以点击的按钮列表,为用户提供直观的可选产品。


组件使用

  1. 创建组件,在根目录下创建 components 路径。
  2. components 下创建组件文件夹,右键文件夹创建组件,文件夹名称和组件名称尽量一致。
  3. 在需要引用组件的 wxml 中,以标签形式使用组件。
  4. 在需要引用组件的 json 文件 usingComponents 数组下,添加组件相对路径。


这里新建一个 <Fruits></Fruits> 水果按钮列表组件。

依赖关系-Fruits.png


分析界面结构:
  • 纵向三层,整体居中。
  • 第一层是标题 “请选择水果”,外加两个小手动画。内部是水平布局。
  • 第二层是 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-growflex-shrink。着重简单介绍一下,因为这两个属性经常会用到。


flex-grow

flex-grow 处理父元素在还有剩余空间时的分配规则,分为两种情况。

即:所有元素的 flex-grow 值之和大于1,和小于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
    
  2. 小于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没有分配给任何子元素扩张。

  3. 另外,flex-grow 还会受到父元素的 max-width 影响。如果grow后的结果超出 max-widthmax-width 会优先生效。

flex-shrink

flex-grow 处理父元素剩余空间相对应的,是 flex-shrink 处理父元素空间不足时,子元素的收缩规则。

同样分为两种情况,所有元素的 flex-shrink 值之和大于1,和小于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
    
  2. 小于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没有分配给任何子元素收缩。

  3. 同样,也会受到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 ---


拼团成功组件

group.gif


在落地页中加入拼团成功动画。同样也是使用组件实现。

动画效果设计为,开始显示两个人已在团内,另有一个人的头像在拼团成功时飞入第三个头像框,表示拼团成功。同时文字由“即将成团”变成“拼团成功”。并且倒计时持续刷新。拼团成功会有一个标志章显示出来,然后头像和拼团文字整体向上滚动,最后刷新出下一组拼团头像。

最右侧是一个去拼团的点击按钮。

写动画的难点不是动作怎样写,而是整体的节奏感是否协调。

分析界面布局:

纵向布局分三层

  • 第一层标题,里面水平结构,包含三个文字。
  • 第二层头像,文字,按钮,另外还有一个飞动的图片。这里头像又可以做成组件。
  • 第三层是一个拼团成功的图片。

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.jpg


微信提供轮播图组件,可以设置轮播间隔,提示点,循环等属性。

水平广告轮播

设置一组图片水平方向循环轮播

<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 条件判断显示。

为了方便处理下浮层的显示,设置一个浮层基类组件 PromptFruitPrompt 继承自 Prompt

依赖关系-FruitPrompt.png

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>

注册手机号,获取验证码,验证码倒计时

prompt-1.png
分析界面布局

纵向布局:

  • 手机号和验证码用到 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不能刷新界面显示


订单详情

设计显示售罄标记,先到先得标记。


prompt-2.png
分析界面布局:

纵向结构;

  • 水果按钮列表,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-sizeline-height 设置一致时,文字垂直居中。
  • 依靠元素显示类型居中:
    display: flex;
    justify-content: center; // 水平居中
    vertical-align: middle;  // 垂直居中
    

--- NEXT ---


代码下载

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

推荐阅读更多精彩内容