图书管理小程序
一、页面逻辑
1. 动态切换tabbar
问题场景
同一个小程序存在普通用户和管理员两种身份,两种不同的身份需要展示不同的tabbar
解决方法
方法一 - 自定义tabbar
实现过程
- 根据官方提供的自定义tabbar方法
- 见 参考方案 | 自定义动态tabbar 和 微信官方文档 | 自定义tabbar
优劣分析
-
适用于需要的tab页不超过5个的需求
-
容易对tab本身的进行点击事件绑定及其响应,简洁轻便
-
当需求的tab页面超过五个时,需要对页面重复利用
如,用户和管理员都有“我的”页面,可以在该tab页的onLoad生命周期函数中添加身份判断函数,从而渲染不同内容,实现一页多用
但是,上述方法局限于页面渲染内容框架大致相同,差距不会太大的情况
当页面内容差距很大,不能实现一页多用的时候,自定义tabbar的方法不适用
方法二 - 自定义组件
实现过程
- 不使用官方提供的tabbar,因此将app.json中关于tabbar的部分删除
- 将tabbar定义为组件的形式,在每一个页面中使用这个组件
代码实现
- 在pages文件夹同级路径下创建components文件夹,存放自定义tabbar组件文件
注:共有六个主页面需要放在tabbar中,用户和管理员各三个
此处我建立了六个组件,每个页面一个
Q:为什么不将用户的三个合并成一个,管理员的三个合并成一个?
A:因为要实现点击tabbar图标颜色改变的效果,需要对自定义组件文件进行修改;
如果icon图标是以<image src="../../a.jpg">形式写入wxml文件,则需要替换组件wxml文件中的图片,即修改image组件的路径; 如果icon图标是以<text class="iconfont">写入wxml文件,则需要通过修改组件wxss文件的样式,即修改颜色属性; 两种方式都需要对组件的文件本身进行修改,不能绑定点击事件通过.js文件进行响应。 暂时没有想到更好的解决方案。
- 编写自定义组件文件
//components/Uhome/Uhome.json
{
"component": true,
"usingComponents": {}
}
<!--components/Uhome/Uhome.wxml-->
<view class="wrapper">
<view class="home">
<image src="/icon/home-active.png"></image>
<view class="title">首页</view>
</view>
<view class="search" bindtap="gotoSearch">
<image src="/icon/search.png"></image>
<view class="title">找书</view>
</view>
<view class="mine" bindtap="gotoMine">
<image src="/icon/mine.png"></image>
<view class="title">我的</view>
</view>
</view>
// components/Uhome/Uhome.js
Component({
/**
* 组件的属性列表
*/
properties: {
},
/**
* 页面的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
gotoSearch(){
wx.redirectTo({
url: '/pages/search/search',//页面跳转的事件响应
})
},
gotoMine(){
wx.redirectTo({
url: '/pages/mine/mine',
})
}
}
});
/* components/Uhome/Uhome.wxss */
.wrapper{
height: 100rpx;
width: 100%;
background-color: #f8f8f8;
border-top: #e9e9e9 1rpx solid;
display: flex;
flex-direction: row;
justify-content: space-around;
position: fixed;
bottom: 0rpx;
}
.home,
.search,
.mine{
height: 100%;
width: 100rpx;
text-align: center;
font-size: 28rpx;
}
image{
margin-top: 5rpx;
height: 50rpx;
width: 50rpx;
}
- 在主页面中使用组件:
{
"usingComponents": {
"Uhome": "/components/Uhome/Uhome"
},
}
<!--扫码借书-->
<view class="text">扫码借书</view>
<view class="scan" bindtap="scanCode" data-id="borrow">
<text class="iconfont icon-saomiao"></text>
点击图标扫一扫
</view>
<!--扫码还书-->
<view class="text">扫码还书</view>
<view class="scan" bindtap="scanCode" data-id="return">
<text class="iconfont icon-saomiao"></text>
点击图标扫一扫
</view>
<!--底部导航栏-->
<Uhome></Uhome>
主页面中无需再对自定义组件的样式进行修改
若主页面是可滚动的长页面,滚动时需要保证tabbar不动,可以通过设置position: fixed实现
效果展示
- 用户
- 管理员
优劣分析
- 适用于需要多余五个tab页的需求,暴力,简单
- 真机效果里,页面切换的时候会出现tabbar闪烁,暂未找到解决方法
- 当页面元素刚好撑满整个页面不至于上下滚动时,自定义组件的yabbar是会挡住页面元素的,自行给页面增添隐藏的滚动条,这也是缺陷之一
2. 真机测试弹窗一闪而过
问题场景一
需求:在登录页面登录成功后,进入主页面,同时弹出弹窗显示登录成功
问题:登录成功弹窗一闪而过,不会在新页面中出现
解决方法
延后页面跳转操作的执行,使其在弹窗消失后执行
wx.showToast({
title: '登录成功',
icon: "none",
duration: 1000,
})
setTimeout(()=> {
wx.navigateTo({
url: '',
})
}, 1000)
问题场景二
需求:用户在输入框输入关键词查找书籍,点击搜索按钮后弹出加载弹窗显示“正在搜索”,若没有搜索到相关书籍则弹出提示弹窗显示“未找到相关书籍”
问题:提示弹窗在真机上一闪而过
分析:小程序处理wx.showToast和wx.showLoading用的是同一个框,当wx.showToast和wx.showLoading同时被写入全局作用域(即不在延时函数等内)时,两者都被在调用栈中的wx.hideLoading关闭了
如原顺序为 wx.showLoading->wx.hideLoading->wx.showToast,但实际是wx.showLoading->wx.showToast->wx.hideLoading
解决方法
使用延时函数,将wx.showToast放入任务队列中
wx.showLoading();
wx.hideLoading();
setTimeout( () => {
wx.showToast({
title:'未找到该书籍',
icon: "none",
});
setTimeout( () =>{
wx.hideToast();
},2000)
},0);
3. 页面跳转方式及其区别
跳转到tabbar页
跳转到tab页,并关闭其他所有非tabbar页
wx.switchTab({
url: ''
})
跳转到其他页面
方式
wx.navigateTo({
url: ''
})
wx.redirectTo({
url: ''
})
wx.reLaunch({
url: ''
})
区别
页面关闭
-
wx.navigateTo不会关闭当前页面,只是跳转到目标页面
-
wx.redirectTo会关闭当前页面,无法通过wx.navigateBack返回
-
wx.reLauch会关闭所有页面
页面样式
-
wx.navigateTo的目标页左上角会有一个返回键,可以返回上一页
暂时没找到方法隐藏该按钮
-
wx.redirectTo的目标页左上角会有一个主页键,可以返回主页
隐藏此处主页按钮的方法
//目标页的生命周期函数
onShow() {
wx.hideHomeButton();
},
4. 页面间数据传递与共享
问题场景
主页面有四个图书分类按钮,点击按钮可以分别跳转到四类书籍列表页
四个按钮分别有四个id,根据id不同书籍列表页请求不同书籍数据
现需要将按钮的id从主页面传递到书籍列表页
解决方法
方法一 - 数据缓存本地
见官方文档 微信官方文档 | wx.setStorage 和 微信官方文档 | wx.getStorageSync
使用举例可以见下一节点 请求头token的携带
方法二 - 全局变量
- app.js中定义全局变量
//app.js
App({
onLaunch: function () {
},
globalData: {
id: ''
}
})
- 在主页面设置id的值
data-* 可以传递数据给bindtap的函数
<!--pages/home/home.wxml-->
<view bindtap="goTo" data-id="1">分类一</view>
获取数据并设置全局变量中的id
//pages/home/home.js
var app = getApp();//获取全局变量中的数据
Page({
goTo(e){
console.log(e);//从控制台打印的信息可见传递来的id在数据中的位置
app.globalData.id = e.currentTarget.dataset.id;//设置全局数据中的id
wx.navigateTo({
url: '/pages/booklist/booklist',
})
},
})
- 在图书列表页获取数据
//pages/booklist/booklist.js
var app = getApp();
Page({
data: {
categoryId: app.globalData.id
}
})
方法三 - store数据共享
需要装mobx包,版本更新使用方法不同,可下载官网使用样例查看使用方法
当前使用方法适用版本为mobx-miniprogram 4.13.2 和 mobx-miniprogrm-bindings 1.2.1
- 在pages同级路径下创建store文件夹,创建store文件
//store/store.js
import { observable, action } from 'mobx-miniprogram'
export const store = observable({
//数据 - 分类id
categoryId: '',
//方法 - 设置id
updateCategoryId: action(function(id){
this.categoryId = id;
}),
//方法 - 返回id
getCategoryId: action(function(){
return this.categoryId;
}),
})
- 主页获取id
<!--pages/home/home.wxml-->
<view bindtap="goTo" data-id="1">分类一</view>
// pages/search/search.js
import { createStoreBindings, storeBindingsDestroy } from 'mobx-miniprogram-bindings'
import { store } from '../../store/store'
goTo(e){
console.log(e);
this.updateCategoryId(e.currentTarget.dataset.id);
wx.navigateTo({
url: '/pages/booklist/booklist',
})
},
- 图书列表页获取数据
// pages/booklist/booklist.js
import { createStoreBindings, destroyStoreBindings } from 'mobx-miniprogram-bindings'
import { store } from '../../store/store'
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.storeBindings = createStoreBindings(this, {
store,
fields: [ 'categoryId'],
actions: [ 'getCategoryId' ]
});
},
getbooklist(){
let id = this.getCategoryId();//获取数据
wx.request({
url: '',
data: {
categoryId: id
},
header: header
}
5. picker选择器
问题场景
用户填写信息时,需要在给定的一些数据中选择一个数据,页面实现一个可选框供用户选择
实现效果
代码实现
直接看官方 微信官方文档 | picker选择器
6. 自定义下拉框组件
问题场景
小程序没有下拉框的组件,需要自己自定义组件
实现效果
代码实现
- 在components文件夹中创建自定义组件文件
- 编写自定义组件文件
{
"component": true,
"usingComponents": {}
}
<!--components/select/select.wxml-->
<view class="select-box">
<view class="select-current" catchtap="openClose">
<text class="current-name">{{current.name}}</text>
</view>
<view class="option-list" wx:if="{{isShow}}" catchtap="optionTap">
<text class="option"
data-id="{{defaultOption.id}}"
data-name="{{defaultOption.name}}">{{defaultOption.name}}
</text>
<text class="option"
wx:for="{{options}}"
wx:key="{{item.id}}"
data-id="{{item.id}}"
data-name="{{item.name}}">{{item.name}}
</text>
</view>
</view>
/* components/select/select.wxss */
.select-box {
position: relative;
width: 120rpx;
font-size: 20rpx;
border-radius: 8rpx;
}
.select-current {
position: relative;
width: 100%;
padding: 0 10rpx;
line-height: 70rpx;
border: 1rpx solid #ddd;
border-radius: 6rpx;
box-sizing: border-box;
}
/* 下拉角标 */
.select-current::after {
position: absolute;
display: block;
right: 16rpx;
top: 30rpx;
content: '';
width: 0;
height: 0;
border: 10rpx solid transparent;
border-top: 10rpx solid #999;
}
.current-name {
display: block;
width: 85%;
height: 100%;
word-wrap: normal;
overflow: hidden;
}
.option-list {
position: absolute;
left: 0;
top: 70rpx;
width: 100%;
padding: 12rpx 20rpx 10rpx 20rpx;
border-radius: 0rpx;
box-sizing: border-box;
z-index: 99;
box-shadow: 0rpx 0rpx 1rpx 1rpx rgba(0, 0, 0, 0.2) inset;
background-color: #fff;
}
.option {
display: block;
width: 100%;
line-height: 50rpx;
border-bottom: 1rpx solid #eee;
}
.option:last-child {
border-bottom: none;
padding-bottom: 0;
}
// components/select/select.js
Component({
properties: {
options: {
type: Array,
value: []
},
defaultOption: {
type: Object,
value: {
id: '0',
name: '任意词'
}
},
key: {
type: String,
value: 'id'
},
text: {
type: String,
value: 'name'
}
},
data: {
result: [],
isShow: false,
current: {}
},
methods: {
optionTap(e) {
let dataset = e.target.dataset;
this.setData({
current: dataset,
isShow: false
});
// 调用父组件方法,并传参
this.triggerEvent("change", { ...dataset})
},
openClose() {
this.setData({
isShow: !this.data.isShow
})
},
// 此方法供父组件调用
close() {
this.setData({
isShow: false
})
}
},
lifetimes: {
attached() {
// 属性名称转换, 如果不是 { id: '', name:'' } 格式,则转为 { id: '', name:'' } 格式
let result = []
if (this.data.key !== 'id' || this.data.text !== 'name') {
for (let item of this.data.options) {
let { [this.data.key]: id, [this.data.text]: name } = item
result.push({ id, name })
}
}
this.setData({
current: Object.assign({}, this.data.defaultOption),
result: result
})
}
}
})
- 页面中使用自定义组件
//pages/index/index.json
{
"usingComponents": {
"select": "/components/select/select"
}
}
<!--pages/AbookStatus/AbookStatus.wxml-->
<select id="select" options="{{options}}" key="id" text="name" bind:change="change"></select>
// pages/AbookStatus/AbookStatus.js
Page({
/**
* 页面的初始数据
*/
data: {
options: [
{
id: '1',
name: '名次'
},
{
id: '2',
name: '作者'
}],
selected: {},
},
/* 下拉框组件的方法 */
change (e) {
this.setData({
selected: { ...e.detail }
})
},
close () {
// 关闭select
this.selectComponent('#select').close()
},
7. 表单提交
问题场景
用户提出建议,提交数据给小程序,要求实现页面到请求的数据传递,通过表单实现
代码实现
<!--pages/advice/advice.wxml-->
<form bindsubmit="formSubmit">
<!--建议文本框-->
<textarea name="advice" class="textbox" placeholder="编辑建议草稿" maxlength="-1" placeholder-style="color:#000;"></textarea>
<!--建议人信息-->
<input name="username" type="text" value="" placeholder='提出人' class="first" placeholder-style="color:#000;"/>
<input name="contact" type="text" value="" placeholder='联系电话' placeholder-style="color:#000;"/>
<!--提交按钮-->
<button id="submitBtn" formType="submit">提交</button>
</form>
// pages/advice/advice.js
Page({
/**
* 页面的初始数据
*/
data: {
content: '无',
username: '',
contact: '',
}
//捕获表单数据的函数
formSubmit(e){
//从打印的数据中可找到需要的数据
console.log(e);
this.setData({
content: e.detail.value.advice,
username: e.detail.value.username,
contact: e.detail.value.contact
});
wx.request({
method: 'POST',
url: '',
data: {
content: this.data.content,
username: this.data.username,
contact: this.data.contact
},
header: header,
success: res=>{
console.log(res);
}
})
},
二、前后交互
1. 请求头携带token方法封装
问题场景
登录账号后后端会自动分配给用户一个字符串令牌token,当用户需要进行请求数据等操作时,需要在请求头携带该token用于身份验证
解决方法
由于小程序中多处地方需要用到请求头携带token的操作,可以将创建请求头的操作封装成一个模块函数
代码实现
- 登录页面将后台返回的token本地缓存
//pages/login/login.js
//登录请求成功后的回调函数中
wx.setStorage({
key: 'token',
data: res.data.token//登录成功后后台返回的token
});
- 与pages同级的目录下创建fuctions文件夹,在其中创建functions.js文件,编写创建请求头的函数
//functions/functions.js
//创建携带token的请求头
function createHeader(type){//type为请求的类型
let header = {};
//post请求
if (type == 'POST'){
header = {
'content-type': 'application/x-www-form-urlencoded'
}
}
//get请求
else{
header = {
'content-type': 'application/json'
}
};
//在请求头中携带token
let token = wx.getStorageSync('token');//读取缓存中的token
header['token'] = token;
return header;
};
//将函数exports出去
module.exports.createHeader = createHeader;
- 页面中使用模块函数
//pages/booklist/booklist.js
//引入模块
var toolFunction = require('../../functions/functions');
//使用创建请求头的函数,以get请求为例
getRequest(){
let header = toolFunction.createHeader('GET');
wx.request({
url: '',
data: data,
header: header,
success: (res)=>{
console.log(res);
}
})
}
2. 常见报错
-
502 Bad Gateway
网关错误,服务器没开或宕机了
-
500 Internal Sever Error
服务器遇到意外的情况并阻止其执行请求
-
400 Bad Request
请求路径有误,检查是否有空格,尽量用手打,不要复制粘贴
在控制台的wxml面板查看发出请求的路径,接口是否打错,携带数据是否格式错误是否传递成功
如果都没有问题,可能是后端设置有误
-
401 Unauthorized
token过期,见 参考方案 | 401错误
三、样式问题
1. 图片大小自适应
问题场景
在给定的图片大小比例不一致的前提下,要将图片渲染到大小固定的框内,不发生偏移压缩
代码实现
<!--pages/index/index.wxml-->
<!-- 给图片开启aspectFill模式即可 -->
<image src="{{imageUrl}}" mode="aspectFill"></image>
/* pages/index/index.wxss */
/* 图片存放框 */
.bookImage{
height: 210rpx;
width: 150rpx;
margin: auto 0;
}
/* 图片 */
image{
/* 图片自适应 */
max-height: 210rpx;
max-width: 150rpx;
}
2. 默认图片设置
问题场景
向后台请求图书信息,返回的图书图片路径可能存在可能不存在,当不存在时渲染默认图片
代码实现
- 在使用到默认图片的.js文件中放置一个变量存放默认图片路径
// pages/booklist/booklist.js
/**
* 页面的初始数据
*/
data: {
defaultImage: '../../image/dedaultImage.jpg'
}
- 在使用到图片的地方进行判断是否使用默认图片
<!--pages/booklist/booklist.wxml-->
<view class="book-item" wx:for="{{booklist}}" wx:key="id">
<view class="bookImage">
<image src="{{item.imageUrl==null?defaultImage:item.imageUrl}}"></image>
</view>
</view>
3. botton样式无法修改
问题场景
在wxss文件中对botton的样式进行修改时,发现类选择器和元素选择器对其都无效
代码解决
发现id选择器有效
<button id="submitBtn" formType="submit">提交</button>
#submitBtn{
width: 300rpx;
height: 90rpx;
background-color: #bafdff;
text-align: center;
line-height: 90rpx;
}
4. 文字自动换行及其他排布
问题场景一 - 文字自动换行
-
给定文字显示框,文字不超过框时,要求文字居中显示
-
文字长度超过文本框长度,要求文字自动换行(小程序中文字不会自动换行)
-
多行文字,超过整个文本框,要求实现在文本框范围内有滚动条可上下滚动
代码实现
.textBox{
width: 400rpx;
height: 300rpx;
border: 1rpx solid #efefef;
/* 文字居中效果 */
display: flex;
justify-content: center;
align-items: center;
/* 文本超出范围换行 */
word-break: break-all;
word-break: break-all;
/* 文字过多出现滚动条 */
overflow: scroll;
}
问题场景二 - 文字超出范围部分省略号显示
- 给定文本框,文字超出超出文本框时,用省略号代替文本
代码实现
<!--pages/AbookStatus/AbookStatus.wxml-->
<view class="book-item" wx:for="{{booklist}}" wx:key="id">
<view class="bookInfo">
<text>书名:{{item.name}}</text>
<text>出版社:{{item.publisher}}</text>
<text>isbn: {{item.isbn}}</text>
<text>作者:{{item.author}}</text>
<text>馆藏数:{{item.rest}}</text>
</view>
</view>
/* pages/AbookBorrowed/AbookBorrowed.wxss */
.bookInfo text{
font-size: 30rpx;
width: 250rpx;
/* 文本省略号代替 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}