likes
comments
collection
share

活动报名小程序模板(二)

作者站长头像
站长
· 阅读数 16

接上一篇:活动报名小程序模板(一)

2. 视图样式

2.6 自定义评分组件

  1. 需求描述:

    需要实现一个自定义组件如下图,要求能够根据评分显示星星的数量,评分可以为非整数: 活动报名小程序模板(二)

  2. 实现方案:

    需求要求使用TDesign中的icon图标,而该图标没有提供相应的按照比例填充颜色的属性,只有star-fulfilled​​填充星星和star​​线条星星两种;

    我的实现思路是上下两层五星图标,底层灰色,顶层橙色,顶层通过浮动定位与底层在z轴方向上重叠,通过控制顶层显示的宽度来实现展示半星的效果。

    此处可以使用overflow: hidden​​属性,那么五颗星星的位置与宽度就需要固定,通过改变width​​来截断部分星星:

    <!--components/stars-mark/index.wxml-->
    <view class="score">
      <view class="stars-container">
        <view class="stars-background">
          <t-icon name="star-filled" size="16px" />
          <t-icon name="star-filled" size="16px" />
          <t-icon name="star-filled" size="16px" />
          <t-icon name="star-filled" size="16px" />
          <t-icon name="star-filled" size="16px" />
        </view>
        <view class="stars" style="width: {{score*16 + (score-0.5)*5.15}}px">  <!--动态变化宽度-->
          <t-icon name="star-filled" size="16px" />
          <t-icon name="star-filled" size="16px" />
          <t-icon name="star-filled" size="16px" />
          <t-icon name="star-filled" size="16px" />
          <t-icon name="star-filled" size="16px" />
        </view>
      </view>
      <text>{{score}}分</text>
    </view>
    
    /* components/stars-mark/index.wxss */
    .score {
      height: 20px;
      width: 143.6px;
      font-size: 12px;
      font-weight: 600;
      font-family: "PingFang SC";
      line-height: 20px;
      display: inline-flex;
      position: relative;
    }
    .score text{
      color: #e37318;
      position: absolute;
      right: 0;
    }
    .stars-container{
      display: inline-flex;
      width: 100.6px;
      position: relative;
    }
    .stars-background{
      color: #dcdcdc;
      display: inline-flex;
      padding-top: 2px;
    }
    .stars{
      color: #e37318;
      display: inline-flex;
      padding-top: 2px;
      position: absolute;
      overflow: hidden;
    }
    .stars-background view,
    .stars view{
      padding-right: 5.15px;
    }
    
    // components/stars-mark/index.js
    Component({
      /**
       * 组件的属性列表
       */
      properties: {
        score: {
          "type": Number,
          "value": ""
        }
      },
      ...
    })
    
    {
      "component": true,
      "usingComponents": {
        "t-icon": "tdesign-miniprogram/icon/icon"
      }
    }
    

    在上面的less文件中的代码并不是很好的less风格代码,还需要进行进一步优化,这里只记录实现的思路。

2.7 tabBar遮挡底部内容

  1. 问题描述:

    使用自定义tabbar组件时,由于页面的可滚动,可能会出现tabbr遮挡页面底部内容的问题:

    活动报名小程序模板(二)

  2. 问题分析:

    页面底部会被tabbar遮挡的原因是自定义的tabbar设置了fixed​​定位,独立于文档流中。因此可以通过将页面中的内容通过一个<scroll-view>​​包裹起来,设置与tabbar相等高度的padding-bottom​​与底部tabbar错开,实现不被遮挡的效果:

    <!--home.wxml-->
    <scroll-view scroll-y="true">
      <view class="swiper">
        ...
      </view>
      <view class="list">
        ...
      </view>
    </scroll-view>
    

    但有个问题是,页面的tabbar高度是随着机型的变化而变化的,所以不能将<scroll-view>​​的padding-bottom​​数值写死,需要进行机型的适配。查看自定义组件tabbar的盒子模型,可以知道高度组成有margin + height​​:

    活动报名小程序模板(二)

    查看组件样式代码,找到对应属性:

    height: var(--td-tab-bar-height, 80rpx);
    margin: 16rpx 0;
    

    此处的height​​性使用了变量,默认值为80rpx​​。由于在文件中没有对--td-tab-bar-height​​这一变量值进行设置,所以实际是采用默认值80rpx​​。此处通过使用相对单位​**rpx**​​实现动态变化高度,因此在设置scroll-view​​的padding​​时也要使用单位rpx​​。可以计算出tabbar的高度为112rpx​​。

    但是注意一点,对于iPhoneX系列的机型,底部会存在一部分黑色滑动条的空存区域:

    活动报名小程序模板(二)

    这个区域称为底部安全区域,是指不会被设备的虚拟按键、导航栏等系统 UI 元素遮挡的区域。开发过程中可以通过css环境变量safe-area-inset-bottom​​获取其值。对于这部分区域,tabbar也做了相应的处理,称为安全区域填充

    padding-bottom: constant(safe-area-inset-bottom);
    padding-bottom: env(safe-area-inset-bottom);
    

    其中,constant()​​ 和 env()​​ 是两种用于获取 CSS 环境变量值的函数,它们在获取变量值的方式和应用场景上有一些区别。

    • constant()​​ 函数**:**

      • constant()​​ 函数用于获取 CSS 自定义属性(变量)的值。这些自定义属性可以在 CSS 中定义,然后在整个文档中使用;
      • constant()​​ 函数会获取自定义属性的初始值,无论环境变化。即使环境变量的值发生变化,constant()​​ 也会保持最初定义的值不变,这意味着,constant()​​ 适合用于那些不会根据环境变化而变化的值,例如品牌颜色、固定尺寸等。
    • env()​​函数:

      • env()​​ 函数同样用于获取 CSS 自定义属性(变量)的值,但是它更倾向于获取环境变量的值,如设备的尺寸、安全区域等;
      • env()​​ 函数可以获取随环境变化而变化的值,比如在不同设备上,env()​​ 函数会根据环境提供不同的值;

    即,constant()​​ 和 env()​​ 都用于获取 CSS 自定义属性的值,但 constant()​​ 更适合获取在全局范围内保持不变的值,而 env()​​ 更适合获取随环境变化而变化的值,如设备尺寸、安全区域等。

    综上所述,最终scroll-view​​的padding-bottom​​值应该设置为:

    scroll-view{
      padding-bottom: calc(env(safe-area-inset-bottom) + 112rpx);
    };  // calc函数用于变量计算的处理
    

2.8 背景图不可使用本地图片

  1. 问题描述:

    在设置组件的背景图片时,在wxss​​​文件的background-image​​​属性中指定图片路径不生效:

    .fixed-container{
      background-image: url(../../src/imgs/topbar/topbar-bc.png);
    }
    

    查看开发者面板:

    活动报名小程序模板(二)

  2. 原因分析:

    微信小程序为了确保用户信息和设备的安全性,采用了沙盒环境的安全机制。沙盒环境是一种计算机安全机制,用于隔离和限制程序的运行环境,以确保系统的安全性和稳定性。在沙盒环境中,程序被限制在一个受控制的、受限的执行环境中运行,而不允许其访问或影响系统的关键部分或其他程序。

    沙盒环境的主要特点:

    • 隔离性: 沙盒环境将程序与其它程序和系统资源隔离开来,以防止不同程序之间的相互干扰和冲突。这意味着程序只能访问其被授权的资源和环境,不能越界访问;
    • 限制资源访问: 沙盒环境通常会限制程序对系统资源的访问,如文件系统、网络、内存等。程序只能在允许的范围内使用这些资源;
    • 安全性: 沙盒环境旨在提高系统的安全性,防止恶意代码或漏洞利用对系统造成危害。它可以防止程序执行危险的操作,如修改系统文件、操纵进程、窃取用户数据等;
    • 稳定性: 沙盒环境有助于维护系统的稳定性,因为它可以防止程序由于错误或异常情况而导致系统崩溃或出现严重问题。即使程序崩溃,它也不应该对整个系统造成影响;
    • 权限控制: 沙盒环境通常使用权限控制机制,以确定哪些操作和资源访问是被允许的,哪些是被禁止的。这样,可以根据需要对不同的程序分配不同的权限。

    主要应用领域:

    • Web浏览器:Web浏览器使用沙盒环境来隔离和限制JavaScript代码,以确保恶意网站不能访问用户的敏感信息或操作用户的系统;
    • 移动应用:移动操作系统通常将每个应用程序放置在独立的沙盒中,以确保应用程序之间的隔离和安全性;
    • 操作系统:某些操作系统部分地将运行的程序置于沙盒中,以限制其对核心系统资源的访问,从而增强系统的稳定性和安全性;
    • 沙盒测试:在软件开发中,沙盒环境也用于测试和隔离代码,以确保代码的可靠性和安全性。

    因此,在wxss文件这种不能直接使用本地资源文件,属性background-image​​​只能引用网络上的图片资源。

  3. 解决方法:

    有3种可行的解决方法:

    • 使用内联样式:

      直接将样式写在wxml标签的​**style​​属性**中:

      <view class="fixed-container" 
         style="background-image: url(/src/imgs/topbar/topbar-bc.png);">
      

      这是因为 wxml 文件主要用于构建页面的结构和布局,而这些页面通常需要引用本地资源,如图片、音频、视频等。微信小程序的运行时环境会允许 wxml 文件引用这些本地资源,以便正确显示页面内容;

      这种方法最简单,但是没有遵循代码各司其职的原则,将样式写在了wxml内;同时,可复用性也不强,如果背景图片需要修改还需要重新编写代码、上传图片资源。

    • 通过服务器请求图片资源:

      将图片放在服务器上,wxss属性中配置资源路径,每次加载该页面的时候就向服务器发起请求获取图片:

      .fixed-container{
        background-image: url('https://myresource.com/images/background.jpg');
      }
      

      这种方法的可复用性很高,每次要更换背景图片只需要在服务器上更换即可,不用修改小程序的代码。缺点是需要服务器,会增加HTTP请求次数。

    • 图片转换成Base64格式:

      图片转换为Base64是指将二进制图像数据编码成文本格式。Base64编码是一种将二进制数据转换为ASCII字符集的编码方式,它使用64个不同的字符,通常是字母、数字和一些特殊字符,来表示二进制数据的每6位。

      这是一种将二进制图像数据嵌入文档中的方式,不需要额外的图像文件,从而减少了HTTP请求次数。

      图片转换为base64编码:

      活动报名小程序模板(二)

      将编码嵌入wxss文件:

      .fixed-container{
        background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhE...);
      }
      

      这种方法对大背景图片并不友好,因为转换成base64编码后数据量比原始二进制数据大得多,只适用于小图标、按钮等小图像。

    对比上面几种方法,最适合当前场景需求的是使用内联样式:

    <view class="fixed-container" 
       style="background-image: url(/src/imgs/topbar/topbar-bc.png);">
    

    活动报名小程序模板(二)

2.9 可定位顶部导航栏

  1. 需求分析:

    需要实现一个这样的导航栏:

    活动报名小程序模板(二)

    涉及到两部分的内容:

    • 自定义顶部导航栏;
    • 地图定位实现;
  2. 解决方案:

    • 自定义顶部导航栏:

      • 首先在页面的json​​文件中声明自定义顶部导航栏:

        // pages/home/index.json
        {
          "navigationStyle": "custom"
        }
        
      • 定义顶部导航栏组件:

        由于该组件在目前的需求中,只会在home​​页面中使用,所以我将该组件的文件夹放在了home​​文件夹里,而不是放在根目录下的component​​下:

        活动报名小程序模板(二)

        导航栏的基本尺寸信息随机型不同而不同,不能把数据写死,要适配各种机型。我们可以通过wx.getSystemInfoSync()​​和wx.getMenuButtonBoundingClientRect()​​两个官方接口获取,分别表示状态栏信息和胶囊按钮信息:

        活动报名小程序模板(二)

        通过这些可以获取的信息,我们直到导航栏高度基本组成:导航栏总高度 = 状态栏高度 + 胶囊上下边距 * 2 + 胶囊高度:

        活动报名小程序模板(二)

        因此,可以在app.js​​中获取并配置这些数据,随后在组件中使用:

        // app.js
        App({
          onLaunch() {
            // 获取topbar相关信息
            const that = this;
            // 顶部状态栏的信息
            const systemInfo = wx.getSystemInfoSync();
            // 胶囊按钮位置信息
            const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
            // 导航栏高度 = 状态栏到胶囊的间距(胶囊距上距离-状态栏高度) * 2 + 胶囊高度 + 状态栏高度
            that.globalData.navBarHeight = (menuButtonInfo.top - systemInfo.statusBarHeight) * 2 + menuButtonInfo.height + systemInfo.statusBarHeight;
            that.globalData.menuRight = systemInfo.screenWidth - menuButtonInfo.right;
            that.globalData.menuBottom = menuButtonInfo.top - systemInfo.statusBarHeight;
            that.globalData.menuHeight = menuButtonInfo.height;
            that.globalData.menuWidth = menuButtonInfo.width;
            that.globalData.statusBarHeight = systemInfo.statusBarHeight;
          },
          globalData: {
            userInfo: null,
          
            // 自定义topbar相关信息
            navBarHeight: 0, // 导航栏高度
            statusBarHeight: 0, // 状态栏的高度
            menuRight: 0, // 胶囊距右方间距(保持左、右间距一致)
            menuBottom: 0, // 胶囊距底部间距(保持底部间距一致)
            menuHeight: 0, // 胶囊高度(自定义内容可与胶囊高度保证一致)
            menuWidth: 0, // 胶囊宽度(自定义内容可与胶囊宽度保证一致)
          }
        })
        

        组件中获取手机数据并配置组件宽高:

        // pages/home/components/custom-top-bar/index,js
        const app = getApp();
        Component({
          data: {
            navBarHeight: app.globalData.navBarHeight,
            statusBarHeight: app.globalData.statusBarHeight,
            menuRight: app.globalData.menuRight,
            menuBottom: app.globalData.menuBottom,
            menuHeight: app.globalData.menuHeight,
            menuWidth: app.globalData.menuWidth,
        
            city: '深圳市'
          }
        })
        
        // pages/home/components/custom-top-bar/index,wxml
        <view class="topbar-container" style="height: {{navBarHeight}}px; ">
          <view class="location" style="height: {{navBarHeight - statusBarHeight}}px; margin-top: {{statusBarHeight}}px;">
            <t-icon name="location" size="20px" style="margin-right: 4px;"></t-icon>{{city}}
          </view>
        </view>
        

        这样能够适配机型的自定义顶部导航栏就实现了。

    • 地图定位实现:

      腾讯官方提供了腾讯位置服务的JavaScriptSDK,根据官方文档进行配置即可实现获取当前定位的功能。只不过我使用的是个人开发者身份,请求次数十分有限,所以目前对city​​设置默认字段'深圳市'​​。下面是自定义顶部导航栏组件的完整js文件:

      // components/custom-top-bar/index.js
      const app = getApp();
      // 使用定位服务
      var QQMapWX = require("../../../../lib/qqmap/qqmap-wx-jssdk");
      var qqmapsdk;
      
      Component({
        /**
         * 组件的属性列表
         */
        properties: {
      
        },
      
        /**
         * 组件的初始数据
         */
        data: {
          navBarHeight: app.globalData.navBarHeight,
          statusBarHeight: app.globalData.statusBarHeight,
          menuRight: app.globalData.menuRight,
          menuBottom: app.globalData.menuBottom,
          menuHeight: app.globalData.menuHeight,
          menuWidth: app.globalData.menuWidth,
      
          city: '深圳市',
          title: '主页',
      
          locateKey: 'K3NBZ-O7NK5-4JLIM-IWXVC-MOD67-6PF5G'
        },
      
        /**
         * 组件的方法列表
         */
        methods: {
          getLocation: function () {
            let component = this;
            qqmapsdk.reverseGeocoder({
              success: function(res){
                console.log(res.result.ad_info.city);
                component.setData({
                  city: res.result.ad_info.city
                })
              },
              fail: function(error){
                console.log(error);
              }
            })
          }
        },
        lifetimes: {
          attached: function(){
            qqmapsdk = new QQMapWX({
              key: this.data.locateKey
            });
            this.getLocation();
          }
        }
      })
      

2.10 不同机型的页面适配

  1. 需求描述:

    有一个实现弹窗的需求,不同于弹性较强其他页面比例,弹窗内元素布局较为紧凑,比例控制严格,多一点就不能容纳所有元素。给定的UI设计稿使用的是绝对单位px,比例都是按照iphoneX来的,如果在开发的使用绝对单位,对不同机型的适配性就会非常差:

    活动报名小程序模板(二)

  2. 解决方法:

    采用相对单位vw/vh/rpx​​等,在渲染页面的时候对元素的宽高进行计算。

    vh/vw​​是css3的新单位,是一种视窗单位,小程序中也同样适用:

    • 窗口固定宽度为100vw,将窗口宽度平均分为100份,每一份是1vw;
    • 窗口固定高度为100vh,将窗口高度平均分为100份,每一份是1vh。

    也就是说,如果一个元素的高度是30vh,那其高度就是页面高度的30%。

    设计稿中,弹窗的高度为656px,页面的高度为812px,那么弹窗采用相对单位的高度应该是 656/812 = 80.8 vh;

    活动报名小程序模板(二)

    这样,弹窗即使是在较为矮短的iphone5都能有较好的显示啦:

    活动报名小程序模板(二)

    使用相对单位需要注意的几点:

    • 既然要使用相对单位,就应该将该组件的所有单位都统一成相应的相对单位,包括字体大小,以达到最好的显示效果;

      如果绝对单位和相对单位混杂着用,一方面代码看起来混乱,另一方面容易出现意外的显示效果,不好维护;

    • 使用绝对单位没办法完全还原设计稿的比例,例如上图中按钮的上下间距和左右间距都是12px,那应该用vh还是vw呢?12/812 vh 与 12/375 vw 的是绝对不相等的。那要是都使用vh,对于比较矮短的机型,在宽度上会不会导致按钮间距较大超出弹窗或很丑?这应该是需要和UI协商好怎样解决。

2.11 文档元素重排

  1. 问题描述:

    弹窗有两个显示需求,一是主筛选面板,二是通过主筛选面板的选择日期按钮进入的日期选择面板:

    活动报名小程序模板(二)

    初步实现我是将主筛选的部分和日历两个部分分别封装成组件,通过wx:if = "{{ isChosingDate }}"​​​条件渲染两个组件。但在使用的时候会存在一个bug:当从日历面板退出来的时候,之前选择的面向领域等的tag都归零了。

  2. 问题分析:

    条件渲染的本质是根据指定条件来控制是否在页面上渲染特定的 DOM 元素。当条件满足时,相关的 DOM 元素会被渲染在页面上,而当条件不满足时,相关的 DOM 元素会被从页面上移除或隐藏。条件渲染可以是通过动态地添加或移除 DOM 元素,也可以是通过设置 DOM 元素的样式属性来隐藏或显示它们;

    而微信小程序中的 wx:if​​​ 通常采用的是动态移除或添加 DOM 元素的方式来实现条件渲染。因此,在退出日历面板后,主筛选页面的内容被重新渲染了,按钮的点击状态被清空,回到初始化状态。所以上面这种实现方式是不可行的,在弹出日历面板的同时应该还应保留原来的面板与状态,即用两个弹窗分别实现。

  3. 内容扩展:

    既然条件渲染能够将组件的所有状态和数据回到初始化,那么是否可以开辟一种新用法,用在重置按钮上。通过给主面板添加条件渲染条件wx:if = "{{ reset }}"​​​,重置按钮绑定事件,每次点击重置都将reset​​​的值从1改成2,或者从2改成1,这样当小程序引擎识别到reset​​​的值发生改变时,都会重新解析并渲染筛选面板,但是面板并不会消失,而是回到初始状态。这样就能简易实现重置,而不用通过给按钮组件和面板组件之间传递数据,遍历按钮一个个修改已选择的状态了。

3. 逻辑交互

3.1 数据mock

通常页面中的图片、文本信息等的数据是通过向服务器发起请求来获取的,而在开发过程中,一般会自己写一些模拟数据使用。但这个小程序是通用模板程序,既需要在没有服务器的情况下使用模拟数据,又不能将数据直接写在js文件中写死,要形成能够很容易修改为发起AJAX请求的方法。

下面参照项目TDsign-零售行业模板对获取数据的方法进行封装:

  1. 基本思路:

    • 定义一个配置文件:使用变量useMock​​表示是否要使用模拟数据;

      true​​表示要使用mock的模拟数据;

      false​​表示要使用真实的api获取后端数据;

    • 封装获取数据的方法:根据变量useMock​​的真假,对数据进行返回;

      userMock​​为真,返回已经写好的mock模拟数据,并且数据已经包装成Promise对象;

      useMock​​为假,返回封装成Promise的wx.request​​请求;

  2. 代码实现:

    • 定义配置文件:

      // config/index.js
      export const config = {
        // 是否使用mock数据代替api返回
        useMock: true
      }
      
    • 定义模拟网路延迟的方法:

      因为从发送请求到获取数据是由一定的延迟的,所以可以通过setTimeOut​​方法对数据的返回进行一段时间的延后。因为每一个请求都是需要延迟的,所以可以将这个延迟抽象成一个方法:

      // services/delay.js
      /** 模拟网络请求延时 */
      export default function delay(ms = 500) {
        return new Promise(resolve => {
          setTimeout(resolve, ms);
        })
      }
      
    • 封装获取数据的方法:

      // services/fetchAtvsList.js
      /** 模拟主页的请求 */
      import { config } from '../config/index';
      import delay from './delay';
      
      const atvsList = [{
        ...
      };
      
      // mock活动列表数据
      function mockFetchAtvsList(){
        return delay().then(() => {
          return {
            data: atvsList
          }
        })
      }
      
      // 获取活动的请求
      export function fetchAtvsList() {
        if (config.useMock) {
          return mockFetchAtvsList();
        } else {
          return new Promise((resolve) => {
            // 在这个Promise中用户就可以自定义获取数据的请求
            resolve('real api');
          })
        }
      }