前言
在微信小程序开发中,我们经常会遇到需要实现多个页面之间的切换和导航的需求。为了提高代码的复用性和可维护性,我们可以使用自定义组件来实现tabs功能。本文将介绍如何使用自定义组件tabs实现页面切换,并结合会议管理、投票管理和个人中心三个功能模块进行讲解。
一、自定义组件
从小程序基础库版本 1.6.3 开始,小程序支持简洁的组件化编程。所有自定义组件相关特性都需要基础库版本 1.6.3 或更高。
开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。
1.创建自定义组件
类似于页面,一个自定义组件由 json
wxml
wxss
js
4个文件组成。要编写一个自定义组件,首先需要在 json
文件中进行自定义组件声明(将 component
字段设为 true
可将这一组文件设为自定义组件):
{ "component": true }
同时,还要在 wxml
文件中编写组件模板,在 wxss
文件中加入组件样式,它们的写法与页面的写法类似。具体细节和注意事项参见 组件模板和样式 。
代码示例:
<!-- 这是自定义组件的内部WXML结构 --> <view class="inner"> {{innerText}} </view> <slot></slot>
/* 这里的样式只应用于这个自定义组件 */ .inner { color: red; }
注意:在组件wxss中不应使用ID选择器、属性选择器和标签名选择器。
在自定义组件的 js
文件中,需要使用 Component()
来注册组件,并提供组件的属性定义、内部数据和自定义方法。
组件的属性值和内部数据将被用于组件 wxml
的渲染,其中,属性值是可由组件外部传入的。更多细节参见 Component构造器 。
代码示例:
Component({ properties: { // 这里定义了innerText属性,属性值可以在组件使用时指定 innerText: { type: String, value: 'default value', } }, data: { // 这里是一些组件内部数据 someData: {} }, methods: { // 这里是一个自定义方法 customMethod: function(){} } })
2.使用自定义组件
使用已注册的自定义组件前,首先要在页面的 json
文件中进行引用声明。此时需要提供每个自定义组件的标签名和对应的自定义组件文件路径:
{ "usingComponents": { "component-tag-name": "path/to/the/custom/component" } }
这样,在页面的 wxml
中就可以像使用基础组件一样使用自定义组件。节点名即自定义组件的标签名,节点属性即传递给组件的属性值。
开发者工具 1.02.1810190 及以上版本支持在 app.json 中声明 usingComponents 字段,在此处声明的自定义组件视为全局自定义组件,在小程序内的页面或自定义组件中可以直接使用而无需再声明。
代码示例:
<view> <!-- 以下是对一个自定义组件的引用 --> <component-tag-name inner-text="Some text"></component-tag-name> </view>
自定义组件的 wxml
节点结构在与数据结合之后,将被插入到引用位置内。
3.注意事项
一些需要注意的细节:
- 因为 WXML 节点标签名只能是小写字母、中划线和下划线的组合,所以自定义组件的标签名也只能包含这些字符。
- 自定义组件也是可以引用自定义组件的,引用方法类似于页面引用自定义组件的方式(使用
usingComponents
字段)。- 自定义组件和页面所在项目根目录名不能以“wx-”为前缀,否则会报错。
注意,是否在页面文件中使用
usingComponents
会使得页面的this
对象的原型稍有差异,包括:
- 使用
usingComponents
页面的原型与不使用时不一致,即Object.getPrototypeOf(this)
结果不同。- 使用
usingComponents
时会多一些方法,如selectComponent
。- 出于性能考虑,使用
usingComponents
时,setData
内容不会被直接深复制,即this.setData({ field: obj })
后this.data.field === obj
。(深复制会在这个值被组件间传递时发生。)
如果页面比较复杂,新增或删除 usingComponents
定义段时建议重新测试一下。
二、自定义组件tabs案例
1.创建自定义组件
新建一个components文件夹 --> tabs文件夹 --> tabs文件
注意:
创建好之后,win7以上系统可能会出现以下错误
解决方法:
在project.config.json文件里添加两行配置
"ignoreDevUnusedFiles": false, "ignoreUploadUnusedFiles": false,
2.文件内容的编写
tabs.json
{ "component": true, "usingComponents": {} }
tabs.wxml
<!--components/tabs/tabs.wxml--> <view class="tabs"> <view class="tabs_title"> <view wx:for="{{tabList}}" wx:key="id" class="title_item {{index==tabIndex?'item_active':''}}" bindtap="handleItemTap" data-index="{{index}}"> <view style="margin-bottom:5rpx">{{item}}</view> <view style="width:30px" class="{{index==tabIndex?'item_active1':''}}"></view> </view> </view> <view class="tabs_content"> <slot></slot> </view> </view>
tabs.wxss
/* components/tabs/tabs.wxss */ .tabs { position: fixed; top: 0; width: 100%; background-color: #fff; z-index: 99; border-bottom: 1px solid #efefef; padding-bottom: 20rpx; } .tabs_title { width: 400rpx; width: 90%; display: flex; font-size: 9pt; padding: 0 20rpx; } .title_item { color: #999; padding: 15rpx 0; display: flex; flex: 1; flex-flow: column nowrap; justify-content: center; align-items: center; } .item_active { /* color:#ED8137; */ color: #000000; font-size: 11pt; font-weight: 800; } .item_active1 { /* color:#ED8137; */ color: #000000; font-size: 11pt; font-weight: 800; border-bottom: 6rpx solid #333; border-radius: 2px; }
tabs.js
// components/tabs/tabs.js Component({ /** * 组件的属性列表 */ properties: { tabList:Object }, /** * 组件的初始数据 */ data: { tabIndex:0 }, /** * 组件的方法列表 */ methods: { handleItemTap(e){ // debugger // 获取索引 const {index} = e.currentTarget.dataset; // 触发 // this.triggerEvent("tabsItemChange",{index}) this.triggerEvent("tabsItemChange",{index:index}) this.setData({ tabIndex:index }) } } })
三、自定义组件tabs的使用和会议管理页面的搭建
1.使用tabs组件
在需要使用自定义组件的json中添加配置
"usingComponents": { "tabs": "/components/tabs/tabs" }
2.在页面使用组件并添加事件
//wxml <tabs tabList="{{tabs}}" bindtabsItemChange="tabsItemChange"> </tabs>
js中定义变量和方法
tabs:['会议中','已完成','已取消','全部会议'] tabsItemChange(e){ // debugger let tolists; if(e.detail.index==1){ tolists = this.data.lists1; }else if(e.detail.index==2){ tolists = this.data.lists2; }else{ tolists = this.data.lists3; } this.setData({ lists: tolists }) }
3.页面搭建
页面搭建内容wxml
<!--pages/meeting/list/list.wxml--> <!-- <text>pages/meeting/list/list.wxml</text> --> <tabs tabList="{{tabs}}" bindtabsItemChange="tabsItemChange"> </tabs> <view style="height: 100rpx;"></view> <block wx:for-items="{{lists}}" wx:for-item="item" wx:key="item.id"> <view class="list" data-id="{{item.id}}"> <view class="list-img al-center"> <image class="video-img" mode="scaleToFill" src="{{item.image}}"></image> </view> <view class="list-detail"> <view class="list-title"><text>{{item.title}}</text></view> <view class="list-tag"> <view class="state al-center">{{item.state}}</view> <view class="join al-center"><text class="list-num">{{item.num}}</text>人报名</view> </view> <view class="list-info"><text>{{item.address}}</text>|<text>{{item.time}}</text></view> </view> </view> </block>
样式设计wxss
.list{ background-color: #fff; display: flex; margin: 10rpx; padding: 10rpx; } .list-img,.video-img{ height: 150rpx; width: 150rpx; } .list-img{ margin: 20rpx 0 0 0; } .list-detail{ margin: 0 0 0 15rpx; } .list-title{ font-weight: 700; } .list-tag{ display: flex; margin: 10px 0; } .state{ border: 2px solid lightskyblue; padding: 2px; color: lightskyblue; } .join{ border: 2px solid #fff; padding: 2px; margin: 0 0 0 20rpx; color: gray; } .list-num{ color: red; } .list-info{ color: gray; } .bottom-line{ text-align: center; margin-bottom: 10px; }
模拟数据进行页面展示js
// pages/meeting/list/list.js Page({ /** * 页面的初始数据 */ data: { tabs:['会议中','已完成','已取消','全部会议'] ,lists: [ { 'id': '1', 'image': '/static/persons/1.jpg', 'title': '对话产品总监 | 深圳·北京PM大会 【深度对话小米/京东/等产品总监】', 'num':'304', 'state':'进行中', 'time': '10月09日 17:59', 'address': '深圳市·南山区' }, { 'id': '1', 'image': '/static/persons/2.jpg', 'title': 'AI WORLD 2016世界人工智能大会', 'num':'380', 'state':'已结束', 'time': '10月09日 17:39', 'address': '北京市·朝阳区' }, { 'id': '1', 'image': '/static/persons/3.jpg', 'title': 'H100太空商业大会', 'num':'500', 'state':'进行中', 'time': '10月09日 17:31', 'address': '大连市' }, { 'id': '1', 'image': '/static/persons/4.jpg', 'title': '报名年度盛事,大咖云集!2016凤凰国际论坛邀您“与世界对话”', 'num':'150', 'state':'已结束', 'time': '10月09日 17:21', 'address': '北京市·朝阳区' }, { 'id': '1', 'image': '/static/persons/5.jpg', 'title': '新质生活 · 品质时代 2016消费升级创新大会', 'num':'217', 'state':'进行中', 'time': '10月09日 16:59', 'address': '北京市·朝阳区' } ], lists1: [ { 'id': '1', 'image': '/static/persons/1.jpg', 'title': '对话产品总监 | 深圳·北京PM大会 【深度对话小米/京东/等产品总监】', 'num':'304', 'state':'进行中', 'time': '10月09日 17:59', 'address': '深圳市·南山区' }, { 'id': '1', 'image': '/static/persons/2.jpg', 'title': 'AI WORLD 2016世界人工智能大会', 'num':'380', 'state':'已结束', 'time': '10月09日 17:39', 'address': '北京市·朝阳区' }, { 'id': '1', 'image': '/static/persons/3.jpg', 'title': 'H100太空商业大会', 'num':'500', 'state':'进行中', 'time': '10月09日 17:31', 'address': '大连市' } ], lists2: [ { 'id': '1', 'image': '/static/persons/1.jpg', 'title': '对话产品总监 | 深圳·北京PM大会 【深度对话小米/京东/等产品总监】', 'num':'304', 'state':'进行中', 'time': '10月09日 17:59', 'address': '深圳市·南山区' }, { 'id': '1', 'image': '/static/persons/2.jpg', 'title': 'AI WORLD 2016世界人工智能大会', 'num':'380', 'state':'已结束', 'time': '10月09日 17:39', 'address': '北京市·朝阳区' } ], lists3: [ { 'id': '1', 'image': '/static/persons/1.jpg', 'title': '对话产品总监 | 深圳·北京PM大会 【深度对话小米/京东/等产品总监】', 'num':'304', 'state':'进行中', 'time': '10月09日 17:59', 'address': '深圳市·南山区' }, { 'id': '1', 'image': '/static/persons/2.jpg', 'title': 'AI WORLD 2016世界人工智能大会', 'num':'380', 'state':'已结束', 'time': '10月09日 17:39', 'address': '北京市·朝阳区' }, { 'id': '1', 'image': '/static/persons/3.jpg', 'title': 'H100太空商业大会', 'num':'500', 'state':'进行中', 'time': '10月09日 17:31', 'address': '大连市' }, { 'id': '1', 'image': '/static/persons/4.jpg', 'title': '报名年度盛事,大咖云集!2016凤凰国际论坛邀您“与世界对话”', 'num':'150', 'state':'已结束', 'time': '10月09日 17:21', 'address': '北京市·朝阳区' }, { 'id': '1', 'image': '/static/persons/5.jpg', 'title': '新质生活 · 品质时代 2016消费升级创新大会', 'num':'217', 'state':'进行中', 'time': '10月09日 16:59', 'address': '北京市·朝阳区' } ] }, /** * 生命周期函数--监听页面加载 */ onLoad(options) { }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady() { }, /** * 生命周期函数--监听页面显示 */ onShow() { }, /** * 生命周期函数--监听页面隐藏 */ onHide() { }, /** * 生命周期函数--监听页面卸载 */ onUnload() { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh() { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom() { }, /** * 用户点击右上角分享 */ onShareAppMessage() { }, tabsItemChange(e){ // debugger let tolists; if(e.detail.index==1){ tolists = this.data.lists1; }else if(e.detail.index==2){ tolists = this.data.lists2; }else{ tolists = this.data.lists3; } this.setData({ lists: tolists }) } })
效果演示
四、投票管理页面搭建
这里我们也用到了tabs组件,这里就不过多介绍了
在会议管理中我们是直接通过tabs组件中的内容进行点击获取查询到相应的信息,那我们又该如何去搭建每一个页面呢。
我们就可以通过绑定变量的关系去设置内容
<view wx:if="{{activeTab === 0}}" style="margin-top: 50px;"> <!-- 发起投票的内容 --> </view> <view wx:if="{{activeTab === 1}}" style="margin-top: 50px;"> <!-- 我的投票的内容 --> </view> <view wx:if="{{activeTab === 2}}" style="margin-top: 50px;"> <!-- 所有投票的内容 --> </view> <view wx:if="{{activeTab === 3}}" style="margin-top: 50px;"> <!-- 投票报表的内容 --> </view>
然后在js定义该变量
activeTab: 0, // 默认显示第一个tab的内容 tabsItemChange(e){ let index = e.detail.index; this.setData({ activeTab: index }); },
页面搭建wxml
<!--pages/vote/list/list.wxml--> <!-- <text>pages/vote/list/list.wxml</text> --> <tabs tabList="{{tabs}}" bindtabsItemChange="tabsItemChange"> </tabs> <view wx:if="{{activeTab === 0}}" style="margin-top: 50px;"> <!-- 发起投票的内容 --> <view class="container"> <view class="title">投票名称</view> <input class="input" placeholder="请输入投票名称" bindinput="onNameInput"/> <view class="title">投票描述</view> <textarea class="textarea" placeholder="请输入投票描述" bindinput="onDescInput"></textarea> <view class="title">投票选项</view> <view class="options"> <view wx:for="{{options}}" wx:key="{{index}}"> <input class="option-input" placeholder="请输入选项" value="{{item}}" bindinput="onOptionInput"/> <button class="option-delete" wx:if="{{options.length > 2}}" data-index="{{index}}" bindtap="onOptionDelete">删除</button> </view> <button class="option-add" bindtap="onOptionAdd">添加选项</button> </view> <view class="title">投票时间</view> <view class="picker-put"> <picker mode="time" value="{{startTime}}" bindchange="onStartTimeChange"> <view class="picker1">{{startTime}}</view> </picker> <text class="tex">至</text> <picker mode="time" value="{{endTime}}" bindchange="onEndTimeChange"> <view class="picker2">{{endTime}}</view> </picker> </view> <view class="title">是否多选</view> <checkbox-group bindchange="onMultiSelectChange"> <label class="checkbox-label"> <checkbox value="true" checked="{{multiSelect}}"/> <text>开启</text> </label> <label class="checkbox-label"> <checkbox value="false" checked="{{!multiSelect}}"/> <text>关闭</text> </label> </checkbox-group> <view class="title">投票前显示投票纪录</view> <checkbox-group bindchange="onShowResultChange"> <label class="checkbox-label"> <checkbox value="true" checked="{{showResult}}"/> <text>开启</text> </label> <label class="checkbox-label"> <checkbox value="false" checked="{{!showResult}}"/> <text>关闭</text> </label> </checkbox-group> <view class="title">匿名投票</view> <checkbox-group bindchange="onAnonymousChange"> <label class="checkbox-label"> <checkbox value="true" checked="{{anonymous}}"/> <text>开启</text> </label> <label class="checkbox-label"> <checkbox value="false" checked="{{!anonymous}}"/> <text>关闭</text> </label> </checkbox-group> <button class="submit" bindtap="onSubmit">提交</button> </view> </view> <view wx:if="{{activeTab === 2}}" style="margin-top: 50px;"> <!-- 所有投票的内容 --> <text>这里是所有投票的内容</text> </view> <view wx:if="{{activeTab === 3}}" style="margin-top: 50px;"> <!-- 投票报表的内容 --> <text>这里是投票报表的内容</text> </view>
样式设计wxss
/* pages/vote/list/list.wxss */ /* vote.wxss文件内容 */ .container { padding: 20rpx; } .title { font-weight: 700; font-size: 40rpx; margin-top: 20rpx; } .input { border: 1rpx solid #ccc; border-radius: 10rpx; padding: 20rpx; margin-top: 10rpx; font-size: 28rpx; padding-bottom: 10px; } .textarea { border: 1rpx solid #ccc; border-radius: 10rpx; padding: 10rpx; margin-top: 10rpx; font-size: 28rpx; } .options { margin-top: 10rpx; } .option-input { border: 1rpx solid #ccc; border-radius: 10rpx; padding: 20rpx; margin-top: 10rpx; font-size: 28rpx; width: 80%; } .option-delete { width: 20rpx; height: 70rpx; background-color: #f00; color: #fff; border-radius: 10rpx; padding: 10rpx; margin: 10rpx; } .option-add { background-color: #09f; color: #fff; border-radius: 10rpx; padding: 10rpx; margin-top: 10rpx; } .picker1,.picker2 { border: 1rpx solid #ccc; border-radius: 10rpx; padding: 20rpx; margin-top: 10rpx; font-size: 28rpx; text-align: center; width: 100%; } .picker1{ width: 20%; padding-right: 50px; } .picker2{ width: 30%; padding-left: 40px; padding-right: 10px; padding-bottom: 10px; } .tex{ padding: 0,5px,0,5px; } .picker-put{ display: flex; align-items: center; } .checkbox-label { padding-bottom: 10px; display: inline-flex; align-items: center; margin-right: 30rpx; } .submit { background-color: #09f; color: #fff; border-radius: 10rpx; padding: 20rpx; margin-top: 30rpx; font-size: 32rpx; width: 100%; }
数据引入js
// pages/vote/list/list.js Page({ /** * 页面的初始数据 */ data: { tabs:['发起投票','我的投票','所有投票','投票报表'], activeTab: 0, // 默认显示第一个tab的内容 name: '', desc: '', options: ['', ''], startTime: '00:00', endTime: '23:59', multiSelect: false, showResult: true, anonymous: false }, onNameInput(event) { this.setData({ name: event.detail.value }) }, onDescInput(event) { this.setData({ desc: event.detail.value }) }, onOptionInput(event) { const index = event.currentTarget.dataset.index const value = event.detail.value const options = this.data.options options[index] = value this.setData({ options: options }) }, onOptionAdd() { const options = this.data.options options.push('') this.setData({ options: options }) }, onOptionDelete(event) { const index = event.currentTarget.dataset.index const options = this.data.options options.splice(index, 1) this.setData({ options: options }) }, onStartTimeChange(event) { this.setData({ startTime: event.detail.value }) }, onEndTimeChange(event) { this.setData({ endTime: event.detail.value }) }, onMultiSelectChange(event) { this.setData({ multiSelect: event.detail.value[0] === 'true' }) }, onShowResultChange(event) { this.setData({ showResult: event.detail.value[0] === 'true' }) }, onAnonymousChange(event) { this.setData({ anonymous: event.detail.value[0] === 'true' }) }, onSubmit() { // TODO: 提交表单 }, tabsItemChange(e){ let index = e.detail.index; this.setData({ activeTab: index }); }, /** * 生命周期函数--监听页面加载 */ onLoad(options){ }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady() { }, /** * 生命周期函数--监听页面显示 */ onShow() { }, /** * 生命周期函数--监听页面隐藏 */ onHide() { }, /** * 生命周期函数--监听页面卸载 */ onUnload() { }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh() { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom() { }, /** * 用户点击右上角分享 */ onShareAppMessage() { } })
效果演示
五、个人中心页面搭建
wxml页面搭建
<!--pages/ucenter/index/index.wxml--> <!-- <text>pages/ucenter/index/index.wxml</text> --> <view class="userInfo"> <image class="user-head" src="/static/persons/8.jpg"></image> <text class="user-name">孤留光乩</text> <text class="user-edit">修改</text> </view> <view class="list"> <view class="list-item"> <image class="item-icon" src="/static/tabBar/sdk.png"></image> <text class="item-title">我主持的会议</text> <text space="nbsp" class="item-num">1 </text> <text class="item-detail">></text> </view> <view class="hr"></view> <view class="list-item"> <image class="item-icon" src="/static/tabBar/sdk.png"></image> <text class="item-title">我参与的会议</text> <text class="item-num">10</text> <text class="item-detail">></text> </view> </view> <view class="list"> <view class="list-item"> <image class="item-icon" src="/static/tabBar/sdk.png"></image> <text class="item-title">我发布的投票</text> <text space="nbsp" class="item-num">1 </text> <text class="item-detail">></text> </view> <view class="hr"></view> <view class="list-item"> <image class="item-icon" src="/static/tabBar/sdk.png"></image> <text class="item-title">我参与的投票</text> <text class="item-num">10</text> <text class="item-detail">></text> </view> </view> <view class="list"> <view class="list-item"> <image class="item-icon" src="/static/tabBar/sdk.png"></image> <text class="item-title">消息</text> <text class="item-num"></text> <text space="nbsp" class="item-detail"> ></text> </view> <view class="hr"></view> <view class="list-item"> <image class="item-icon" src="/static/tabBar/sdk.png"></image> <text class="item-title">设置</text> <text space="emsp" class="item-num"></text> <text space="nbsp" class="item-detail"> ></text> </view> </view>
样式设计wxss
/* pages/ucenter/index/index.wxss */ Page{ background-color: lightgray; } .userInfo{ display: flex; background-color: #fff; /* border: 1px solid red; */ padding: 20rpx; } .user-head{ width: 150rpx; height: 150rpx; } .user-name,.user-edit{ display: flex; align-items: center; margin: 0 0 0 20rpx; } .user-name{ /* display: inline-block; */ width: 450rpx; font-weight: 700; } .user-edit{ color: gray; } .list{ height: 280rpx; width: 750rpx; display: flex; flex-direction: column; } .list-item{ /* height: 130rpx; */ } .item-icon{ height: 60rpx; width: 60rpx; margin-top: 20px; /* border: 1px solid red; */ } .item-title,.item-num,.item-detail{ /* border: 1px solid red; */ position: relative; top:-10px; display: inline-block; } .item-title{ font-size: 18px; width: 520rpx; height: 25px; margin-left: 10px; } .item-num{ margin-right: 10px; } .item-detail{ color: gray; } .list .hr{ background-color: lightgray; height: 1px; width: 400px; display: inline-block; }
效果演示