图书管理小程序总结

1,194 阅读12分钟

图书管理小程序

一、页面逻辑

1. 动态切换tabbar

问题场景

同一个小程序存在普通用户和管理员两种身份,两种不同的身份需要展示不同的tabbar

解决方法

方法一 - 自定义tabbar

实现过程
优劣分析
  • 适用于需要的tab页不超过5个的需求

  • 容易对tab本身的进行点击事件绑定及其响应,简洁轻便

  • 当需求的tab页面超过五个时,需要对页面重复利用

    如,用户和管理员都有“我的”页面,可以在该tab页的onLoad生命周期函数中添加身份判断函数,从而渲染不同内容,实现一页多用

    但是,上述方法局限于页面渲染内容框架大致相同,差距不会太大的情况

    当页面内容差距很大,不能实现一页多用的时候,自定义tabbar的方法不适用

方法二 - 自定义组件

实现过程
  • 不使用官方提供的tabbar,因此将app.json中关于tabbar的部分删除
  • 将tabbar定义为组件的形式,在每一个页面中使用这个组件
代码实现
  1. 在pages文件夹同级路径下创建components文件夹,存放自定义tabbar组件文件

image-20221215213726-hh4njmt.png

image-20221215213857-quggx61.png

注:共有六个主页面需要放在tabbar中,用户和管理员各三个

   此处我建立了六个组件,每个页面一个

Q:为什么不将用户的三个合并成一个,管理员的三个合并成一个?

A:因为要实现点击tabbar图标颜色改变的效果,需要对自定义组件文件进行修改;

  如果icon图标是以<image src="../../a.jpg">形式写入wxml文件,则需要替换组件wxml文件中的图片,即修改image组件的路径;

  如果icon图标是以<text class="iconfont">写入wxml文件,则需要通过修改组件wxss文件的样式,即修改颜色属性;

  两种方式都需要对组件的文件本身进行修改,不能绑定点击事件通过.js文件进行响应。

  暂时没有想到更好的解决方案。
  1. 编写自定义组件文件
//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;
}
  1. 在主页面中使用组件:
{
  "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实现

效果展示
  1. 用户

image-20221215220746-8qdqn6k.png

  1. 管理员

image-20221215220828-petx6x5.png

优劣分析
  • 适用于需要多余五个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的目标页左上角会有一个返回键,可以返回上一页

image-20221216181558-u9i6le3.png

暂时没找到方法隐藏该按钮

  • wx.redirectTo的目标页左上角会有一个主页键,可以返回主页

image-20221216181749-43t0ho1.png

隐藏此处主页按钮的方法

//目标页的生命周期函数
onShow() {
    wx.hideHomeButton();
},

4. 页面间数据传递与共享

问题场景

主页面有四个图书分类按钮,点击按钮可以分别跳转到四类书籍列表页

四个按钮分别有四个id,根据id不同书籍列表页请求不同书籍数据

现需要将按钮的id从主页面传递到书籍列表页

解决方法

方法一 - 数据缓存本地

见官方文档 微信官方文档 | wx.setStorage微信官方文档 | wx.getStorageSync

使用举例可以见下一节点 请求头token的携带

方法二 - 全局变量

  1. app.js中定义全局变量
//app.js
App({
  onLaunch: function () {
  },
  globalData: {
     id: ''
  }
})
  1. 在主页面设置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',
      })
    },
})
  1. 在图书列表页获取数据
//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

  1. 在pages同级路径下创建store文件夹,创建store文件

image-20221216220254-mh79oma.png

//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;
    }),
})
  1. 主页获取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',
    })
},
  1. 图书列表页获取数据
// 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选择器

问题场景

用户填写信息时,需要在给定的一些数据中选择一个数据,页面实现一个可选框供用户选择

实现效果

image-20221217000406-cqhqfkj.png

代码实现

直接看官方 微信官方文档 | picker选择器

6. 自定义下拉框组件

问题场景

小程序没有下拉框的组件,需要自己自定义组件

实现效果

image-20221217002242-e89e7v3.png

代码实现

  1. 在components文件夹中创建自定义组件文件
  2. 编写自定义组件文件
{
  "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
      })
    }
  }
})
  1. 页面中使用自定义组件
//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的操作,可以将创建请求头的操作封装成一个模块函数

代码实现

  1. 登录页面将后台返回的token本地缓存
//pages/login/login.js
//登录请求成功后的回调函数中
  wx.setStorage({
      key: 'token',
      data: res.data.token//登录成功后后台返回的token
  });
  1. 与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;
  1. 页面中使用模块函数
//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. 常见报错

  1. 502 Bad Gateway

    网关错误,服务器没开或宕机了

  2. 500 Internal Sever Error

    服务器遇到意外的情况并阻止其执行请求

  3. 400 Bad Request

    请求路径有误,检查是否有空格,尽量用手打,不要复制粘贴

    在控制台的wxml面板查看发出请求的路径,接口是否打错,携带数据是否格式错误是否传递成功

    如果都没有问题,可能是后端设置有误

  4. 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. 默认图片设置

问题场景

向后台请求图书信息,返回的图书图片路径可能存在可能不存在,当不存在时渲染默认图片

代码实现

  1. 在使用到默认图片的.js文件中放置一个变量存放默认图片路径
// pages/booklist/booklist.js
/**
   * 页面的初始数据
*/
data: {
   defaultImage: '../../image/dedaultImage.jpg'
}
  1. 在使用到图片的地方进行判断是否使用默认图片
<!--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;
}

效果展示

image-20221216232937-o416swz.png